Repository navigation
feat(core): one shared edge reveal in the animated-layout module, which the SideNav rail moves onto - #8721
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
Changes requested. This pull request's own change is the 278-line glide routing in animatedLayout.ts and rectMorph.ts, but its diff against main carries the whole #8329 stack (19,553 authored lines), so please retarget it onto #8329 or rebase once #8329 lands. Two fixes are needed in the glide itself:
- A retarget keeps only the velocity along the new straight path, so in a wrapping flex row or a grid an element caught mid-glide by a sideways move turns abruptly, where
spec:AST-105's Coverage row "Elements already moving" says it keeps its running motion; please carry the full velocity and add an out-of-line case to the retarget test. - The rect morph sets
transform-origininline and removes it later, so a shifted element's own inlinetransform-originis lost after its first glide; set it only when the path scales, or restore the element's value.
The comment at animatedLayout.ts:494-497 still describes the old curve summing.
[Automated review]
| const travel = fromX * fromX + fromY * fromY; | ||
| const velocity = | ||
| travel > 0 | ||
| ? -(entry.velocity.x * fromX + entry.velocity.y * fromY) / travel | ||
| : 0; |
There was a problem hiding this comment.
This keeps only the part of the running velocity that lies along the new straight path. When the new move is out of line with the running glide (a wrapping flex row or a grid), the element turns at this frame instead of keeping its running motion, which spec:AST-105's Coverage row "Elements already moving" asks for: a glide down at 539 px/s whose place moves 160 px sideways 100 ms in restarts at (211, 102) px/s. Carrying the full velocity, for example a start velocity per axis for a path that keeps its size, plus an out-of-line case in the retarget test, would close this.
| // running glide stops (so its rectangle is its new layout place) and the | ||
| // rest of its curve carries into the one new glide, velocity included, | ||
| // summed with the new move. |
There was a problem hiding this comment.
This comment still describes the curve summing this change removes; the code below now keeps only the velocity along the new move, as the file header says.
| ownsOpacity ||= start.opacityAt != null; | ||
| // A dismissal on a copy let the element go (dismiss, below). | ||
| animator.attach(element); | ||
| element.style.setProperty('transform-origin', '0px 0px'); |
There was a problem hiding this comment.
On a layout glide this element is page content that never opted in (spec:AST-105 FR3). This line overwrites any inline transform-origin it has, and clearInline later removes the property, so the element's own value is gone after its first glide. A path that keeps its size plays no scale and needs no origin, so setting it only when the path scales, or restoring the element's own value, would avoid the loss.
|
Thanks. This pull request depends on #8329, and its own change is vjeux/astryx@d0f24e1...cf20a38. #8329's branch lives on a fork, which a pull request here can't target, so this one stays against
|
There was a problem hiding this comment.
Changes requested on scope: the diff against main still carries the whole #8329 stack, 19,768 authored lines of which this change is 509, so please update the branch from main once #8329 lands. The glide change itself now looks right: a retargeted glide keeps its running motion in any direction, the morph leaves an element's own transform-origin alone, and the retarget comment matches the code.
[Automated review]
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC |
|---|---|---|
| PowerSearch | 174.5 kB (+1011 B) · 20.3 kB CSS (0) | — |
| Collapsible | 20.6 kB (+920 B) · 4.5 kB CSS (0) | — |
| TreeList | 42.6 kB (+869 B) · 5.0 kB CSS (0) | — |
6 more components
| Component | Size | LoC |
|---|---|---|
| SideNav | 67.7 kB (+388 B) · 10.9 kB CSS (0) | — |
| AppShell | 71.5 kB (+3 B) · 12.0 kB CSS (0) | — |
| SideNavCollapseButton | 49.5 kB (0) · 9.8 kB CSS (0) | — |
| SideNavHeading | 61.5 kB (0) · 10.5 kB CSS (0) | — |
| SideNavItem | 57.6 kB (0) · 10.4 kB CSS (0) | — |
| SideNavSection | 6.6 kB (0) · 3.7 kB CSS (0) | — |
Package size: measured; main has no measurement of the merge base to compare yet.
API changes: core +8 · ~20 · −0 (report stale: run pnpm api:report); richtext +0 · ~1 · −0 (report stale: run pnpm api:report)
29 API changes (advisory)
| Package | Where | Signature | Usage | |
|---|---|---|---|---|
| + | core |
. · Types · interface FieldStatusProps |
message: string |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {message: …} |
| + | core |
. · Types · interface FieldStatusProps |
ref?: React.Ref<HTMLDivElement> |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {ref: …} |
| + | core |
. · Types · interface FieldStatusProps |
type: InputStatusType |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {type: …} |
| + | core |
. · Types · interface FieldStatusProps |
variant?: FieldStatusVariant = 'attached' |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {variant: …} |
| + | core |
. · Types · interface FieldStatusProps |
xstyle?: StyleXStyles |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {xstyle: …} |
| + | core |
. · Types · interface FieldStatusProps |
inherits AriaAttributes, DOMAttributes, HTMLAttributes |
import type {FieldStatusProps} from '@astryxdesign/core' |
| + | core |
. · Types · interface SearchContext |
interface SearchContext<T extends SearchableItem = SearchableItem> |
import type {SearchContext} from '@astryxdesign/core' |
| + | core |
./Typeahead · Also exported from an earlier entry point · SearchContext |
SearchContext (.) |
import {SearchContext} from '@astryxdesign/core/Typeahead' |
| ~ | core |
. · Components · component ComplexSelector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary and ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary and ghost selectors |
<ComplexSelector statusVariant="attached" /> |
| ~ | core |
. · Components · component DateInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<DateInput statusVariant="attached" /> |
| ~ | core |
. · Components · component DateRangeInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<DateRangeInput statusVariant="attached" /> |
| ~ | core |
. · Components · component Field |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Field statusVariant="attached" /> |
| ~ | core |
. · Components · component FieldStatus |
component FieldStatus(props: FieldStatusProps) → component FieldStatus @deprecated |
<FieldStatus /> |
| ~ | core |
. · Components · component FieldStatus |
prop variant?: FieldStatusVariant = 'attached' → prop variant: FieldStatusVariant = 'attached' |
<FieldStatus variant="attached" /> |
| ~ | core |
. · Components · component FileInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<FileInput statusVariant="attached" /> |
| ~ | core |
. · Components · component MultiSelector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary and ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary and ghost selectors |
<MultiSelector statusVariant="attached" /> |
| ~ | core |
. · Components · component NumberInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<NumberInput statusVariant="attached" /> |
| ~ | core |
. · Components · component PowerSearch |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<PowerSearch statusVariant="attached" /> |
| ~ | core |
. · Components · component Selector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary/ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary/ghost selectors |
<Selector statusVariant="attached" /> |
| ~ | core |
. · Components · component TextArea |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TextArea statusVariant="attached" /> |
| ~ | core |
. · Components · component TextInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TextInput statusVariant="attached" /> |
| ~ | core |
. · Components · component TimeInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TimeInput statusVariant="attached" /> |
| ~ | core |
. · Components · component Tokenizer |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Tokenizer statusVariant="attached" /> |
| ~ | core |
. · Components · component Typeahead |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Typeahead statusVariant="attached" /> |
| ~ | core |
. · Types · interface FieldStatusProps |
interface FieldStatusProps extends BaseProps<HTMLDivElement> (props of FieldStatus) → interface FieldStatusProps extends BaseProps<HTMLDivElement> |
import type {FieldStatusProps} from '@astryxdesign/core' |
| ~ | core |
. · Types · interface FieldStatusVariantMap |
tooltip: true → tooltip: true @deprecated |
import type {FieldStatusVariantMap} from '@astryxdesign/core'; const x: FieldStatusVariantMap = {tooltip: …} |
| ~ | core |
. · Types · interface SearchSource |
search: (query: string) => Promise<T[]> | T[] → search: (query: string, context?: SearchContext<T>) => Promise<T[]> | T[] |
import type {SearchSource} from '@astryxdesign/core'; const x: SearchSource = {search: …} |
| ~ | core |
. · Types · interface UseInputStatusIconOptions |
statusVariant?: FieldStatusVariant → statusVariant?: FieldStatusPlacement |
import type {UseInputStatusIconOptions} from '@astryxdesign/core'; const x: UseInputStatusIconOptions = {statusVariant: …} |
| ~ | richtext |
. · Components · component RichTextEditor |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusVariant | 'tooltip' = 'attached' |
<RichTextEditor statusVariant="tooltip" /> |
Not run on this PR: add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.
Generated by PR Enrichment workflow | View full report
|
Merging main here isn't mechanical: |
|
Posted by Cindy's agent on her behalf. A heads-up on this conflict: it isn't only textual. Main now has the in-place swap in |
|
Thanks, will do. I'll rework this onto main keeping the in-place swap (#8734), the rail collapse (#8723) and the follower's ride intact, with SideNav's rail-motion tests and the rail-collapse browser spec as the oracle, and I'll read #8783 first. The glide keeps the |
|
Routing the glide through the rect morph would break three things your module now guarantees and tests: the rail's one clock (the animator holds a curve's end for 60 s), the swap's parts sampling frame for frame with the glide, and #8639's single layout pass (the animator reads each element's style as it starts it). Making the morph honor them means giving it your sampler, so it would share only a name. Since the module already plays a compositor-only translate and keeps a retargeted glide's velocity, shall I drop INV12's rect-morph clause and close this? |
cf20a38 to
14bcc93
Compare
There was a problem hiding this comment.
This needs an engineering owner's decision before it can be approved. spec:AST-105 FR9, which covers the edges other than block-end and the resized-element path, is not on main yet, so either #8783 lands first or an engineering owner approves that direction here by commenting /approve-spec 14bcc93db8f5c08542e9f7a5bec1ddd996ce9688 (the author can do this as an engineering owner). Whatever the decision, three changes are needed:
- When two changes compose, as in a
CollapsibleGroupinsinglemode switching items, the edge rides the neighbour's whole glide offset, so please ride it relative to the glide that moves the element (animatedLayout.ts:1401-1407) and add an exclusive-group case, since the opening content now overlaps the next item by about 72 px at 60 ms. - The changeset says a reversal always continues from what it shows, but under Reduce Motion with a coarse pointer the cross-fade still restarts, so please track the fade or qualify that sentence.
- At 2,809 authored lines the PR is over the size threshold, so please either say in the description that it is a behavior-module PR for
motion/animatedLayout.tsor split it.
[Automated review]
| } | ||
| // The neighbour's offset from its place, towards the edge's side. | ||
| const sign = isEnd ? 1 : -1; | ||
| const hidden = (t: number): number => { |
There was a problem hiding this comment.
This uses the neighbour glide's whole offset. When another change in the same batch also moves the element (its root glides, as with a CollapsibleGroup in single mode switching from an upper item to a lower one), the edge is off by that change's remaining travel: about 72 px of overlap at 60 ms in Chromium, where the base is flush. Subtracting the offset of the glide that moves the element itself (its own, or its nearest gliding ancestor's) before hiddenAt would keep the two together.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] With an `animatedLayout` handle, a Collapsible or a TreeList row closed while it opens, or opened again while it closes, continues from what it shows: the content's edge stays on the content after it on every frame, instead of jumping back to fully shown or hidden. |
There was a problem hiding this comment.
Under Reduce Motion with a coarse pointer, neither cross-fade is tracked (playLayoutChange's fade returns early, and holdLayoutExit tracks only its spring branch), so a reversal still restarts at opacity 1 or 0. Please either track the fade or qualify this sentence.
14bcc93 to
dd3eeec
Compare
There was a problem hiding this comment.
Changes requested: the shared edge reveal fixes a Collapsible or TreeList row reversed mid-motion, but its ride regresses when another change in the same batch moves the revealed element. Please address the points below and add a browser case for an exclusive CollapsibleGroup switch between items of unequal height.
rideNeighbouruses the neighbour glide's whole offset, so in aCollapsibleGroup(single) with a handle, opening an item while a taller one above it closes shows its content in full over the next trigger (about 52 px at 60 ms) wheremainstays flush; subtract the offset of the glide that moves the element itself.- The changeset says a reversal always continues from what it shows, but under Reduce Motion with a coarse pointer the cross-fades are not tracked and still restart; track them or qualify the sentence.
- At 2,874 authored lines, please declare this a behavior-module PR naming
packages/core/src/motion/animatedLayout.tsin the description, or split it.
[Automated review]
| const hidden = (t: number): number => { | ||
| const offset = glide.offsetAt(t); | ||
| return hiddenAt( | ||
| sign * | ||
| (edge.startsWith('inline') ? (rtl ? -offset.x : offset.x) : offset.y), | ||
| ); | ||
| }; |
There was a problem hiding this comment.
This uses the neighbour glide's whole offset. When another change in the same batch also moves the revealed element (an ancestor glides, as in a CollapsibleGroup in single mode switching between items of unequal height), the edge is off by that glide's remaining travel: the opening content shows in full over the next trigger, and a closing one is cut short. Subtract the offset of the glide that moves the element itself (its own, or its nearest gliding ancestor's) at the same t.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] With an `animatedLayout` handle, a Collapsible or a TreeList row closed while it opens, or opened again while it closes, continues from what it shows: the content's edge stays on the content after it on every frame, instead of jumping back to fully shown or hidden. |
There was a problem hiding this comment.
Under Reduce Motion with a coarse pointer neither cross-fade is tracked (playLayoutChange returns its fade untracked, and holdLayoutExit tracks only its spring branch), so a reversal there still restarts at opacity 1 or 0. Track the fades as edge motions, or qualify this sentence with "with motion on".
dd3eeec to
d76a1a9
Compare
|
Addressed in d76a1a9:
|
There was a problem hiding this comment.
Changes requested: the shared edge reveal fixes Collapsible and TreeList reversals and moves the SideNav rail onto the module cleanly, but the one-state conceal from a start edge does not work yet. holdLayoutExit with edge: 'block-start' or 'inline-start' still frees the element's space by its end margin, so the element jumps by its own size the moment the conceal starts and a gap opens beside its moving edge (80 px for a block-start conceal in an end-aligned column, 120 px for an end panel's inline-start conceal). Please free the space on the edge's side (as holdExtent does with margin-${edge}) or refuse start edges in holdLayoutExit, and add the block-start reveal-and-conceal browser case that the FR9 Coverage row lists.
[Automated review]
| handle: AnimatedLayout | null | undefined, | ||
| element: HTMLElement, | ||
| done: () => void, | ||
| {edge = 'block-end'}: {edge?: LayoutEdge} = {}, |
There was a problem hiding this comment.
This option reaches the clip and the ride, but the space is still freed by giveUpSpace below, which always uses the end margin. For block-start or inline-start the content that moves is the content before the element, so the negative end margin moves the element itself by its whole size when the conceal starts. In a Chromium probe, a block-start conceal in an end-aligned column jumped 80 px and kept its shown edge 84 px from the row it rides (4 px at rest), and an end panel's inline-start conceal jumped 120 px. Freeing the start margin against the content before (as holdExtent does with margin-${edge}), or refusing start edges here, would fix it. A block-start reveal-and-conceal browser case, as the FR9 Coverage row lists, would cover it.
d76a1a9 to
7eb2a20
Compare
|
Addressed in 7eb2a20: |
There was a problem hiding this comment.
Recommending approval: the shared edge reveal now matches spec:AST-105 FR9 at this head, and the start-edge conceal raised on the previous revision frees space on its own side and holds the element in place. Collapsible and TreeList reversals continue from what they show, and the SideNav rail passes its existing browser spec after its move onto the module's two-state form. Two optional follow-ups: reportLayoutEdge and its tests could ship with #9197 for a smaller diff, and SideNav FR6's implementation-gap note can be updated once this lands.
[Automated review]
7eb2a20 to
70ebcc7
Compare
There was a problem hiding this comment.
Recommending approval: the shared edge reveal matches spec:AST-105 FR9, Collapsible and TreeList reversals now continue from what they show, and the SideNav rail moves onto the module with its existing proofs passing. Two optional follow-ups, neither blocking:
giveUpSpacestill picks its scale axis fromisRowwhile the margin now follows the edge (see the inline note).component:SideNavFR6 andspec:AST-105's current-state impact still describe the rail's move as an open gap; a record-only follow-up can drop that text.
[Automated review]
| element.style.setProperty(property, `${-size}px`); | ||
| const guess = placeOf(next, rtl)[axis]; | ||
| const guess = placeOf(checked[0], rtl)[axis]; | ||
| const correction = (target[axis] - guess) / perPx; |
There was a problem hiding this comment.
Nit, non-blocking: perPx (line 1665) still follows isRow, but the margin now follows edge, so an inline-edge exit in a block container, or a block-start exit in a flex row, divides this correction by the other axis's scale. Under a non-uniformly scaled ancestor that misses the landing check and the exit hides at once. const perPx = isInline ? scale.x : scale.y; fixes it and leaves the default block-end path unchanged.
…ch the SideNav rail moves onto spec:AST-105 FR9's edge reveal, the one edge reveal in Core: - The one-state form (playLayoutChange, holdLayoutExit) reveals and conceals from the block-end edge by default, or from the block-start or an inline edge it is given, right to left mirrored. Once the content on that edge's side glides, the moving edge rides that glide frame by frame, so no gap opens, and a reversal (a close while it opens, an open while it closes) starts from what the element shows. - The two-state form (holdLayoutEdge, playLayoutEdge) holds the old state as the swap's 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 caller adds only its own fades. Reduce Motion follows design:reduced-motion DR2 and DR3. - A resized element with no copy (reportLayoutEdge, a resize controller's pane): measured before its size change commits and played after the new size applies; a smaller extent is held at the extent it was laid out at, its edge's margin freeing the difference, and clipped to the new one. - 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. - The edge rides the content beside it relative to any glide that moves the element itself in the same batch, such as an item above them closing in a single-open CollapsibleGroup; the AnimatedLayout story gains one. - The SideNav rail plays through the two-state form on its inline-end edge and keeps only its labels' and icons' fades. Collapsible and TreeList keep their block-end form and gain the ride and the reversal. Nothing public changes: the new functions stay out of every entry point.
70ebcc7 to
f20b096
Compare
There was a problem hiding this comment.
Recommending approval: the shared edge reveal matches spec:AST-105 FR9 as it stands on main, Collapsible and TreeList reversals now continue from what they show in both directions, and the SideNav rail moves onto the module with its existing proofs passing. Two small follow-ups, neither blocking: giveUpSpace still takes its scale axis from isRow while the margin now follows the edge (see the inline note), and component:SideNav FR6 and spec:AST-105's current-state impact still describe the rail's move as an open implementation gap.
[Automated review]
| element.style.setProperty(property, `${-size}px`); | ||
| const guess = placeOf(next, rtl)[axis]; | ||
| const guess = placeOf(checked[0], rtl)[axis]; | ||
| const correction = (target[axis] - guess) / perPx; |
There was a problem hiding this comment.
Nit, non-blocking: perPx (line 1665) still follows isRow, while property and axis now follow edge. An inline-edge exit in a block container, or a block-start exit in a flex row, under a non-uniformly scaled ancestor divides this correction by the other axis's scale, misses the landing check and lands at once. const perPx = isInline ? scale.x : scale.y; matches what holdExtent already does.
Implements the records #8783 put on main:
spec:AST-105FR2 and FR9,architecture:react-component-runtimeINV12,component:SideNavFR6,component:ResizableFR8,component:LayoutPanelFR7 and AR2, andcomponent:TableFR15. 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-105FR9's edge reveal, in three forms over one ride: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.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:SideNavFR6); its frames are unchanged.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:ResizableFR8 andcomponent:LayoutPanelFR7 in refactor(Resizable): the resize model's pure parts live in one non-React module #9197 and feat(Resizable): deprecate useResizable's configuration-object form for useResizable(handleRef, options) #9202).Reduce Motion follows
design:reduced-motionDR2 under a coarse primary pointer and DR3 otherwise.Current specification
spec:AST-105FR9 ("One shared edge reveal") and FR2,architecture:react-component-runtimeINV12,component:SideNavFR6,component:ResizableFR8,component:LayoutPanelFR7 and AR2, andcomponent:TableFR15, all current on main since #8783. A LayoutPanel given a hand-builtResizablePropsgets no slide, and the hook's ownpropspassed throughresizableslide; 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.tschecks this), anduseAnimatedLayout'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
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:changesetsand the motion, SideNav and AppShell unit suites (1,020) pass, andspecs:forstill returnscomponent:SideNavandspec:AST-105for the changed files. At 70ebcc7 every gate finished:test:ui,check:repo, the node suites (only the twocache.test.mjslock 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:repoand 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
translateis 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
Overlaps: #8677 also edits
motion/animatedLayout.ts(the theme's motion knobs), and #3890 editsSideNav/SideNav.tsx. This PR leavesSideNav.tsxunchanged.Testing
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=4and 1 worker.