feat(core)!: Banner content is visible by default; collapse moves to a collapsible prop - #5255
feat(core)!: Banner content is visible by default; collapse moves to a collapsible prop#5255freddymeta wants to merge 5 commits into
collapsible prop#5255Conversation
…`collapsible`
Banner inferred its disclosure from its content: any `children` got a chevron
in the header and were hidden until it was pressed. There was no way to show
content without a toggle -- the case a banner most often wants, a list of the
three fields that failed validation -- and `defaultIsExpanded` was the only
knob, with no controlled mode.
Adding an `isCollapsible` boolean beside `defaultIsExpanded` would have put two
props on one axis, where one silently voids the other. The whole axis is now a
single `boolean | CollapsibleConfig` prop, the shape API Conventions prescribes
and `SideNav.collapsible` set, and the state machine is the shared
`useCollapsible` hook rather than Banner's own `useState`:
<Banner status="error" title="3 fields need attention"> always visible
<Banner collapsible> collapsible, open
<Banner collapsible={{defaultIsOpen: false}}> starts collapsed
<Banner collapsible={{isOpen, onOpenChange}}> controlled (new)
Omitting the prop means no toggle, no aria-expanded/aria-controls, and a
content region that stays mounted.
BREAKING CHANGE: `defaultIsExpanded` is removed, and children with no
`collapsible` prop now render instead of hiding behind a chevron. The
`banner-collapsible-content` codemod (staged in transforms/next) rewrites
`defaultIsExpanded` to the equivalent config and adds
`collapsible={{defaultIsOpen: false}}` to banners that relied on the implicit
collapse, preserving behaviour; those sites also get a TODO comment, because
always-visible content is usually what they want.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…mment
An empty slot -- {false}, {null}, {undefined}, {''} -- is not renderable, so
the old Banner drew no chevron and hid nothing there. Marking those collapsed
invented an affordance rather than preserving one; the transform now mirrors
isRenderable.
The TODO comment goes too. The rewrite is unambiguous and behaviour-preserving,
so the note was advice, not a decision the author has to make -- and advice does
not belong in every migrated file. It moves to the transform title, which the
upgrade CLI prints, and the changelog.
Storybook's Banner stories are now exactly this transform's output plus the new
always-visible demos, so verify-codemods sees the codemod's changes as a subset
of the PR's.
Follows the codemod change: the nudge now lives in the transform title and here, not in every migrated file.
PR Analysis Report📚 Storybook PreviewView Storybook for this PR 🧪 Sandbox PreviewView Sandbox for this PR Modified ComponentsBanner (@astryxdesign/core) · View in Storybook
Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
cixzhang
left a comment
There was a problem hiding this comment.
This is the better pattern but I think we should avoid breakage. Could we:
- keep an internal default for collapsible
- allow users to pass collapsible={false} or collapsible={} in order to customize it?
Review feedback (#5255): keep an internal default for `collapsible` and let consumers pass `false` or a config, so nothing breaks silently. `collapsible` now defaults to `true`, and Banner supplies `defaultIsOpen: false` to the shared hook — the one place it departs from `useCollapsible`, which opens by default. A banner's message lives in its header and the content is supplementary, and this is the behaviour Banner has always had. So a call site that never mentioned the collapse axis renders exactly as before: chevron, content closed. What remains breaking is the prop rename: `defaultIsExpanded` is gone in favour of `collapsible={{defaultIsOpen: true}}`. That is a type error at every call site, not a silent behaviour change, and the codemod covers it. The codemod shrinks to match: it no longer touches banners that never set the prop, and `defaultIsExpanded={false}` is simply dropped, since starting closed is the default. Its docblock states the one scope boundary — the prop inside a props object is left to the compiler.
|
Done — the default is back, and the only thing left breaking is the prop name.
So the axis reads: <Banner status="warning" title="Config changed">…</Banner> // unchanged
<Banner collapsible={false}>…</Banner> // content, no toggle
<Banner collapsible={{defaultIsOpen: true}}>…</Banner> // was defaultIsExpanded
<Banner collapsible={{isOpen, onOpenChange}}>…</Banner> // controlled
One scope boundary worth flagging: the codemod is JSX-only. |
The gap
Banner infers its disclosure from its content: any
childrenget a chevron inthe header and are hidden until it is pressed. There is no way to have content
and no toggle — which is the case a banner most often wants, the list of the
three fields that failed validation — and
defaultIsExpandedis the only knob,with no controlled mode.
The shape
Adding an
isCollapsibleboolean besidedefaultIsExpandedwould put two propson one axis, where one silently voids the other — the prop-independence
principle, and precisely the clutter API Conventions § Boolean-or-Config
Props
names (
isCollapsible+defaultIsCollapsed+onCollapsedChange). So thewhole axis becomes one prop, the shape
SideNav.collapsibleset:The default is unchanged.
collapsibledefaults totrue, and Banner handsthe hook
defaultIsOpen: false— the one place it departs fromuseCollapsible,which opens by default. A banner's message lives in its header and its content is
supplementary, and that is the behaviour Banner has always had.
Three deliberate choices:
CollapsibleConfig, not a Banner-only vocabulary. The exported type fromuseCollapsible— so a banner's disclosure is configured exactly likeCollapsible's, and there is no third spelling of the same three fields.useState. Banner no longer owns collapsestate at all (Use the System, rule 3). Controlled mode comes for free, which
is why it is in this PR rather than a later one.
collapsible={false}is the opt-out; there is nothing else on the axis to contradict it.
Lint-wise the
boolean | Configunion is exempt from@astryx/boolean-prop-naming, so the unprefixed name is legal; a bareisCollapsible: booleanwould not have been.With
collapsible={false}there is no toggle, noaria-expanded/aria-controls, and the content region stays mounted with noid.Evidence
Real builds, shot in Chromium —
beforefrommainat 5ab0625,afterfromthis branch. Harness and DOM facts on the
assets branch
(delete it after merge).
Nothing moves for existing code. A banner with children and no collapse prop,
and one that starts open, are byte-identical PNGs across the two builds
(
dfbfe0a7…and147e808b…):collapsible={false}is the new capability. Before, that content wasunreachable behind a chevron; after, it is simply there.
Read off the DOM: before
{buttons: ["Expand"], listItems: 0, ariaExpanded: ["false"]}, after{buttons: [], listItems: 3, ariaExpanded: []}.What breaks, and the codemod
Only the prop rename:
defaultIsExpandedis gone in favour of the config. Thatis a type error at every call site, not a silent behaviour change — nothing
changes under anyone without a compiler error first.
Codemod:
npx astryx upgrade --codemod banner-collapsible-content(staged in
transforms/next/per Release Process § Writing acodemod).
defaultIsExpanded/={true}collapsible={{defaultIsOpen: true}}defaultIsExpanded={false}defaultIsExpanded={expr}collapsible={{defaultIsOpen: expr}}It only fires on a
Bannerimported from@astryxdesign/core—defaultIsExpandedis aChatToolCallsprop too — and declines to guess arounda spread. One deliberate scope boundary: the prop inside a props object (a
Storybook
args) is left alone rather than rewritten on a guess about whichcomponent the object feeds; the removed prop makes those a type error, so the
compiler finds them.
verify-codemodspasses as a superset — Storybook's Bannerstories are this transform's own output plus the new opt-out demos, with the
argssites migrated by hand.Accessibility note
Content that is always visible inside
role="alert"means an error bannerannounces its detail along with its title. Usually what you want for a list of
validation errors; worth knowing when reaching for
collapsible={false}. TheBanner.doc.mjsbest practices now say to use it when the user needs the contentto act on the message, and to keep the toggle when the detail is long enough to
bury the banner's own message.
Test plan
pnpm test— 536 files / 10946 tests pass. Banner is 47 tests (8 new orrewritten: default-unchanged, explicit
true,collapsible={false}, thedisclosure wiring absent when opted out,
defaultIsOpen: true,onOpenChange, controlled). Codemod is 12 tests.pnpm lint:strict— 0 errors (56 pre-existing warnings, none in touched files).pnpm build— clean.pnpm -F @astryxdesign/core typecheck— clean.node .github/scripts/codemod-verify.mjs— reproducible.Also updated:
Banner.doc.mjs(en + zh + dense, props, anatomy, best practices),stories, and the
BannerCollapsibleContentshowcase block.