Repository navigation
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.
Approval of this deprecation waits on an engineering owner's decision about the spec record edits, and some changes are needed whatever that decision is. The head rewrites component:Resizable FR3: createResizable owns the resize behavior, and with autoSaveId storage receives the final size once when a drag ends, cancels or unmounts. It also marks LayoutPanel's resizable concept Deprecated. An engineering owner approves or rejects those edits by commenting /approve-spec <full head SHA>; the author may do this as an engineering owner. Independently of that decision:
- The docsite build fails at
pnpm generatebecauseinternal/shadcn-registry/routes.lock.jsonlacks the newhook-create-resizableroute, which is also why the preview deployment fails. - Rebase onto
mainonce #7782 and #9197 merge: 4,883 of the 5,456 changed lines belong to them. - SideNav's server-rendered resize handle no longer carries
aria-valuenow,aria-valueminandaria-valuemax(260, 180 and 480 before) until hydration, becauseResizeHandleonly learns its region in an effect. useResizable(handleRef, options)never binds a handle that a child component re-mounts on its own, so the new handle has no value and ignores input.- Migration step 2 should tell callers to drop
shrinkOrderand that resolved bounds are no longer returned, and LayoutPanel'shasDividerdoc still refers toresizable.
[Automated review]
| isAlwaysVisible={false} | ||
| xstyle={styles.resizableHandle} | ||
| resizable={resizableHook.props} | ||
| ref={resizeHandleRef} |
There was a problem hiding this comment.
With the ref form, the server-rendered separator here has no aria-valuenow, aria-valuemin or aria-valuemax until hydration (the base renders 260, 180 and 480): ResizeHandle learns its region only in an effect. Letting ResizeHandle read the region's state while rendering, through the RefObject it shares with useResizable, keeps the released server markup.
| // region whose handle has not rendered yet, or has gone away, still answers | ||
| // collapse, expand and resize; a new handle gets a new factory that starts | ||
| // where the old one stood, and the old one is destroyed. | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This check runs only when the component that calls the hook commits, so a ResizeHandle that a child component re-mounts on its own is never bound. The new handle renders no value and ignores the keyboard (react-component-runtime INV6). ResizeHandle's claim callback ref already sees every attach and detach; binding there by the shared ref would cover it. Please add a test with a child-owned handle.
| [feat] `useResizable`'s configuration-object form is deprecated in favor of `useResizable(handleRef, options)`, and with it the `resizable` props of `ResizeHandle` and `LayoutPanel` and the form's types (`ResizableRegionConfig`, `UseResizableSingleConfig`, `UseResizableMultiConfig`, `ResizableRegion`, `ResizableProps`). Old usage works unchanged until a minor removes it: TypeScript marks it deprecated, and nothing warns at runtime. To migrate: | ||
|
|
||
| 1. Create a ref for each handle: `const handleRef = useRef(null)`. | ||
| 2. Pass it first, with the same options: `useResizable(handleRef, options)`. A `regions` call becomes one call per region, with the shared `containerRef` and `direction`; an `autoSaveId` of `<autoSaveId>:<region key>` keeps each region's saved size. |
There was a problem hiding this comment.
"With the same options" is not quite true: CreateResizableOptions has no shrinkOrder, so an inline object that keeps it stops typechecking. The ref form also returns no resolved bounds (props._minSizePx and _maxSizePx). Worth a clause telling callers to drop shrinkOrder and that the bounds are no longer returned.
|
|
||
| /** @type {import('@astryxdesign/cli/authoring').HookDoc} */ | ||
| export const docs = { | ||
| name: 'createResizable', |
There was a problem hiding this comment.
This page adds the ShadCN route hook-create-resizable (hooks/create-resizable), but internal/shadcn-registry/routes.lock.json has no entry for it. The docsite's pnpm generate stops with "Generated ShadCN names or routes changed", which matches the failing preview deployment. Running UPDATE_SHADCN_ROUTE_LOCK=1 node apps/docsite/scripts/generate-data.mjs adds just that entry.
c3abb48 to
205d49f
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about the rewritten component:Resizable FR3, which makes createResizable the owner of drag, keyboard, collapse, snapping and persistence that ResizeHandle and useResizable go through; no current record states that yet. An engineering owner approves or changes it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever that decision, these need changes:
- SideNav's server-rendered resize handle no longer carries
aria-valuenow,aria-valueminandaria-valuemaxuntil hydration, becauseResizeHandlehas no region on the server. - A
ResizeHandlethat a child component re-mounts on its own is never bound touseResizable(handleRef, options), so it shows no value and ignores input. - Rebase onto
mainonce #9197 merges: 9,199 of the 9,681 changed lines belong to it. - Migration step 2 should say to drop
shrinkOrder, LayoutPanel'shasDividerdocs still point atresizable, and the Resizable and LayoutPanel records still list the slide and focus move as missing.
[Automated review]
| // its region answers collapse and expand with or without the handle, which | ||
| // renders only while the nav is resizable and expanded. | ||
| const resizeHandleRef = useRef<HTMLDivElement>(null); | ||
| const resizableHook = useResizable(resizeHandleRef, { |
There was a problem hiding this comment.
With the ref form, SideNav's server-rendered separator has no aria-valuenow, aria-valuemin or aria-valuemax until hydration (the base rendered 260, 180 and 480): ResizeHandle reads its region through useSyncExternalStore(…, () => null) and no effect runs on the server. That changes accessibility semantics in a patch. Could ResizeHandle render the hook's initial view on the server, with a SideNav server-render assertion?
| // region whose handle has not rendered yet, or has gone away, still answers | ||
| // collapse, expand and resize; a new handle gets a new factory that starts | ||
| // where the old one stood, and the old one is destroyed. | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This check runs only when the component that calls the hook commits, so a ResizeHandle that a child component re-mounts on its own (a key change or a conditional inside the child) is never handed to createResizable: it renders no value and ignores keyboard and pointer input. Driving rebinding from ResizeHandle's callback ref would cover node replacement; a child re-mount case in useResizableRef.test.tsx would pin it.
| [feat] `useResizable`'s configuration-object form is deprecated in favor of `useResizable(handleRef, options)`, and with it the `resizable` props of `ResizeHandle` and `LayoutPanel` and the form's types (`ResizableRegionConfig`, `UseResizableSingleConfig`, `UseResizableMultiConfig`, `ResizableRegion`, `ResizableProps`). Old usage works unchanged until a minor removes it: TypeScript marks it deprecated, and nothing warns at runtime. To migrate: | ||
|
|
||
| 1. Create a ref for each handle: `const handleRef = useRef(null)`. | ||
| 2. Pass it first, with the same options: `useResizable(handleRef, options)`. A `regions` call becomes one call per region, with the shared `containerRef` and `direction`; an `autoSaveId` of `<autoSaveId>:<region key>` keeps each region's saved size. |
There was a problem hiding this comment.
"With the same options" is not quite true: CreateResizableOptions has no shrinkOrder, so an inline object that keeps it stops typechecking. The option never had an effect, so this step can tell callers to drop it.
205d49f to
5bf6daa
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about the rewritten component:Resizable FR3, which makes createResizable the owner of the resize behavior that ResizeHandle and useResizable go through; no current record states that yet. An engineering owner approves or changes it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever that decision, these need changes:
- SideNav's server-rendered resize separator no longer carries
aria-valuenow,aria-valueminandaria-valuemax(260, 180 and 480 onmain) until hydration. - A
ResizeHandlethat a child component re-mounts on its own is never bound touseResizable(handleRef, options), so it shows no value and ignores input. - Migration step 2 fails to typecheck for an inline object that keeps
shrinkOrder, the Resizable and LayoutPanel records still list the slide and focus move as missing, andhasDivider's doc comment still teachesresizable. - Rebase once #9197 lands: 9,591 of the 10,072 changed lines are its commits.
[Automated review]
| isAlwaysVisible={false} | ||
| xstyle={styles.resizableHandle} | ||
| resizable={resizableHook.props} | ||
| ref={resizeHandleRef} |
There was a problem hiding this comment.
With the ref form, SideNav's server-rendered separator has no aria-valuenow, aria-valuemin or aria-valuemax until hydration (main renders 260, 180 and 480): ResizeHandle reads its region through useSyncExternalStore(…, () => null) and nothing binds it on the server. Rendering the region's starting view when the handle has no binding yet, plus a SideNav server-render assertion, would keep the released markup.
| // region whose handle has not rendered yet, or has gone away, still answers | ||
| // collapse, expand and resize; a new handle gets a new factory that starts | ||
| // where the old one stood, and the old one is destroyed. | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This check runs only when the component that calls the hook commits, so a ResizeHandle that a child component re-mounts on its own (a key change or a conditional inside the child) is never handed to createResizable: it renders no value and ignores keyboard and pointer input. ResizeHandle's claim callback ref already sees every attach; letting it ask the hook to rebind, with a child re-mount test, would cover node replacement (react-component-runtime INV6).
| [feat] `useResizable`'s configuration-object form is deprecated in favor of `useResizable(handleRef, options)`, and with it the `resizable` props of `ResizeHandle` and `LayoutPanel` and the form's types (`ResizableRegionConfig`, `UseResizableSingleConfig`, `UseResizableMultiConfig`, `ResizableRegion`, `ResizableProps`). Old usage works unchanged until a minor removes it: TypeScript marks it deprecated, and nothing warns at runtime. To migrate: | ||
|
|
||
| 1. Create a ref for each handle: `const handleRef = useRef(null)`. | ||
| 2. Pass it first, with the same options: `useResizable(handleRef, options)`. A `regions` call becomes one call per region, with the shared `containerRef` and `direction`; an `autoSaveId` of `<autoSaveId>:<region key>` keeps each region's saved size. |
There was a problem hiding this comment.
"With the same options" fails for an inline object that keeps shrinkOrder: CreateResizableOptions has no such field, so the call stops typechecking (TS2353). Since the option never had an effect, the step can tell callers to drop it.
…ch the SideNav rail moves onto (#8721) Implements the records #8783 put on main: `spec:AST-105` FR2 and FR9, `architecture:react-component-runtime` INV12, `component:SideNav` FR6, `component:Resizable` FR8, `component:LayoutPanel` FR7 and AR2, and `component:Table` FR15. Resizable's and LayoutPanel's slide and LayoutPanel's focus rule (AR2: focus moves to the ResizeHandle only when a collapsing pane holds it) are built on this path in #9197 and #9202. This PR moves no focus anywhere. Behavior-module PR: `packages/core/src/motion/animatedLayout.ts` (FR9's shared edge reveal). ## User need A panel that collapses beside content, a Collapsible closed while it opens, a resizable pane sliding shut: each reveals or conceals an element from one of its edges while the content beside it moves. Before this change Core had one such reveal of its own per component. The SideNav rail clipped and rode its content in its own code. Collapsible's reveal could only open downward, and a close while it opened restarted the conceal from fully shown while the content below kept gliding, so a gap opened. Nothing a resize controller could reuse existed. #8783 made these one shared edge reveal in the animated-layout module. ## Problem and solution fit The animated-layout module now produces `spec:AST-105` FR9's edge reveal, in three forms over one ride: - **One-state** (`playLayoutChange`, `holdLayoutExit`): reveals and conceals from the block-end edge by default, or from the block-start or an inline edge it is given, mirrored right to left. Once the content on that edge's side glides, the moving edge rides that glide frame by frame (its keyframes replaced, its clock the glide's), so no gap opens. It rides the content's offset relative to the element: when the same batch also moves the element itself, such as an item above them closing in a single-open CollapsibleGroup, the edge follows only the difference. A conceal by a start edge frees the element's space on that edge's side, so the element stays where it shows while the content before it moves; a layout where that would move the element itself is not held, and the element goes at once. A reversal starts from what the element shows. Collapsible, TreeList and Table's row expansion already play this block-end form, so they gain the ride, and Collapsible and TreeList, which hold their exits, the reversal too. - **Two-state** (`holdLayoutEdge`, `playLayoutEdge`): holds the old state as FR8's swap copy, clips the larger state on the edge from the extent it shows, rides a gliding element, and raises the element above the copy while it conceals. The SideNav rail now plays through it on its inline-end edge and keeps only its labels' and icons' fades (`component:SideNav` FR6); its frames are unchanged. - **A resized element with no copy** (`reportLayoutEdge`): measured before a size change commits and played after the new size applies. A larger extent reveals from the extent shown. A smaller one holds the element at the extent it was laid out at, freeing the difference by the margin on that edge's side so what lies beyond stays where the new size puts it, then clips down to the new extent. This is the path a resize controller's pane uses (`component:Resizable` FR8 and `component:LayoutPanel` FR7 in #9197 and #9202). Reduce Motion follows `design:reduced-motion` DR2 under a coarse primary pointer and DR3 otherwise. ## Current specification `spec:AST-105` FR9 ("One shared edge reveal") and FR2, `architecture:react-component-runtime` INV12, `component:SideNav` FR6, `component:Resizable` FR8, `component:LayoutPanel` FR7 and AR2, and `component:Table` FR15, all current on main since #8783. A LayoutPanel given a hand-built `ResizableProps` gets no slide, and the hook's own `props` passed through `resizable` slide; both are #9197 and #9202's to implement. ## Public delta and caller burden None. The new functions and types stay internal: no entry point exports them (`animatedLayout.boundary.test.ts` checks this), and `useAnimatedLayout`'s contract is unchanged. The visible changes are Collapsible's and TreeList's: content closed while it opens, or opened while it closes, continues from what it shows, and its edge stays on the content after it (`[fix]` changeset). Table's row expansion reveal rides the rows after it. ## Design and alternatives - **Riding:** the edge's keyframes are sampled on the glide's own sample grid and clock, so the clip and the glide are complementary by construction. The alternative, a separate spring tuned to match, drifts whenever the glide carries a running motion. - **Resized element without a copy:** holding the old extent keeps the pane's content laid out as it was. Clipping a pane already at its new size would need its content to overflow, or a copy, which the runtime record rules out for a resize controller. - **Morph routing** of the glide stays out until the shared rect morph lands (#8329), as INV12's "until then" clause says. ## Evidence Status at this head: rebased onto main 8a03d75 after the component records moved to `docs/specs/components/`; the patch is byte-identical to 70ebcc7's. Here, build, type check, CI-strict lint, `check:knowledge`, the record-path tests (knowledge paths, check-knowledge, specs-for-file), `check:changesets` and the motion, SideNav and AppShell unit suites (1,020) pass, and `specs:for` still returns `component:SideNav` and `spec:AST-105` for the changed files. At 70ebcc7 every gate finished: `test:ui`, `check:repo`, the node suites (only the two `cache.test.mjs` lock cases that fail on main), and the three motion specs ×10 in WebKit under the CI job's command (740, with 100 Chromium-only skips) and in Chromium pinned to 2 cores at 4 workers (840). **Still running:** `test:ui`, the node suites, `check:repo` and the three motion specs ×10 in both engines at this head. Each new proof failed on main first: - `animatedLayout.edge.test.ts`: 17 of the 18 one- and two-state cases fail on main (the block-end default passes there and guards it), and the resized-element cases have no function there. - `SideNavRailMotion.test.tsx`: a new case asserts the rail plays through the module's two-state form with no clip, copy or ride of its own. It fails on main; the rail's existing cases pass unchanged with the rail moved. - `animatedLayout.interaction.browser.spec.ts` (FR9): a block-end reveal reversed into a conceal, an inline-end reveal and conceal (LTR and RTL), the two-state form, and a resized pane (start pane LTR to 48 px, start pane RTL to 0, end pane LTR to 48), each with a reversal. On every frame the moving edge stays within 1.5 px of what it rides. All fail on main: the block-end reversal jumped 40 px, the inline edge was clipped from the bottom, and the other forms don't exist there. - `animatedLayoutAdopters.interaction.browser.spec.ts`: a Collapsible closed while it opens, and a TreeList row collapsed while it expands, keep their edge on the content after them. On main each conceal restarted from fully shown: −16 px and −36 px gaps. Two more cases guard this PR's own paths. A block-start reveal and conceal in a column that stacks from its end keep the edge on the content before it, with the element staying where it shows; before the start-edge fix the conceal freed its space at its end, so the element jumped by its 80 px and an 80 px gap opened. And in a single-open CollapsibleGroup switching from a taller item to a shorter one below it, the opening item's edge stays on the next item on every frame; before the ride was taken relative to the element, the content showed 31 px over the next item. How the proofs read a ride: both sides on the module's clock, the edge from its clip animation's keyframes and what it rides from its layout position plus its glide's keyframes, at the animations' current time. Chromium also asserts that its evaluated clip and rect equal those (under 0.5 px). In WebKit the two run a little apart, and that skew is reported per test, not hidden: a compositor-run `translate` is evaluated a little ahead of the frame and, on a late first frame, a main-thread clip reads one frame behind. Under the CI WebKit job's command (`LP_NUM_THREADS=4`), the summed skew per frame reached 0.7 to 3.4 px depending on the test. The reversals pause and seek every animation to 120 ms, and the adopters' wait for the component to commit the close before playing on, so neither a late first frame nor a component committing in a later task moves the moment they test. ## Scope - [x] One capability is added (FR9's shared edge reveal), with its adopters in this PR's scope (the SideNav rail; Collapsible, TreeList and Table's row expansion keeping their form). - [x] Unrelated fixes, cleanup, and policy changes were removed or split. Resizable's and LayoutPanel's adoption is #9197 and #9202. - [x] Consumer docs, focused tests, and migration evidence ship with the feature. - [x] Public text and artifacts contain no internal Meta context. Overlaps: #8677 also edits `motion/animatedLayout.ts` (the theme's motion knobs), and #3890 edits `SideNav/SideNav.tsx`. This PR leaves `SideNav.tsx` unchanged. ## Testing ``` pnpm exec vitest run --project ui packages/core/src/motion packages/core/src/SideNav packages/core/src/AppShell packages/core/src/Collapsible packages/core/src/TreeList packages/core/src/Table pnpm build && pnpm storybook:build pnpm test:interaction packages/core/src/motion/__tests__/animatedLayout.interaction.browser.spec.ts packages/core/src/motion/__tests__/animatedLayoutAdopters.interaction.browser.spec.ts packages/core/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts --project chromium ``` For WebKit, use the CI WebKit job's container command (`internal/interaction-harness/scripts/webkit-container.mjs`) with podman `--cpus=4`, `CI=1`, `LP_NUM_THREADS=4` and 1 worker.
5bf6daa to
593895d
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about the rewritten component:Resizable FR3, which makes createResizable the owner of the resize behavior that ResizeHandle and useResizable go through; no current record states that yet. An engineering owner approves or changes it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever that decision, these need changes:
- SideNav's server-rendered resize separator no longer carries
aria-valuenow,aria-valueminandaria-valuemax(260, 180 and 480 onmain) until hydration. - A
ResizeHandlethat a child component re-mounts on its own is never bound touseResizable(handleRef, options), so it shows no value and ignores input. - Migration step 2 fails to typecheck for an inline object that keeps
shrinkOrder, the Resizable and LayoutPanel records still list the slide and focus move as missing, andhasDivider's doc comment still teachesresizable. - Rebase once #9297 and #9300 land: 479 of the 8,466 changed lines are this pull request's own.
[Automated review]
| isAlwaysVisible={false} | ||
| xstyle={styles.resizableHandle} | ||
| resizable={resizableHook.props} | ||
| ref={resizeHandleRef} |
There was a problem hiding this comment.
With the ref form, SideNav's server-rendered separator has no aria-valuenow, aria-valuemin or aria-valuemax until hydration (main renders 260, 180 and 480): ResizeHandle reads its region through useSyncExternalStore(…, () => null) and nothing binds it on the server. Rendering the region's starting view when the handle has no binding yet, with a SideNav server-render assertion, would keep the released markup.
| // attributes, and a factory with no handle keeps the region answering | ||
| // collapse, expand and resize; a new element gets a new factory. Each new | ||
| // factory starts where the old one stood. | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This check runs only when the component that calls the hook commits, so a ResizeHandle that a child component re-mounts on its own (a key change or a conditional inside the child) is never handed to createResizable: it renders no value and ignores keyboard and pointer input. ResizeHandle's claim callback ref already sees every attach; letting it ask the hook to rebind, with a child re-mount test, would cover node replacement (react-component-runtime INV6).
| [feat] `useResizable`'s configuration-object form is deprecated in favor of `useResizable(handleRef, options)`, and with it the `resizable` props of `ResizeHandle` and `LayoutPanel` and the form's types (`ResizableRegionConfig`, `UseResizableSingleConfig`, `UseResizableMultiConfig`, `ResizableRegion`, `ResizableProps`). Old usage works unchanged until a minor removes it: TypeScript marks it deprecated, and nothing warns at runtime. To migrate: | ||
|
|
||
| 1. Create a ref for each handle: `const handleRef = useRef(null)`. | ||
| 2. Pass it first, with the same options: `useResizable(handleRef, options)`. A `regions` call becomes one call per region, with the shared `containerRef` and `direction`; an `autoSaveId` of `<autoSaveId>:<region key>` keeps each region's saved size. |
There was a problem hiding this comment.
"With the same options" fails for an inline object that keeps shrinkOrder: CreateResizableOptions has no such field, so the call stops typechecking (TS2353). Since the option never had an effect, the step can tell callers to drop it.
593895d to
0869685
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about the rewritten component:Resizable FR3, which makes createResizable the owner of the resize behavior that ResizeHandle and useResizable go through; no current record states that yet. An engineering owner approves or changes it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Whatever that decision, these need changes:
- SideNav's server-rendered resize separator no longer carries
aria-valuenow,aria-valueminandaria-valuemax(260, 180 and 480 onmain) until hydration. - A
ResizeHandlethat a child component re-mounts on its own is never bound touseResizable(handleRef, options), so it shows no value and ignores input. - Migration step 2 fails to typecheck for an inline object that keeps
shrinkOrder, the Resizable and LayoutPanel records still list the slide and focus move as missing, andhasDivider's doc comment still teachesresizable. - Rebase once #9297 and #9300 land: 480 of the 7,809 changed lines are this pull request's own.
[Automated review]
| // its region answers collapse and expand with or without the handle, which | ||
| // renders only while the nav is resizable and expanded. | ||
| const resizeHandleRef = useRef<HTMLDivElement>(null); | ||
| const resizableHook = useResizable(resizeHandleRef, { |
There was a problem hiding this comment.
With the ref form, SideNav's server-rendered separator has no aria-valuenow, aria-valuemin or aria-valuemax until hydration (main renders 260, 180 and 480): ResizeHandle reads its region through useSyncExternalStore(…, () => null) and nothing binds it on the server. Rendering the hook's starting view (from readInitialResizableState) until a controller binds would keep the server markup unchanged.
| // attributes, and a factory with no handle keeps the region answering | ||
| // collapse, expand and resize; a new element gets a new factory. Each new | ||
| // factory starts where the old one stood. | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
This check runs only when the component that calls the hook commits, so a ResizeHandle that a child component re-mounts on its own (a key change or a conditional inside the child) is never handed to createResizable: it renders no value and ignores keyboard and pointer input. ResizeHandle's claim callback ref already sees every attach, so it could ask the hook to rebind (react-component-runtime INV6).
| [feat] `useResizable`'s configuration-object form is deprecated in favor of `useResizable(handleRef, options)`, and with it the `resizable` props of `ResizeHandle` and `LayoutPanel` and the form's types (`ResizableRegionConfig`, `UseResizableSingleConfig`, `UseResizableMultiConfig`, `ResizableRegion`, `ResizableProps`). Old usage works unchanged until a minor removes it: TypeScript marks it deprecated, and nothing warns at runtime. To migrate: | ||
|
|
||
| 1. Create a ref for each handle: `const handleRef = useRef(null)`. | ||
| 2. Pass it first, with the same options: `useResizable(handleRef, options)`. A `regions` call becomes one call per region, with the shared `containerRef` and `direction`; an `autoSaveId` of `<autoSaveId>:<region key>` keeps each region's saved size. |
There was a problem hiding this comment.
"With the same options" fails for an inline object that keeps shrinkOrder: CreateResizableOptions has no such field, so the call stops typechecking (TS2353). Since the option never had an effect, the step can tell callers to drop it.
…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.
…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. A controlled isCollapsed is read from the options in the render that passes it, so size and isCollapsed land in the commit of the parent's own state (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, the slot remembering it while an <Activity> hides the hook's component. The same options follow a container swapped behind containerRef. A plain element is named and announced through the provider's catalog messages. 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. It 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 keeps its own input, and warns once (useDevWarning) when it also has a bound region.
…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: the committed report is otherwise left as it is).
…he region's state has one copy (spec:AST-107 FR6; architecture:public-component-api INV22) Red: the configuration-object form creates no factory yet. The tests check that it creates one, with no handle, from the configuration, updates it, destroys it on unmount and returns what it reports, in its size, collapse state and props. Two more guard the move and pass on the current implementation: a configuration-object region 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 (component:Resizable FR3) 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. Behavior unchanged: size and isCollapsed still land in the commit of onCollapseChange's own state, a controlled isCollapsed included (FR8); the region keeps its size across an <Activity>, and follows a container swapped behind containerRef with the same configuration. ResizeHandle's input for a `resizable` prop is now the factory's input too (resizeGesture.ts), over the props, so the handle has one implementation of the drag, the keys and the double-click.
…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 now go through createResizable, so FR3 names it as the owner they go through (architecture:public-component-api INV22). The changeset records the move as maintenance: no behavior changes.
… 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), 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: the committed report is otherwise left as it is), and the changeset records the new public factory in place of the internal one.
…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.
SideNav's region is useResizable(handleRef, options), and its overlay ResizeHandle takes the ref. The region stays the sole collapse owner in every mode: it answers collapse and expand with or without the handle, which renders only while the nav is resizable and expanded.
…spec:AST-017 FR28) The configuration-object overloads of useResizable, their types (ResizableRegionConfig, UseResizableSingleConfig, UseResizableMultiConfig, ResizableRegion, ResizableProps), and the `resizable` props of ResizeHandle and LayoutPanel that take the form's props carry static @deprecated metadata naming the replacement: useResizable(handleRef, options), the ref on ResizeHandle, and LayoutPanel width={region.size}. Old usage works unchanged and adds no runtime warning (FR29, FR29a); the removal lands in a minor.
…7 FR29b)
Resizable's, useResizable's and LayoutPanel's docs and the side-panels
guide show the ref form: the ref on ResizeHandle, and the panel at
width={region.size}. The configuration-object form and the resizable
props stay documented as deprecated, with the replacement and the
migration steps. LayoutPanel's record marks its resizable prop
deprecated for width; Resizable's records the deprecation in its
compatibility notes.
…AST-017 FR29b) Every Resizable and useResizable story, and the window-splitter binding the accessibility lanes render, give the handle's ref to useResizable and render the panel at its size. The structured-percent story shows the size it selected; the resolved bound it also printed came from the deprecated props.
…eRef) (spec:AST-017 FR29b)
The page templates (ai-chat, canvas-editor, ide, incident-console,
table-filter, table-grouped, table-inbox), the Grid and Resizable
blocks, and the docsite playground give each handle's ref to
useResizable and pass LayoutPanel width={region.size}, so new apps
start on the ref form.
…ect form, with its migration steps The changeset names the deprecated form, props and types, says old usage works unchanged until a minor removes it, and gives the four ordered migration steps (spec:AST-017 FR28). It ships in the release that adds the ref form.
…zable useResizable now delegates to its public factory, createResizable, through its ref-taking signature, and its configuration-object signature is deprecated, which is how INV22 says a hook conforms.
…orm deprecated The report's rows for the configuration-object form's types and the `resizable` props of LayoutPanel and ResizeHandle carry @deprecated, as the build now declares them (only those rows: the committed report is otherwise left as it is).
0869685 to
5019d87
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision about the rewritten component:Resizable FR3, which makes createResizable the owner of the resize behavior that ResizeHandle and useResizable go through; no current record states that yet. An engineering owner approves or changes it by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Whatever that decision, these need changes:
- SideNav's server-rendered resize separator no longer carries
aria-valuenow,aria-valueminandaria-valuemax(260, 180 and 480 onmain) until hydration, becauseResizeHandleonly learns its region after the first commit. - Migration step 2 ("with the same options") fails to typecheck for an inline object that keeps
shrinkOrder, and does not say the ref form returns noprops. - The Resizable and LayoutPanel records still list the slide and focus move as missing, and
hasDivider's doc comment still teachesresizable. - Rebase once #9297, #9370, #9372 and #9300 land and resolve the
SideNav.tsxconflict withmain: this pull request's own commits change 494 of the 8,290 changed lines.
[Automated review]
| }, | ||
| [claim, ref], | ||
| ); | ||
| const binding = useSyncExternalStore(claim.subscribe, claim.get, () => null); |
There was a problem hiding this comment.
With the ref form this reads null on the server and in the first client render, so <SideNav resizable>'s server markup has no aria-valuenow, aria-valuemin or aria-valuemax (the base renders 260, 180 and 480), and the layout effect below repairs it with a second render. Letting the hook record its starting view (readInitialResizableState) against the ref object during render, and reading it here from the ref prop until a controller binds, would keep the server markup and the first render complete.
| [feat] `useResizable`'s configuration-object form is deprecated in favor of `useResizable(handleRef, options)`, and with it the `resizable` props of `ResizeHandle` and `LayoutPanel` and the form's types (`ResizableRegionConfig`, `UseResizableSingleConfig`, `UseResizableMultiConfig`, `ResizableRegion`, `ResizableProps`). Old usage works unchanged until a minor removes it: TypeScript marks it deprecated, and nothing warns at runtime. To migrate: | ||
|
|
||
| 1. Create a ref for each handle: `const handleRef = useRef(null)`. | ||
| 2. Pass it first, with the same options: `useResizable(handleRef, options)`. A `regions` call becomes one call per region, with the shared `containerRef` and `direction`; an `autoSaveId` of `<autoSaveId>:<region key>` keeps each region's saved size. |
There was a problem hiding this comment.
"With the same options" fails for an inline object that keeps shrinkOrder: CreateResizableOptions has no such field, so the call stops typechecking (TS2353). Since the option never had an effect, the step can tell callers to drop it, and that the result has no props.
The last of five stacked pull requests for createResizable; the extraction (#9197) is merged. It is built on the LayoutPanel slide (#9300) and merges after it; this branch carries the commits of #9297, #9370, #9372 and #9300. This head's own changes: vjeux/astryx@3522fbf...5019d87. It lands in the same release as the ref form and the public createResizable (#9370, #9372), so the configuration-object form's
@deprecatedships with its replacement (architecture:public-component-apiINV21, INV22).Primary intent
The deprecation patch for useResizable's configuration-object form (
architecture:public-component-apiINV22 and INV9;spec:AST-017FR28, FR29–FR29c). No other template fits: it adds no capability (the replacement ships in #9370 and #9372) and it changes the public lifecycle, so it is not maintenance.User or maintainer impact
Builders using
useResizable(config)keep working unchanged and see their call, its types and theresizableprops marked deprecated in TypeScript, with the replacement named. Nothing warns at runtime. A minor removes the form later.Problem and solution fit
@deprecatedon the two configuration-object overloads, their types (ResizableRegionConfig,UseResizableSingleConfig,UseResizableMultiConfig,ResizableRegion,ResizableProps), and theresizableprops of ResizeHandle and LayoutPanel, each naming the replacement (FR29a).regionscall becomes one call per region; anautoSaveIdof<autoSaveId>:<region key>keeps each region's saved size.width={region.size}slides closed and open. With the deprecatedresizableprops, the hook's own props slide too and a hand-built object does not; both keep the focus move and the inert state while collapsed.resizableprop deprecated forwidth; Resizable's notes the deprecation in its compatibility section.resizableprops of LayoutPanel and ResizeHandle (only those rows; the committed report is otherwise left as it is).architecture:public-component-api's "Known verification gaps" no longer listsuseResizableunder INV22. This PR completes its conformance: the ref-taking signature (feat(Resizable): useResizable(handleRef, options) is createResizable's thin React wrapper, and ResizeHandle renders the factory bound to its element #9370), the public factory with the configuration-object form on it (feat(Resizable): createResizable is public, and useResizable's configuration-object form and ResizeHandle go through it (component:Resizable FR3) #9372), and the configuration-object signature deprecated here.Authority
architecture:public-component-apiINV22 (current): "the old signature is deprecated under INV9".spec:AST-017FR28 (a patch deprecation ships the working replacement and ordered, testable instructions), FR29 (no runtime behavior), FR29a (static@deprecated), FR29b (maintained docs, templates, examples and stories teach the replacement), FR29c (maintained packages migrate in the deprecation patch).Change and evidence
component:ResizableFR8's verification covers both. They are kept out of the docs and the visual matrix, so no maintained story teaches the deprecated form.Overlaps with open pull requests:
SideNav.tsx. This PR changes that file'suseResizablecall and the handle'sresizableprop toref; whichever lands second keeps both.hideBelow) conflicts with this PR inLayoutPanel.doc.mjs, at thewidthandresizableentries. The resolution keeps both.Resizable.spec.md(now atdocs/specs/components/Resizable/). This PR adds one bullet to its compatibility section.Writing rubric results
Records touched:
component:LayoutPanel(the Resizable concept row, the compatibility row and the migration decision),component:Resizable(one compatibility bullet), andarchitecture:public-component-api(one name out of INV22's known-gap list).Rejected:lines are untouched.Scope
Testing
Local gates at 5019d87: unit tests (test:ui, 17,493 passed), check:repo, lint, root build then typecheck, the node project (only the known cache.test.mjs red), the docsite build and tests (732 passed), the Storybook build, and the core API report regenerated from this build (on this base, 00146a7, the committed report lacks rows that #9344 later refreshed; otherwise it matches), and interaction Chromium for the frame proof (1/1). Its code is byte-identical to fedb68c8dd's, which passed accessibility in Chromium (77/77), the routed audits (0 new), interaction Chromium (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.