Repository navigation
feat(Resizable): resizable takes useResizable's output on LayoutPanel, ResizeHandle and SideNav, and focus leaves a collapsing panel - #9300
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| LayoutFooter | 5.9 kB (0) · 1.6 kB CSS (0) | — | ✓ · ✓ · ✗ |
| LayoutHeader | 6.0 kB (0) · 1.6 kB CSS (0) | — | ✓ · ✓ · ✗ |
| LayoutPanel | 20.3 kB (+1.2 kB) · 4.9 kB CSS (0) | — | ✓ · ✓ · ✓ |
9 more components
| Component | Size | LoC | Visual · A11y · RTL |
|---|---|---|---|
| AppShell | 75.2 kB (+984 B) · 12.2 kB CSS (0) | — | — · — · — |
| ResizeHandle | 20.4 kB (+840 B) · 4.3 kB CSS (0) | — | ✓ · ✓ · ✓ |
| SideNav | 75.3 kB (+610 B) · 10.1 kB CSS (0) | — | ✓ · ✓ · ✓ |
| Layout | 6.8 kB (0) · 2.3 kB CSS (0) | — | ✓ · ✓ · ✓ |
| LayoutContent | 17.4 kB (0) · 1.7 kB CSS (0) | — | ✓ · ✓ · ✓ |
| SideNavCollapseButton | 46.7 kB (0) · 8.8 kB CSS (0) | — | ✓ · ✓ · ✓ |
| SideNavHeading | 60.6 kB (0) · 10.0 kB CSS (0) | — | ✓ · ✓ · ✓ |
| SideNavItem | 55.6 kB (0) · 9.8 kB CSS (0) | — | ✓ · ✓ · ✓ |
| SideNavSection | 6.6 kB (0) · 3.8 kB CSS (0) | — | ✓ · ✓ · ✓ |
Package size: @astryxdesign/core 6.47 MB packed (+9.0 kB); no new dependencies.
API changes: core +0 · ~3 · −0
Committed API report behind this build: cli 9 rows, core 4 rows; run pnpm api:report.
3 API changes (advisory)
| Package | Where | Signature | Usage | |
|---|---|---|---|---|
| ~ | core |
. · Components · component LayoutPanel |
prop resizable?: ResizableProps → prop resizable?: ResizableRegion | ResizableProps |
<LayoutPanel resizable={…} /> |
| ~ | core |
. · Components · component ResizeHandle |
prop resizable?: ResizableProps → prop resizable?: ResizableRegion | ResizableProps |
<ResizeHandle resizable={…} /> |
| ~ | core |
. · Components · component SideNav |
prop resizable?: boolean | ResizableConfig = false → prop resizable?: boolean | ResizableConfig | ResizableRegion = false |
<SideNav resizable /> |
Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.
Generated by PR Enrichment workflow | View full report
There was a problem hiding this comment.
The slide and the focus move match the current component:Resizable FR8 and component:LayoutPanel FR7 and AR2 records, but approval waits on one spec decision and three fixes. The head rewords component:Resizable FR3 so that createResizable is the owner ResizeHandle goes through, which no current record states. An engineering owner needs to approve or reject that wording by commenting /approve-spec with the full head SHA; the author may do this if they are an engineering owner. Independently of that decision, please address:
- An end
LayoutPanelreversed by<ResizeHandle isReversed>whose region starts collapsed is neitherinertnoraria-hiddenuntil the factory's next update, because the factory looks for the pane before the handle reports its reversal (findPaneandconfigureincreateResizable.ts). - Move focus with the shared
focusForLastInput(utils/focusReturn.ts) instead of a raw.focus()inLayoutPanel.tsx. - The diff carries about 8,000 authored lines from three stacked changes; land the lower pull requests first so this one carries only its own commits.
[Automated review]
| const binding: ResizeHandleBinding = { | ||
| gesture, | ||
| read: () => view as ResizeHandleView, | ||
| configure(config) { |
There was a problem hiding this comment.
configure only refreshes the view. The factory's first commit() already ran findPane() with isReversed() false, so an end panel reversed by <ResizeHandle isReversed> is not found, and is not held collapsed or made inert, until the next commit. A region that starts collapsed leaves that panel reachable by Tab (component:LayoutPanel AR2). Re-finding the pane here when reversal changes (for example by calling commit()) fixes it.
There was a problem hiding this comment.
Fixed in ff985c2: a ResizeHandle's own isReversed makes the factory look for its pane on the other side, so an end panel whose region starts collapsed is inert from its first commit. A test covers it.
| const element = elementRef.current; | ||
| const active = element?.ownerDocument.activeElement; | ||
| if (element != null && active != null && element.contains(active)) { | ||
| (paneLink.focusTarget() ?? resizeHandleBeside(element, area))?.focus(); |
There was a problem hiding this comment.
This programmatic move bypasses focusForLastInput (utils/focusReturn.ts), which owns focus moves under architecture:interaction-modality INV1 and architecture:react-component-runtime INV16. Calling it here keeps this path on the shared pointer handling.
There was a problem hiding this comment.
Fixed in ff985c2: the move goes through moveFocusLeftBehind, which moves focus with focusForLastInput.
8c85319 to
93f3dfa
Compare
There was a problem hiding this comment.
This pull request isn't ready to approve: it waits on one spec decision, and three fixes are needed regardless. The head rewords component:Resizable FR3 so that createResizable is the owner ResizeHandle and useResizable go through, which no current record states; an engineering owner needs to approve or reject that wording by commenting /approve-spec with the full head SHA, and the author may do so if they are an engineering owner. Independently of that decision, please address:
- Keep a region's size and collapse state across an
<Activity mode="hidden">hide and show: bothuseResizableforms now come back at their defaults because the layout-effect cleanup destroys the factory and the next one starts from the options alone (useResizable.ts), where the base kept both. - Replace ResizeHandle's two ref-guarded warning effects with the shared
useDevWarninghook. - Land #9297 first and retarget so this pull request carries only its own slide and focus commits; the diff is about 7,400 authored lines today.
[Automated review]
| }); | ||
| useIsomorphicLayoutEffect( | ||
| () => () => { | ||
| slot.get()?.resizable.destroy(); |
There was a problem hiding this comment.
This cleanup destroys the factory that holds the region's size and collapse state. When <Activity mode="hidden"> hides the tree and shows it again, the create effect above builds a new factory from the options alone, so the region comes back at defaultSize and defaultIsCollapsed; the base's configuration-object form kept both in React state. Carrying readResizableSelection(...) across this cleanup, as the element-swap path does, would keep them.
There was a problem hiding this comment.
Fixed in #9297: the factory keeps the region while an <Activity> hides its component, so the region comes back at its size and collapse state.
| if (isDisabled || !resizable || !resizable._collapsible) { | ||
| return; | ||
| }, [direction, regionDirection]); | ||
| useEffect(() => { |
There was a problem hiding this comment.
This ref-guarded useEffect + devWarn (and the axis warning above it) is what useDevWarning(component, message, condition) in hooks/useDevWarning.ts already does once per mount. Calling the shared hook keeps one path for dev warnings.
93f3dfa to
3522fbf
Compare
There was a problem hiding this comment.
This pull request isn't ready to approve: it waits on one spec decision, and several fixes are needed regardless. The head rewords component:Resizable FR3 so that createResizable is the owner ResizeHandle and useResizable go through, which no current record states; an engineering owner needs to approve or reject that wording by commenting /approve-spec with the full head SHA, and the author may do so as an engineering owner. Independently of that decision, please address:
- Route LayoutPanel's focus move through
useFocusLeavingChangeandmoveFocusLeftBehind, whichmainnow uses for SideNav and Collapsible, instead of a second layout effect. - Move focus out of, and make inert, a
LayoutPanelsized bywidth={region.size}from the configuration-object form, which still keeps focus when it collapses (component:LayoutPanelAR2). - Replace ResizeHandle's remaining ref-guarded axis warning with
useDevWarning. - Land the stacked #9297, #9370 and #9372 first and retarget, so this pull request carries only its own commits; the diff is about 7,800 authored lines today.
[Automated review]
| if (!isRegionCollapsed && isSettledCollapsed) { | ||
| setIsSettledCollapsed(false); | ||
| } | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This effect re-implements the focus-leaving sequence that main now owns in useFocusLeavingChange (utils/useFocusLeavingChange.ts) with noteFocusWithin and moveFocusLeftBehind (utils/focusReturn.ts), which SideNav and Collapsible call from their LayoutChangeSnapshot. architecture:react-component-runtime INV16 asks for one call to the shared owner and no component-level effect for the concern. After rebasing, note focus before the collapse with useFocusLeavingChange, move it with moveFocusLeftBehind(note, paneLink.focusTarget() ?? resizeHandleBeside(panel, area), panel), and keep the settle state only for inert.
There was a problem hiding this comment.
Fixed in ff985c2: LayoutPanel notes focus with useFocusLeavingChange, keyed on the region's collapse state, and moves it with moveFocusLeftBehind. The settle state is only for inert.
| paneLink.isCollapsed, | ||
| returnFalse, | ||
| ); | ||
| const isFoundBeside = resizable == null; |
There was a problem hiding this comment.
A panel given width={region.size} from the configuration-object form, beside <ResizeHandle resizable={region.props}> (the shape of the HorizontalSplit story), lends itself here, but no controller ever finds it: the configuration-object factory has no handle, and this panel has no resizable prop. Collapsing it from a button inside leaves focus there, and the panel is neither inert nor aria-hidden, which component:LayoutPanel AR2 asks for under any resize controller. The ResizeHandle given resizable props could tell the panel beside it the region's collapse state for the focus move and inert only, with no slide.
There was a problem hiding this comment.
Fixed in ff985c2: a ResizeHandle given resizable props holds the LayoutPanel beside it collapsed, so a panel at width={region.size} moves focus to the handle and goes inert. A test fails before the fix.
| hookDirection !== direction && | ||
| !didWarnAxisRef.current | ||
| ) { | ||
| if (didWarnAxisRef.current) { |
There was a problem hiding this comment.
This ref-guarded effect is what useDevWarning(component, message, condition) already does, and the call just below uses it. useDevWarning('ResizeHandle', <the same message>, regionDirection != null && regionDirection !== direction) replaces didWarnAxisRef and the effect.
3522fbf to
c5b5bca
Compare
There was a problem hiding this comment.
This pull request isn't ready to approve: it waits on one spec decision, and several fixes are needed regardless. The head rewords component:Resizable FR3 so that createResizable is the owner ResizeHandle and useResizable go through; an engineering owner needs to approve or reject that wording by commenting /approve-spec <full head SHA> (the author may do so as an engineering owner), or FR3 keeps its current wording here. Independently of that decision:
- Move LayoutPanel's focus through
main'suseFocusLeavingChangeandmoveFocusLeftBehindinstead of its own layout effect (LayoutPanel.tsx:302). - A
LayoutPanelgivenwidth={region.size}from the configuration-object form, beside<ResizeHandle resizable={region.props}>, still keeps focus and stays reachable when it collapses, whichcomponent:LayoutPanelAR2 covers; fix it or narrow the[fix]changeset. - At 7,963 authored lines the diff carries all of #9297, #9370 and #9372; land those first so this pull request carries only its own commits.
- Replace ResizeHandle's ref-guarded axis-warning effect with
useDevWarning(ResizeHandle.tsx:557).
[Automated review]
| if (!isRegionCollapsed && isSettledCollapsed) { | ||
| setIsSettledCollapsed(false); | ||
| } | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This effect re-implements the focus-leaving sequence main owns in useFocusLeavingChange (utils/useFocusLeavingChange.ts) with noteFocusWithin and moveFocusLeftBehind (utils/focusReturn.ts), which SideNav and Collapsible call from their LayoutChangeSnapshot's onBeforeChange (architecture:react-component-runtime INV16). After rebasing, note focus with useFocusLeavingChange keyed on the region's collapse state and move it with moveFocusLeftBehind(note, paneLink.focusTarget() ?? resizeHandleBeside(panel, area), panel); keep the settle state only for inert.
There was a problem hiding this comment.
Fixed in ff985c2: LayoutPanel notes focus with useFocusLeavingChange, keyed on the region's collapse state, and moves it with moveFocusLeftBehind. The settle state is only for inert.
| paneLink.isCollapsed, | ||
| returnFalse, | ||
| ); | ||
| const isFoundBeside = resizable == null; |
There was a problem hiding this comment.
A panel given width={region.size} from the configuration-object form, beside <ResizeHandle resizable={region.props}> (the Collapsible story's shape), lends itself here, but the configuration-object factory has no handle and never looks for it (createResizable.ts:832). It keeps focus and stays reachable when it collapses, which component:LayoutPanel AR2 covers ("the configuration-object form (FR7) included").
There was a problem hiding this comment.
Fixed in ff985c2: a ResizeHandle given resizable props holds the LayoutPanel beside it collapsed, so a panel at width={region.size} moves focus to the handle and goes inert. A test fails before the fix.
| // means there is no region axis to compare, not that it is horizontal. | ||
| const didWarnAxisRef = useRef(false); | ||
| const regionDirection = resizable?._direction ?? view?.direction; | ||
| useEffect(() => { |
There was a problem hiding this comment.
This ref-guarded effect is what useDevWarning(component, message, condition) already does, and the call just below uses it: useDevWarning('ResizeHandle', <the same message>, regionDirection != null && regionDirection !== direction), deleting didWarnAxisRef and this effect.
c5b5bca to
ff985c2
Compare
There was a problem hiding this comment.
This pull request isn't ready to approve: it waits on two spec decisions, and three fixes are needed regardless.
Spec decisions. An engineering owner records each by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.
- The head rewords
component:ResizableFR3 so thatcreateResizableowns the drag, keyboard, collapse, snapping, persistence and size behavior that ResizeHandle anduseResizablego through; no current record says ResizeHandle is built on the factory, so either that wording is approved or FR3 keeps its current text. - The Grab zone now stays centred on the divider whatever
pillPlacementsays, shrinks to half of a pane shorter than--spacing-4, and lets the painted pill take the pointer, none of which current FR1 states.
Required changes:
ResizeHandle.tsx:617–644copiescreateResizable's find-and-hold-the-pane sequence (createResizable.ts:860–881and:1200–1207), so move it into one helper both call and subscribe to pane changes only while the handle has a region.- The Grab zone clamp contradicts FR1's
--spacing-4strip on short panes, so split the Grab zone change into its own pull request with an FR1 amendment, or drop it here. - At 9,917 authored lines (7,519 from the stack below and 2,435 of this pull request's own), the diff is far over the 2,000-line scope limit, so land the stack first and retarget this pull request to its own commits.
The slide, the focus move and the new factory otherwise match component:Resizable FR8, component:LayoutPanel FR7 and AR2, and the public API record. Non-blocking: two changesets still call the factory internal, and ResizablePaneLink.render is never called.
[Automated review]
| const heldPaneRef = useRef<ResizablePane | null>(null); | ||
| const isRegionCollapsed = resizable?._isCollapsed ?? false; | ||
| useEffect(() => watchResizablePanes(rerender), []); | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This repeats createResizable's findPane/setPane (createResizable.ts:860–881) and its pane watcher (:1200–1207): find the lent panel beside the handle, release the old one, hold the new one with the region's collapse state. Two copies already read reversal differently. Could both call one helper in resizablePane.ts (or could a handle given the hook's own props hand its element to the region, so the factory's findPane serves it)? The watcher on :625 also runs for every ResizeHandle, region or not.
There was a problem hiding this comment.
Done in e210af4: ResizeHandle and createResizable find the pane with resizablePaneFor and move their hold with handOverPane. A ResizeHandle watches for lent panes only while it has a region.
| // An inline Handle's zone is centred on its line, so half of it reaches into | ||
| // the pane. On a pane shorter than the zone, that is at most half the pane. | ||
| hitAreaLengthX: (pane: number) => ({ | ||
| width: `min(${spacingVars['--spacing-4']}, ${pane}px)`, |
There was a problem hiding this comment.
component:Resizable FR1 states the Grab zone is "a --spacing-4 strip on the resize axis"; this caps it at the pane's length, so a 12px item gets a 12px zone. The centring, this clamp and the pill as a pointer target need an FR1 amendment an engineering owner approves; they are separable from the slide, so they could move to their own pull request.
There was a problem hiding this comment.
FR1 states it now: the owner-approved amendment in #9529, which this carries.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] The configuration-object `useResizable` builds on one internal resize region, through the same size model as before, so the region's state has one implementation. |
There was a problem hiding this comment.
Non-blocking: this and create-resizable-plain-handle.md call the factory internal, while create-resizable.md announces it as public in the same release. Folding them into the public entry keeps the release notes consistent.
ff985c2 to
44ba216
Compare
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet, because the pull request is a draft from a fork; once it is ready for review, a maintainer adds the ci:run label so CI runs.
The widened resizable slot and SideNav's region path follow the component:Resizable FR9, component:LayoutPanel FR8 and component:SideNav FR7 rows an engineering owner approved at this head, and the focus move meets component:LayoutPanel AR2, shown by new tests that fail on main and pass here and by a Chromium check. Three non-blocking suggestions are inline: two comment cleanups and isolating the console.warn spies in resizableSlot.test.tsx.
[Automated review]
| }); | ||
|
|
||
| const collapsed = resizableHook.isCollapsed; | ||
| // The region that sizes the rail: the caller's, or SideNav's own. |
There was a problem hiding this comment.
Non-blocking, missed earlier (INV29): this restates const region = ownerRegion ?? resizableHook on the next line; it can go.
There was a problem hiding this comment.
Agreed. This head is approved and ready, so it takes no more pushes; the line goes in a follow-up after it merges.
| return sibling == null ? null : (lent.get(sibling) ?? null); | ||
| } | ||
|
|
||
| /** The lent panel a handle sizes: before it, or after it when reversed. */ |
There was a problem hiding this comment.
Non-blocking, missed earlier (INV29): resizablePaneBeside above is exported but only this function calls it, and both JSDoc blocks say the same thing. Folding them into one function with one comment would remove the repeat.
There was a problem hiding this comment.
Agreed. #9989 replaces this module when it rebases after this merges, and folds the two functions into one there.
| const pane = () => screen.getByTestId('pane'); | ||
| const separator = () => screen.getByRole('separator', {name: 'Resize pane'}); | ||
|
|
||
| afterEach(() => { |
There was a problem hiding this comment.
Non-blocking, missed earlier: the console.warn spies are restored at the end of each case body, so a failing case leaves its spy behind and the next case reads its calls (against the base code the boolean-and-config guard fails only for that reason). Adding vi.restoreAllMocks() here would isolate the cases.
There was a problem hiding this comment.
Agreed: a failing case should not leak its spy into the next. This head is ready, so it takes no more pushes; the vi.restoreAllMocks() cleanup goes in a follow-up after it merges.
There was a problem hiding this comment.
Done in #10032, with the leak shown before and after.
… panel is inert Cases for a LayoutPanel beside its ResizeHandle. When the region collapses with focus inside the panel, focus moves to the handle, with the ring the last input calls for, and the panel is inert and hidden from assistive technology until it opens. Opening moves no focus, and a collapse started from the handle or from outside the panel leaves focus where it is. This holds for the hook's own props, a hand-built object, a reversed end panel, and a panel at the hook's size beside a handle given the props, from the first commit when the region starts collapsed. A panel with no resize controller is never inert. These cases fail until the next commit.
…panel is inert A collapsing LayoutPanel moves focus inside it to its ResizeHandle, through the focus-leaving sequence that disclosures share, so the ring follows the last input. A commit later the panel goes inert and `aria-hidden` until it opens, so inert never drops that focus to the page. The panel reads the collapse state from its `resizable` props, or, with no `resizable`, from the ResizeHandle beside it: a new internal module, resizablePane.ts, lets a panel at the region's size lend its element to that handle, which holds it collapsed with the region. ResizeHandle holds the shared modality tracker and registers its element, through a callback ref, for the panel to find. No prop, option, export or type changes.
…ile collapsed The `resizable` and `width` descriptions, in English, Chinese and dense form, say that focus moves to the handle as a panel collapses and that the panel is inert while collapsed. A `[fix]` changeset says the same.
…ollapsed panel as missing AR2's implementation gap closes: focus leaves a collapsing panel, and the collapsed panel is inert. The current-state note keeps only the slide (FR7), which is still missing.
…ble's output in `resizable` Cases for each `resizable` slot given the hook's output directly, beside the same cases for its `props`: the panel takes the region's size, the handle drives the region, and a collapse makes the panel inert. SideNav given the output sizes its rail from the region and follows the region's collapse state, and its toggle calls the region's `collapse` and `expand`. With the hook's output, the region's own `collapsible` option decides whether the rail collapses. When `collapsible` sets its own collapsed state, or disagrees with the region's option, the region wins and SideNav warns in development, naming the region; when they agree it warns about nothing. A guard case keeps the boolean and config forms as they were: `collapsible` turns collapse on beside them, and only collapse-state keys conflict, with `resizable` winning; it passes on main too. The cases for the output fail until the next commit.
…ble's output in `resizable`
Additive: `resizable` on LayoutPanel and ResizeHandle takes the hook's
output directly, `const resizable = useResizable({…});
<LayoutPanel resizable={resizable}>`, and still takes its `props`.
SideNav's `resizable` also takes the output, beside `true` and its config
object. The caller's region then owns the width and the collapse state:
the rail follows its `isCollapsed`, the toggle calls its `collapse` and
`expand`, and its `autoSaveId` saves the width. With the hook's output,
the region's own `collapsible` option decides whether the rail
collapses. When `collapsible` sets its own collapsed state, or disagrees
with the region's option, the region wins and SideNav warns in
development, as `resizable`'s keys already override `collapsible`'s.
With `true` or a config object, SideNav behaves exactly as before.
…any element with it
The docs and stories name the hook's value `resizable` and pass it
directly: `resizable={resizable}`. LayoutPanel's, ResizeHandle's and
SideNav's prop docs say `resizable` takes the output, or its `props`,
and SideNav's say its region then owns the width and the collapse state.
A new Resizable example sizes a Card that is not a LayoutPanel with the
hook's `size`, through a StyleX dynamic style, beside a ResizeHandle. A
`[feat]` changeset describes the widened prop.
…d SideNav's region wins component:Resizable FR9: `resizable` takes useResizable's output directly, the region's `props` is still accepted, and components take the hook's value through that slot prop (owner decision, 2026-11-04). component:LayoutPanel FR8 states it for the panel. component:SideNav FR7 states it for SideNav, with the owner's rule for a region beside `collapsible`'s own collapsed state: the region wins, and SideNav warns in development. FR7 adds the spec owners' sentence for the hook's output only: the region's own `collapsible` option also decides whether the rail collapses. Each record's verification table gains the row for its new requirement. The three records' Builder impact, Compatibility and public-contract lines name the widened `resizable` as a patch `[feat]`; LayoutPanel's Compatibility row keeps its cell width, so no other row re-pads.
6f21e03 to
acbcf08
Compare
|
/approve-spec acbcf08 |
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet, because the pull request is a draft from a fork; once it is ready for review, a maintainer adds the ci:run label so CI runs.
The widened resizable slot and SideNav's region path follow the component:Resizable FR9, component:LayoutPanel FR8 and component:SideNav FR7 rows an engineering owner approved at this head, and the focus move meets component:LayoutPanel AR2, shown by new tests that fail on main and pass here and by a Chromium check. The rebase onto main merges the scrolling-panel naming into LayoutPanel's ref correctly. The three non-blocking suggestions from the earlier review (two comment cleanups and isolating the console.warn spies in resizableSlot.test.tsx) still apply as follow-ups.
[Automated review]
On main; it replaces the createResizable stack (#9370, #9372, #9202, closed), with no
create…export orhandleRef. Per #9973, a slot-prop hook is not a second API (INV21) and needs nocreate…twin (INV22). The owner scoped the focus fix in. #9989, the slide, is a separate pull request that lands after this one; these 1,222 authored lines are this one's own.User need
useResizablereturns a region, butresizabletook only itsprops, and SideNav took no region. A collapsing LayoutPanel kept focus in a zero-width panel still in the Tab order.Problem and solution fit
resizabletake the hook's output (resizable={resizable}); itsprops,trueand SideNav's config still work.isCollapsed, the toggle callscollapse/expand, itsautoSaveIdsaves the width, and its owncollapsibledecides whether the rail collapses; acollapsiblethat disagrees or sets a collapsed state is ignored, with a development warning.trueand the config behave as before.aria-hiddenuntil it opens.Current specification
component:ResizableFR9,component:LayoutPanelFR8,component:SideNavFR7 (owner decision, 2026-11-04).component:LayoutPanelAR2 (its gap closes);architecture:public-component-apiINV21, INV22.Public delta and caller burden
Widened prop types only, the three api-report lines:
ResizableRegion | ResizablePropson LayoutPanel and ResizeHandle,| ResizableRegionon SideNav. No new export. Changesets:[feat]widenedresizable,[fix]focus move.Design and alternatives
resizablePropsOfreads a region'sprops(one path per component).useFocusLeavingChange; ResizeHandle holds the modality tracker (LayoutPanel PR1).Evidence
sizethrough a StyleX dynamic style. Template rubric 1.4: purity 30, icons 15, CSS 12/15 (the dynamic width), layout 15, images 5, code 10.Writing rubric results
Over the Resizable, LayoutPanel and SideNav records: three new rows carry the owner's lines, each with a verification row; compatibility lines name the widened type as a patch
[feat]; AR2's gap goes. No re-pad. Owner-gated.Scope
Testing
Local checks at this head, on main c5b2c54:
CI=true pnpm lint(0 errors), check:changesets, check:knowledge, core typecheck, prettier on the changed files, the comment-length check, andpnpm review:pre(1,222 authored lines, no findings).pnpm build,pnpm api:reportchanges nothing beyond the three committed prop lines, apart from main's own stalepresentationlines, which stay out.grabZone,ResizeHandleandSideNavRailMotionin WebKit (23 passed, 12 skipped); the Resizable, Layout and SideNav a11y contracts plusscrollRegionName.a11y.chromium.spec.tsin Chromium (47 passed).paneFocus.test.tsxcases fail before the focus fix; 8 of 10resizableSlot.test.tsxcases fail against main's code. The boolean and config-form guard passes on main and here, as do main's own SideNav collapse-ownership and conflict-warning suites.