Repository navigation
feat(Resizable): a resizable LayoutPanel slides closed and open, and focus leaves a collapsing panel - #9300
feat(Resizable): a resizable LayoutPanel slides closed and open, and focus leaves a collapsing panel#9300vjeux wants to merge 22 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
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.
| 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.
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.
| 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.
…t (spec:AST-107 FR6) Red: there is no createResizable yet. The tests drive a plain element through the factory: its role, name, value and tab stop, a pointer drag that holds its pointer, the keys, collapse and restore, a controlled collapse, disabled input, update and subscribe, a percentage basis, persistence, destroy mid-drag, a region with no handle yet, and an import that does no DOM work.
…ain element a window splitter that resizes a region, without React (spec:AST-107 FR6) createResizable(handle, options) gives the element role=separator, a tab stop and the catalog's English name when it has none of its own, its orientation and value, and answers a pointer drag that holds its pointer (so an embedded frame cannot swallow the drag), the arrow keys, Home, End, and Enter and a double-click to collapse and restore. It resolves, clamps, snaps and saves through resizableModel.ts, as useResizable does, reports the size through subscribe and onSizeChange, takes new options through update, and destroy removes every listener, observer and attribute it added. With a null handle the region still answers collapse, expand, resize and subscribe. The input lives in resizeGesture.ts. Importing either module does no DOM work. It is not exported from the package, so the changeset records it as maintenance.
93f3dfa to
3522fbf
Compare
…ions update() replaces the options, as a new render does, so an option left out returns to its default; the region keeps its size and collapse state within the new options. Its JSDoc said the handle and the region keep their state, which read as a merge. A test pins the contract: a full set with one change keeps the selection, the name and the bounds, and a set that leaves options out returns them to their defaults.
…he region's state has one copy (architecture:public-component-api INV22) Red: the configuration-object form creates no factory yet. With a stand-in factory, the tests check that it creates one, with no handle, from the configuration, passes later options through update(), destroys it on unmount and returns what it reports, in its size, collapse state and props. Guards that pass on the current implementation and must keep passing on the factory: the form reports size and isCollapsed in the commit of onCollapseChange's own state, controlled or not, through collapse(), Enter, expand(), double-clicks and a drag past the threshold (component:Resizable FR8); it keeps its size and collapse state across an <Activity> that hides and shows it; and a configuration object kept across renders follows a container swapped behind containerRef.
…and ResizeHandle's `resizable` path goes through its input The configuration-object form is createResizable with no handle of its own, so the region's state has one copy, the factory's (spec:AST-107 FR6; architecture:public-component-api INV22). The props it returns carry what the factory reports, and a ResizeHandle given them drives the factory's region through its model, read at each input so a whole drag reaches the factory as it is now. ResizeHandle's input for a `resizable` prop is the factory's input (resizeGesture.ts) over the props, so the handle has one implementation of the drag, the keys and the double-click. Behavior unchanged: size and isCollapsed land in the commit of onCollapseChange's own state, a controlled isCollapsed included (FR8); the region keeps its size across an <Activity> (the hook's store remembers where it stood and seeds the next factory), and follows a container swapped behind containerRef with the same configuration. React's store subscriptions do not read the store again after an <Activity> shows its subtree, so the hook renders again from the layout phase when the factory made for the reveal reports something new. The factory stays internal, so no public API or record changes; the changeset records the move.
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.
| 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.
| 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.
…ontainer after an <Activity> hid and showed the region before then (spec:AST-010) Red: while the container is not laid out, the temporary 1200px basis stands in, so a '30%' default shows 360px. A factory made for an <Activity> reveal took that stand-in as the region's selection, kept 360px after the container measured 800px, and saved it, where the default should resolve to 240px against the measured container and the temporary basis is never final or saved.
…ry basis's stand-in as the region's size (spec:AST-010) readResizableSelection reports the size only when it is a selection (saved, chosen, or the default resolved against a measured basis); while only the temporary 1200px basis stands in, it reports none, and the hook seeds the next factory with the collapse state alone, so the factory resolves its own default once the container measures. This holds for a factory made after an <Activity> hides and shows the region.
…reateResizable, and ResizeHandle renders the factory bound to its element (spec:AST-107 FR6) Red: there is no ref-taking useResizable yet. The tests check that it creates the factory on the element, passes later options through update(), destroys it on unmount and returns what it reports, in the commit that onCollapseChange's own state lands in, controlled or not (component:Resizable FR8); that a ResizeHandle given the ref renders the region and forwards its drag, keys and double-click to the factory, with its own isReversed and isDisabled, after a caller's onKeyDown (FR7); that a handle a child mounts from its own state, or hides and shows in its own <Activity>, is bound at once, and the region keeps its size across an <Activity> that hides the hook's own component; that a plain element drops its listeners when it unmounts and is named and announced through the message catalog; that the same options follow a container swapped behind containerRef; and that the hook renders its initial size on the server.
…s thin React wrapper, and ResizeHandle renders the factory bound to its element (spec:AST-107 FR6) The hook creates the factory on the element behind the ref as it mounts, passes later options through update(), destroys it as it unmounts, and returns what the factory reports, in the commit of the parent's own state, a controlled isCollapsed included (component:Resizable FR8). The factory follows the element behind the ref at every commit, and whenever a ResizeHandle given the ref attaches or lets go of its element, which a child can do without the hook's component rendering; each new factory starts where the region stood. A plain element is named and announced through the provider's catalog messages, which reach the factory through the package's internal channel. A ResizeHandle given the ref claims its element, so the factory binds to it instead of driving it: the handle renders the factory's region and forwards its pointer, keyboard and double-click events to the factory's input, after a caller's onKeyDown (FR7), and its own isReversed and isDisabled apply with the factory's options. A factory stays bound when the handle lets go of its element and claims it again. The handle follows the binding from the layout phase, so a factory bound in the commit that mounts or shows it renders before paint. A ResizeHandle with a `resizable` prop and a bound region warns once (useDevWarning).
…report rows and its changeset useResizable's page documents the handle ref passed first, with its options, and that a ResizeHandle reports its element as it mounts while a plain element is read when the hook's own component renders. UseResizableResult is exported, and the core API report gains the overload and the type (only those rows; on this base the committed report lacks rows that a later main refreshed).
… drag-across-a-frame proof and API report rows (spec:AST-107 FR6) createResizable, CreateResizableOptions and ResizableController are exported from the Resizable entry point and the root, in the pull request whose FR3 line names createResizable as the owner ResizeHandle and useResizable go through, so the public API and its record line land together. createResizable is documented as a hook beside useResizable (no category, so it is not a tile in the components gallery; the docsite's route lock names its page, and the doc compiler's pin reads it by name); its page says update() takes the full set of options, and useResizable's page names it as the factory both forms wrap. The story drives a plain divider over an embedded frame, and the browser proof drags across that frame, which the held pointer keeps from swallowing the drag. The core API report gains the export rows (only those), and the changeset announces the factory.
…and useResizable go through component:Resizable FR3 owned the drag, keys, double-click, snapping, persistence, reversal and size in ResizeHandle and useResizable. Both forms of the hook and both paths of the handle go through createResizable, now public, so FR3 names it as the owner they go through (architecture:public-component-api INV22).
…asured before the DOM changes, and focus leaves a collapsing pane (component:Resizable FR8; component:LayoutPanel FR7, AR2) Red: nothing plays and no focus moves. A LayoutPanel at a ref-form region's size, or given the configuration-object hook's own props, is measured before the DOM changes and played from the edge facing its handle once it renders its new size, for every collapse and open (collapse(), expand(), a resize out of collapse, Enter, a double-click, the grow arrow, a controlled change, a drag past the threshold); a controlled change is measured in the commit that renders it, which the hook reports in the commit of the parent's state. A drag that reopens, a plain resize and the first render play nothing, an unmount lands a running motion, and a LayoutPanel that replaces another reaches its region through its callback ref. A hook-only pane and a hand-built ResizableProps object show no reveal. Focus inside a collapsing pane moves to its ResizeHandle and the collapsed pane is inert and aria-hidden, in every form; a collapse from the handle or from outside the pane keeps focus.
…d before the DOM changes, and focus leaves a collapsing pane (component:Resizable FR8; component:LayoutPanel FR7, AR2) A LayoutPanel sized by a resize controller lends it its element in its callback ref, through an internal channel (resizablePane.ts), with no new prop: a ref-form factory finds the lent panel beside its handle, and the configuration object's region gets the pane its LayoutPanel hands over, through the hook's own props (a hand-built ResizableProps object has no region and never slides). The factory decides each collapse and open as it makes it (a drag that reopens plays nothing; a key step plays), and LayoutPanel tells it of the change in the commit that renders it, before React changes the DOM (LayoutChangeSnapshot), where the pane is measured; it plays spec:AST-105 FR9's edge reveal from the edge facing the handle once the pane renders its new size, on the factory's own animated-layout handle over the pane's row. A controlled isCollapsed, which both forms report in the commit of the parent's state, is measured the same way before the factory hears of it. A resize that neither collapses nor opens, a pointer reopen, the pane leaving and destroy land a running motion. The animated-layout boundary check lists the controller's edge reveal among the module's users. Focus inside a collapsing panel moves to its ResizeHandle, then a commit later the panel goes inert and aria-hidden until it opens, in every form; LayoutPanel owns both, as family:layout-regions says.
…docs say a pane slides and focus leaves it; changesets for both LayoutPanel's width and resizable entries, useResizable's handle ref and createResizable's practices say that a LayoutPanel beside its ResizeHandle at the region's size slides closed and open, that focus inside it moves to the handle as it collapses, and that it is inert while collapsed; the deprecated props give no slide. A [feat] changeset covers the slide and a [fix] changeset the focus move, in both forms.
…ject does not LayoutPanel's resizable entry and the [feat] changeset say the configuration-object hook's own props slide the panel like the ref form and a hand-built ResizableProps object does not; the [fix] changeset's focus move and inert state hold for both.
…le on every frame, in Chromium and WebKit (component:Resizable FR8; component:LayoutPanel FR7, AR2) Two stories render a LayoutPanel at a useResizable(handleRef) region's size beside its ResizeHandle, at the start and at the end of a Layout. The browser proof checks the pane, handle and content are present and painted, then collapses the pane from a button inside it: the handle travels over several frames, the pane's shown edge stays at its resting offset from the handle on every frame, the pane ends at its collapsed layout, focus is on the handle, and the pane is inert and aria-hidden. Enter on the handle opens it the same way, with no focus move. Under Reduce Motion nothing travels and focus still leaves the pane. Without the controller's reveal the slide cases fail. Each element is read on its own animation's clock: WebKit reports a compositor-run translate about a millisecond ahead of the frame time (up to about 1.6 px at the fastest part of a collapse) while the pane's clip runs on the main thread at the frame time, and both share one start time and sample grid; in Chromium this equals the rect. The shipped-axis guard pairs a handle with its region by the ref it holds, as well as by the resizable prop, so the ref-form stories pair.
…e browser; a hand-built object collapses without a slide (component:Resizable FR8; component:LayoutPanel FR7, AR2) Two browser fixtures, kept out of the docs and the visual matrix, render a start LayoutPanel with the configuration-object hook's own props and with a hand-built copy of them. The first passes the same frame proof as the ref form; the second collapses and opens with no travelling frame, and focus still moves to the handle and the panel goes inert. The proof now places the handle at its layout place, where the pane's margin box ends in the row, plus its glide's keyframes at the glide's own time, instead of reading the handle's rect: WebKit reports a compositor-run translate ahead of the frame time in the rect and the computed style alike, by up to about 1.8 px. The pane moves by no transform, and its clip runs on the main thread at the frame time.
…paused motion, so a starved renderer cannot fail it Under load the proof failed: it sampled the frames the renderer drew, and with 16 WebKit workers on 3 CPUs (and once with 4 Chromium workers on 2 cores) it drew 1 to 5 frames over a whole collapse, so its "a slide, not a jump" check failed although the motion itself runs on its own clock. The proof now pauses every animation the collapse or open starts the moment it is created, and holds it there (the module starts each one on the timeline's current time by setting its start time, which would set it running), so none advances while the test waits for a starved page. Then, in one task, it sets each to the same one of 24 evenly spaced moments before the end and reads the pane and its handle there, finishes them, and checks what they leave. A slide is the pane's clip animation and the handle's glide, with the handle on its way at a third of the moments or more and the pane's shown edge on the handle at every moment; a hand-built object and Reduce Motion start no glide. The frame reader is installed once in the page and read the same way at rest and at every moment.
…led isCollapsed, a double-click, the grow arrow and a drag past the threshold (component:Resizable FR8; component:LayoutPanel FR7) The sliding stories gain a toggle outside the panel, and a story whose parent owns isCollapsed through onCollapseChange with its own toggle. Each path proves the pane clips and the handle glides with the pane's shown edge on the handle at every captured moment: the toggle (which keeps its focus while the panel goes inert), a controlled change both ways, a double-click on the handle, the grow arrow from collapsed, and a drag past the collapse threshold. A drag that reopens the panel follows the pointer with no clip and no glide. Without the panel telling its controller of the change before the DOM changes, every slide case fails and the hand-built and Reduce Motion cases still pass.
…om its first commit, and focus moves to the handle with the last input's ring (component:LayoutPanel AR2; architecture:interaction-modality INV1) Red: an end LayoutPanel whose region starts collapsed, beside a ResizeHandle that reverses it through its own isReversed prop, is neither inert nor aria-hidden after the first commit, because the factory looked for its pane before the handle reported its reversal. And focus moved out of a collapsing panel by a raw focus(), which shows the ring after pointer input too.
…once, and focus moves to the handle with the last input's ring (component:LayoutPanel AR2; architecture:interaction-modality INV1) When a ResizeHandle reports its own isReversed after the factory's first commit, the factory looks for its pane again on the other side of the handle and tells it whether the region is collapsed, so an end panel whose region starts collapsed is inert and aria-hidden from its first commit. LayoutPanel moves focus out of a collapsing panel with the shared focusForLastInput, and tracks the interaction modality of its document, so the handle shows its ring after keyboard input and not after pointer input.
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.
| 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").
| // 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.
The fourth of five stacked pull requests for createResizable; the extraction (#9197) is merged. It is built on the public export of createResizable (#9372) and merges after it; this branch carries the commits of #9297, #9370 and #9372. This head's own changes: vjeux/astryx@7a6ab57...c5b5bca. The deprecation (#9202) follows it.
User need
A LayoutPanel whose region collapses jumped shut, and focus inside it stayed in a panel of no width. The shared edge reveal that SideNav's rail and Collapsible use (
spec:AST-105FR9) now covers it.Problem and solution fit
component:ResizableFR8,component:LayoutPanelFR7). A LayoutPanel at auseResizable(handleRef)region's size beside its ResizeHandle, or given the configuration-object hook's own props, slides closed and open from the edge facing its handle. The content moves on the same motion.collapse(),expand(), a resize out of collapse, Enter, a double-click, the grow arrow, a controlledisCollapsedchange, and a drag past the collapse threshold.ResizablePropsobject, whichuseResizabledid not return, does not slide.component:LayoutPanelAR2). Focus inside a collapsing panel moves to its ResizeHandle. A commit later, the panel goes inert and hidden from assistive technology until it opens. This holds under any resize controller, a hand-built object included.Current specification
spec:AST-105FR2 and FR9architecture:react-component-runtimeINV12family:layout-regions(LayoutPanel lends its element and owns a collapsing panel's focus move and inert state)component:ResizableFR8component:LayoutPanelFR7 and AR2Public delta and caller burden
No prop, option or return member is added. Two changesets: [feat] the slide, and [fix] the focus move under every controller.
Design and alternatives
LayoutChangeSnapshot), and the pane is measured there. The reveal plays once the pane renders its new size, on the factory's own animated-layout handle over the pane's row.isCollapsedin the commit of the parent's state (the same-commit rule of refactor(Resizable): one internal createResizable factory, which useResizable's configuration-object form and ResizeHandle's resizable path go through #9297 and feat(Resizable): useResizable(handleRef, options) is createResizable's thin React wrapper, and ResizeHandle renders the factory bound to its element #9370), so the factory hears of it after the DOM has changed. LayoutPanel's snapshot measures it first, the same way.destroyland a running motion at once.focusForLastInput, so the handle's ring follows the last input (architecture:interaction-modalityINV1). A commit later it goes inert and aria-hidden, so inert never drops focus to the page.isReversedafter the factory's first commit; the factory then looks for its pane on the other side of the handle and tells it whether the region is collapsed, so an end panel whose region starts collapsed is inert from its first commit.resizablePane.ts) among the animated-layout module's users.Called out for the spec owners:
Overlap with an open pull request: #4593 (LayoutPanel
hideBelow) also editsLayoutPanel.tsxandLayoutPanel.doc.mjs. This PR adds the pane channel, the change snapshot, and the focus and inert state inside LayoutPanel's body, and one sentence each to itswidthandresizableentries. Whichever lands second keeps both.Evidence
Unit tests (the shared edge reveal stood in by a recorder):
inline-end, an end pane frominline-start. This holds for the ref form and for the hook's own props.Browser ("Core/Resizable / Sliding Start Panel", "Sliding End Panel", "Sliding Controlled Panel", and the two configuration-object fixtures):
Scope
Testing
Local gates at c5b5bca: unit tests (test:ui, 17,496 passed), check:repo, lint, root build then typecheck, the core API report regenerated from this build (it gains only this pull request's rows; on this base, 00146a7, the committed report lacks rows that #9344 later refreshed), the node project (only the known cache.test.mjs red), the docsite build and tests (732 passed), the Storybook build, accessibility in Chromium (77/77), the accessibility audit of Resizable, SideNav and Layout (0 new) and of every story (0 new), interaction Chromium (createResizable, the slide proof and SideNav's rail motion, 21/21), and the browser proofs under stress: Chromium 2-core ×24 264/264, WebKit CI lane (4 CPUs, CI, LP_NUM_THREADS=4, 1 worker) ×10 110/110, WebKit 3-CPU with 1 worker 110/110 and with 16 workers 440/440.