Skip to content

feat(Resizable): deprecate useResizable's configuration-object form for useResizable(handleRef, options) - #9202

Draft
vjeux wants to merge 27 commits into
meta:mainfrom
vjeux:use-resizable-config-form-deprecated
Draft

vjeux wants to merge 27 commits into
meta:mainfrom
vjeux:use-resizable-config-form-deprecated

Conversation

@vjeux

@vjeux vjeux commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

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 @deprecated ships with its replacement (architecture:public-component-api INV21, INV22).

Primary intent

The deprecation patch for useResizable's configuration-object form (architecture:public-component-api INV22 and INV9; spec:AST-017 FR28, 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 the resizable props marked deprecated in TypeScript, with the replacement named. Nothing warns at runtime. A minor removes the form later.

Problem and solution fit

  • @deprecated on the two configuration-object overloads, their types (ResizableRegionConfig, UseResizableSingleConfig, UseResizableMultiConfig, ResizableRegion, ResizableProps), and the resizable props of ResizeHandle and LayoutPanel, each naming the replacement (FR29a).
  • The changeset gives four ordered migration steps (FR28). A regions call becomes one call per region; an autoSaveId of <autoSaveId>:<region key> keeps each region's saved size.
  • Maintained surfaces move to the ref form (FR29b, FR29c): SideNav's own resize; Resizable's, useResizable's and LayoutPanel's docs; the side-panels guide; the Resizable and useResizable stories and the window-splitter accessibility binding; the docsite playground; the Grid and Resizable blocks; and the ai-chat, canvas-editor, ide, incident-console, table-filter, table-grouped and table-inbox templates. The deprecated surface stays documented, labeled with the replacement.
  • LayoutPanel's docs keep the slide's sentences (feat(Resizable): a resizable LayoutPanel slides closed and open, and focus leaves a collapsing panel #9300). A panel at width={region.size} slides closed and open. With the deprecated resizable props, 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.
  • Records: LayoutPanel's marks its resizable prop deprecated for width; Resizable's notes the deprecation in its compatibility section.
  • The core API report marks the deprecated rows: the configuration-object form's types and the resizable props of LayoutPanel and ResizeHandle (only those rows; the committed report is otherwise left as it is).
  • Record edit, for the record owner's approval: architecture:public-component-api's "Known verification gaps" no longer lists useResizable under 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 component:Resizable FR3 names it as the owner ResizeHandle and useResizable go through #9372), and the configuration-object signature deprecated here.
// before (deprecated)
const sidebar = useResizable({defaultSize: 260});
<LayoutPanel resizable={sidebar.props} />
<ResizeHandle isReversed resizable={sidebar.props} />

// after
const handleRef = useRef(null);
const sidebar = useResizable(handleRef, {defaultSize: 260});
<LayoutPanel width={sidebar.size} />
<ResizeHandle isReversed ref={handleRef} />

Authority

architecture:public-component-api INV22 (current): "the old signature is deprecated under INV9". spec:AST-017 FR28 (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

  • SideNav keeps its region as the sole collapse owner in every mode. The region answers collapse and expand with or without the handle, which renders only while the nav is resizable and expanded.
  • The structured-percent story shows the size it selected. It no longer prints the resolved bound, which came from the deprecated props.
  • The slide's two browser fixture stories (feat(Resizable): a resizable LayoutPanel slides closed and open, and focus leaves a collapsing panel #9300) still render the configuration-object form (the hook's own props, and a hand-built object), because component:Resizable FR8'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:

Writing rubric results

Records touched: component:LayoutPanel (the Resizable concept row, the compatibility row and the migration decision), component:Resizable (one compatibility bullet), and architecture:public-component-api (one name out of INV22's known-gap list).

  • R1 narration: none; each new sentence states what is deprecated and what replaces it, in the present tense, and the gap-list edit removes a name and adds no sentence.
  • R2 research: none.
  • R3 evidence: no basis or verification cell changed; none names a pull request.
  • R4 rejected alternatives: none added; Resizable's two Rejected: lines are untouched.
  • R5 change references: none.
  • R6 open questions: none in any of the three records.

Scope

  • This PR has one primary intent.
  • Independently removable deltas were removed or split.
  • Public text and artifacts contain no internal Meta context.

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.

@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 1, 2026 6:01pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 31, 2026
@astracat-bot

astracat-bot Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 1, 2026, 6:18 PM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 31, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded skipped. Current analysis is unavailable.


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 generate because internal/shadcn-registry/routes.lock.json lacks the new hook-create-resizable route, which is also why the preview deployment fails.
  • Rebase onto main once #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-valuemin and aria-valuemax (260, 180 and 480 before) until hydration, because ResizeHandle only 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 shrinkOrder and that resolved bounds are no longer returned, and LayoutPanel's hasDivider doc still refers to resizable.

[Automated review]

isAlwaysVisible={false}
xstyle={styles.resizableHandle}
resizable={resizableHook.props}
ref={resizeHandleRef}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-valuemin and aria-valuemax until hydration, because ResizeHandle has no region on the server.
  • A ResizeHandle that a child component re-mounts on its own is never bound to useResizable(handleRef, options), so it shows no value and ignores input.
  • Rebase onto main once #9197 merges: 9,199 of the 9,681 changed lines belong to it.
  • Migration step 2 should say to drop shrinkOrder, LayoutPanel's hasDivider docs still point at resizable, 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, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-valuemin and aria-valuemax (260, 180 and 480 on main) until hydration.
  • A ResizeHandle that a child component re-mounts on its own is never bound to useResizable(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, and hasDivider's doc comment still teaches resizable.
  • 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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.

cixzhang pushed a commit that referenced this pull request Nov 1, 2026
…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.
@vjeux
vjeux force-pushed the use-resizable-config-form-deprecated branch from 5bf6daa to 593895d Compare November 1, 2026 10:15

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-valuemin and aria-valuemax (260, 180 and 480 on main) until hydration.
  • A ResizeHandle that a child component re-mounts on its own is never bound to useResizable(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, and hasDivider's doc comment still teaches resizable.
  • 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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-valuemin and aria-valuemax (260, 180 and 480 on main) until hydration.
  • A ResizeHandle that a child component re-mounts on its own is never bound to useResizable(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, and hasDivider's doc comment still teaches resizable.
  • 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, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.
vjeux added 25 commits November 1, 2026 08:05
…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).

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-valuemin and aria-valuemax (260, 180 and 480 on main) until hydration, because ResizeHandle only 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 no props.
  • The Resizable and LayoutPanel records still list the slide and focus move as missing, and hasDivider's doc comment still teaches resizable.
  • Rebase once #9297, #9370, #9372 and #9300 land and resolve the SideNav.tsx conflict with main: 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.

This branch was successfully deployed

1 active deployment
Preview — 5019d87b Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant