Repository navigation
feat(Resizable): useResizable(handleRef, options) is createResizable's thin React wrapper, and ResizeHandle renders the factory bound to its element - #9370
Conversation
…t (spec:AST-107 FR6) Red: there is no createResizable yet. The tests drive a plain element through the factory: its role, name, value and tab stop, a pointer drag that holds its pointer, the keys, collapse and restore, a controlled collapse, disabled input, update and subscribe, a percentage basis, persistence, destroy mid-drag, a region with no handle yet, and an import that does no DOM work.
…ain element a window splitter that resizes a region, without React (spec:AST-107 FR6) createResizable(handle, options) gives the element role=separator, a tab stop and the catalog's English name when it has none of its own, its orientation and value, and answers a pointer drag that holds its pointer (so an embedded frame cannot swallow the drag), the arrow keys, Home, End, and Enter and a double-click to collapse and restore. It resolves, clamps, snaps and saves through resizableModel.ts, as useResizable does, reports the size through subscribe and onSizeChange, takes new options through update, and destroy removes every listener, observer and attribute it added. With a null handle the region still answers collapse, expand, resize and subscribe. The input lives in resizeGesture.ts. Importing either module does no DOM work. It is not exported from the package, so the changeset records it as maintenance.
|
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 |
…ions update() replaces the options, as a new render does, so an option left out returns to its default; the region keeps its size and collapse state within the new options. Its JSDoc said the handle and the region keep their state, which read as a merge. A test pins the contract: a full set with one change keeps the selection, the name and the bounds, and a set that leaves options out returns them to their defaults.
There was a problem hiding this comment.
Requesting changes. The ref-taking useResizable(handleRef, options) follows the conformance path INV22 lists for useResizable, but this head leaves two copies of the resize logic: resizeGesture.ts repeats ResizeHandle's pointer and keyboard handling, and createResizable repeats the configuration form's size model, while both originals stay in use. Please move ResizeHandle's resizable path and the configuration form onto the factory in the change that introduces it, export CreateResizableOptions with the new overload, and land #9297 first so this diff (3,808 authored lines, 1,826 of them #9297's) drops under the 2,000-line scope limit or is split further.
[Automated review]
| model.cancel(); | ||
| }, | ||
|
|
||
| keyDown(event) { |
There was a problem hiding this comment.
This keyDown repeats ResizeHandle.tsx 683–781, and the pointer, snap-point, collapse and body-cursor helpers here repeat 619–680, 392–438 and 126–134. ResizeHandle still runs its own copy for the resizable path. Route that path through createResizeGesture in this change and delete the copy, so one input serves both.
There was a problem hiding this comment.
Moved into #9297: ResizeHandle's resizable path goes through createResizeGesture there, so this pull request adds no copy.
| } | ||
|
|
||
| /** The region's state from its options, its basis and its selection. */ | ||
| function compute(): void { |
There was a problem hiding this comment.
compute() repeats useSingleResizable's model in useResizable.ts (501–600, 811–824): the default that resolves once, the chosen-size latch, the bound re-clamp and the reported maximum. With both in use, a sizing fix reaches one form only. Build the configuration form on this factory in the same change, or extract the shared model both call.
There was a problem hiding this comment.
Moved into #9297: the configuration-object form builds on the factory there.
| hook usePowerSearchConfig: <const D extends ReadonlyArray<FieldDefinition>>(definitions: D, configName?: string) => { config: PowerSearchConfig; applyFilters: <T extends InferData<D>>(filters: ReadonlyArray<PowerSearchFilter>, data: ReadonlyArray<T>) => T[] } | ||
| hook usePressFeedback: () => typeof pressableProps | ||
| hook useResizable | ||
| overload (handleRef: RefObject<HTMLElement | null>, options?: CreateResizableOptions) => UseResizableResult |
There was a problem hiding this comment.
CreateResizableOptions appears in this public signature but no entry point exports it, so callers cannot name the new form's options. Export the type with this overload.
There was a problem hiding this comment.
Per the spec owners, CreateResizableOptions exports with createResizable and ResizableController in #9372, together with FR3.
| defaultIsCollapsed: previous.isCollapsed, | ||
| }; | ||
| const resizable = createResizable(element, createdWith); | ||
| readResizableInternals(resizable)?.setCollapsedText( |
There was a problem hiding this comment.
The translated collapsed text reaches the factory only through this internal channel, and the expanded size only through readResizableSelection, so a createResizable caller has neither. INV22 has the hook delegate entirely to a factory that takes the same options. Give the factory a collapsed-text option and an expanded-size reading before it is exported.
There was a problem hiding this comment.
Outside a provider the factory announces the shipped English catalog's text (architecture:internationalization INV3); how non-React callers localize it is pending a system-wide decision. The spec owners chose no public expanded-size member: a non-React caller's update() is synchronous, so its size is current.
…he region's state has one copy (architecture:public-component-api INV22) Red: the configuration-object form creates no factory yet. With a stand-in factory, the tests check that it creates one, with no handle, from the configuration, passes later options through update(), destroys it on unmount and returns what it reports, in its size, collapse state and props. Guards that pass on the current implementation and must keep passing on the factory: the form reports size and isCollapsed in the commit of onCollapseChange's own state, controlled or not, through collapse(), Enter, expand(), double-clicks and a drag past the threshold (component:Resizable FR8); it keeps its size and collapse state across an <Activity> that hides and shows it; and a configuration object kept across renders follows a container swapped behind containerRef.
…and ResizeHandle's `resizable` path goes through its input The configuration-object form is createResizable with no handle of its own, so the region's state has one copy, the factory's (spec:AST-107 FR6; architecture:public-component-api INV22). The props it returns carry what the factory reports, and a ResizeHandle given them drives the factory's region through its model, read at each input so a whole drag reaches the factory as it is now. ResizeHandle's input for a `resizable` prop is the factory's input (resizeGesture.ts) over the props, so the handle has one implementation of the drag, the keys and the double-click. Behavior unchanged: size and isCollapsed land in the commit of onCollapseChange's own state, a controlled isCollapsed included (FR8); the region keeps its size across an <Activity> (the hook's store remembers where it stood and seeds the next factory), and follows a container swapped behind containerRef with the same configuration. React's store subscriptions do not read the store again after an <Activity> shows its subtree, so the hook renders again from the layout phase when the factory made for the reveal reports something new. The factory stays internal, so no public API or record changes; the changeset records the move.
…ontainer after an <Activity> hid and showed the region before then (spec:AST-010) Red: while the container is not laid out, the temporary 1200px basis stands in, so a '30%' default shows 360px. A factory made for an <Activity> reveal took that stand-in as the region's selection, kept 360px after the container measured 800px, and saved it, where the default should resolve to 240px against the measured container and the temporary basis is never final or saved.
…ry basis's stand-in as the region's size (spec:AST-010) readResizableSelection reports the size only when it is a selection (saved, chosen, or the default resolved against a measured basis); while only the temporary 1200px basis stands in, it reports none, and the hook seeds the next factory with the collapse state alone, so the factory resolves its own default once the container measures. This holds for a factory made after an <Activity> hides and shows the region.
…reateResizable, and ResizeHandle renders the factory bound to its element (spec:AST-107 FR6) Red: there is no ref-taking useResizable yet. The tests check that it creates the factory on the element, passes later options through update(), destroys it on unmount and returns what it reports, in the commit that onCollapseChange's own state lands in, controlled or not (component:Resizable FR8); that a ResizeHandle given the ref renders the region and forwards its drag, keys and double-click to the factory, with its own isReversed and isDisabled, after a caller's onKeyDown (FR7); that a handle a child mounts from its own state, or hides and shows in its own <Activity>, is bound at once, and the region keeps its size across an <Activity> that hides the hook's own component; that a plain element drops its listeners when it unmounts and is named and announced through the message catalog; that the same options follow a container swapped behind containerRef; and that the hook renders its initial size on the server.
…s thin React wrapper, and ResizeHandle renders the factory bound to its element (spec:AST-107 FR6) The hook creates the factory on the element behind the ref as it mounts, passes later options through update(), destroys it as it unmounts, and returns what the factory reports, in the commit of the parent's own state, a controlled isCollapsed included (component:Resizable FR8). The factory follows the element behind the ref at every commit, and whenever a ResizeHandle given the ref attaches or lets go of its element, which a child can do without the hook's component rendering; each new factory starts where the region stood. A plain element is named and announced through the provider's catalog messages, which reach the factory through the package's internal channel. A ResizeHandle given the ref claims its element, so the factory binds to it instead of driving it: the handle renders the factory's region and forwards its pointer, keyboard and double-click events to the factory's input, after a caller's onKeyDown (FR7), and its own isReversed and isDisabled apply with the factory's options. A factory stays bound when the handle lets go of its element and claims it again. The handle follows the binding from the layout phase, so a factory bound in the commit that mounts or shows it renders before paint. A ResizeHandle with a `resizable` prop and a bound region warns once (useDevWarning).
…report rows and its changeset useResizable's page documents the handle ref passed first, with its options, and that a ResizeHandle reports its element as it mounts while a plain element is read when the hook's own component renders. UseResizableResult is exported, and the core API report gains the overload and the type (only those rows; on this base the committed report lacks rows that a later main refreshed).
5b99fc8 to
4905b79
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner deciding the option set of the new useResizable(handleRef, options) overload: its isReversed, isDisabled and label options beside ResizeHandle's own props, the unexported CreateResizableOptions type, and the attributes a plain element gets. An engineering owner can record that decision by commenting /approve-spec <full head SHA> (the author may, as one) or point to the governing record. Two changes are needed either way:
- The configuration form's
collapse(),expand()andresize()now do nothing when called before the hook's factory exists (for example from a child's mountuseLayoutEffect) or while an<Activity>hides the region, which worked on the base; please keep them working, add tests for those cases, and correct the "No behavior changes" changeset. - The diff is 5,090 authored lines, 3,836 of them from the still-open #9297; landing #9297 first and rebasing brings this pull request to about 1,410.
[Automated review]
| const getResizable = useCallback(() => slot.get()?.resizable ?? null, [slot]); | ||
| const collapse = useCallback(() => { | ||
| getResizable()?.collapse(); | ||
| }, [getResizable]); | ||
| const expand = useCallback(() => { | ||
| getResizable()?.expand(); | ||
| }, [getResizable]); | ||
| const resize = useCallback( | ||
| (next: number) => { | ||
| getResizable()?.resize(next); | ||
| }, | ||
| [getResizable], | ||
| ); |
There was a problem hiding this comment.
These controls reach only the factory the slot holds now. Before the first follow() layout effect, and after the hide cleanup sets the slot to null, getResizable() is null and the call is dropped, with no onCollapseChange or onSizeChange. On the base, resize(300) or collapse() from a child's mount useLayoutEffect, or collapse() while an <Activity> hides the region, takes effect. A no-factory path that applies the request to the slot's remembered selection, and fires the callbacks, would keep them working.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] The configuration-object `useResizable` and `ResizeHandle`'s `resizable` path go through one internal resize factory and one input module, through the same size model as before, so the region's state and the handle's input each have one implementation. No behavior changes. |
There was a problem hiding this comment.
"No behavior changes" doesn't hold at this head: configuration-form controls called before the factory exists or while an <Activity> hides the region are now dropped, and its dev warnings start createResizable: instead of useResizable:.
The second of five stacked pull requests for createResizable. It is built on the internal factory and the move of the configuration-object form and ResizeHandle onto it (#9297), and merges after it; this branch carries #9297's commits. This head's own changes: vjeux/astryx@315af0f...4905b79. The public export of createResizable with its docs and
component:ResizableFR3 (#9372) is stacked on it, then the LayoutPanel slide (#9300) and the deprecation (#9202), which lands in the same release as this pull request.User need
spec:AST-107FR6 has each interaction hook be its factory's thin React wrapper. With #9297 the factory exists inside the package and the configuration-object form builds on it, but React callers have no form that takes the handle itself.Problem and solution fit
useResizable(handleRef, options)creates createResizable on the element behind the ref as it mounts, passes later options throughupdate, destroys it as it unmounts, and returns the factory'ssize,isCollapsedand controls.isCollapsedis read from the options in the render that passes it, sosizeandisCollapsedland in the commit of the stateonCollapseChangeset (component:ResizableFR8).onKeyDown(FR7). Its ownisReversedandisDisabledapply with the factory's options: either one reverses or disables the handle.<Activity>, is bound at once, without the hook's component rendering.<Activity>. The region keeps its size and collapse state when an<Activity>hides and shows the hook's own component: the factory goes as the effects unmount, and the next one starts where the region stood.label. The hook reads a plain element when its own component renders.containerRef, at each commit of the hook's component.Current specification
spec:AST-107FR6architecture:public-component-apiINV22component:ResizableFR7 and FR8Public delta and caller burden
A
useResizable(handleRef, options)overload and theUseResizableResulttype; the core API report gains both. Its options are the single-region options plusisReversed,isDisabledandlabel; their type,CreateResizableOptions, exports withcreateResizableandResizableControllerin #9372, with FR3. The configuration-object form, ResizeHandle'sresizableprop and their behavior are unchanged.Design and alternatives
<Activity>shows its subtree, so a factory bound in the commit that mounts or shows a handle, or made for a reveal, would be missed until its next change. ResizeHandle and the hook follow the binding and the region from the layout phase, so both render before paint.isCollapsedin the parent's own commit and an<Activity>reveal; neither adds a public option or member.resizableprop and a bound region warns once, throughuseDevWarning.Evidence
Unit tests (red before this change: 23 failed):
update, destroys it on unmount, and returns what it reports.onCollapseChangeand the hook'ssizeandisCollapsedtogether, throughcollapse(), Enter,expand(), double-clicks and a drag past the threshold.<Activity>, and a factory made directly on a ResizeHandle's element across an<Activity>.<Activity>: the ref form keeps its size and collapse state when its component is hidden and shown.Scope
Testing
Local gates at 4905b79: unit tests (test:ui, 17,471 passed), check:repo, lint, root build then typecheck, the core API report regenerated from this build (it gains only this pull request's rows; on this base, 00146a7, the committed report lacks rows that #9344 later refreshed), the node project (only the known cache.test.mjs red), the docsite build and tests (732 passed), the Storybook build, accessibility in Chromium (77/77), the accessibility audit of Resizable, SideNav and Layout (0 new) and of every story (0 new), and SideNav's rail-motion browser proof in Chromium (10/10).