Skip to content

fix(core): layout glides play the theme's move curve, not a spring - #10236

Merged
cixzhang merged 2 commits into
mainfrom
reland/layout-glide-move-curve
Nov 6, 2026
Merged

cixzhang merged 2 commits into
mainfrom
reland/layout-glide-move-curve

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Supersedes #10094 (co-authored by vjeux)

The change of #10094, re-landed whole on current main from a maintainer branch with the author as co-author. It lands whole because it also adds theme/tokenReferences.ts to the WebKit interaction lane's scope pattern in .github/workflows/ci.yml.

It is #10094's head e69dd39d (which adds the fix that keeps the expand chevron on the area's glide timing under a nested theme), merged with main. The WebKit scope line in .github/workflows/ci.yml, which main also changed, is main's line plus packages/core/src/theme/tokenReferences.ts. The files main also changed since #10094's base merge with no conflict; every other file matches e69dd39d byte for byte.

A follow-up replaces the WebKit lane's hand-kept scope pattern with one derived from where the interaction specs live; this entry moves into its shared-code pattern.


The animated-layout glide now plays the theme's move curve, --ease-move, over --duration-medium-min, as spec:AST-105 FR4 and DEC-2 require. It no longer plays the settle spring. This retires #8677's interim line: "Until the glide moves to the curve tokens (AST-105 DEC-2), its interim springs.settle follows tempo under every pointer."

User impact

People who open a Collapsible, a TreeList branch or a Table row's panel, or who collapse AppShell's side rail, see the content after it glide. On main that glide plays the settle spring, which lasts about 735 ms, longer than the theme's other medium motion. A theme's character never reaches it, so a crisp or expressive theme glides the same as the standard one. With this change the glide follows the theme: 225 ms in the Neutral theme and 310 ms with no theme, on the theme's move curve.

Problem and solution fit

  • Why does the glide feel off? It plays a spring instead of the theme's motion tokens.
  • Why does that hurt? The theme's other medium motion runs on the curve tokens, so the glide lands well after the motion around it, and character has no effect on it.
  • Why does that matter? FR4 promises one motion that follows the theme, and AppShell, Collapsible, Table row expansion and TreeList all play whatever the module plays.

The fix changes only where the module gets its timing. It reads --duration-medium-min and --ease-move where the area is, through #10068's readThemeTiming. Summing a running glide with a new move works on any progress curve, so that code is unchanged.

Expected behavior and authority

  • spec:AST-105 FR4 and DEC-2: the glide, the in-place swap and the edge reveal play --ease-move over --duration-medium-min, never a spring. The record's gap line saying they still play springs.settle goes, since FR4 now holds.
  • spec:AST-094 FR45: motion that a tap or a click starts plays the theme's motion tokens.
  • spec:AST-108 FR2: the duration tokens carry the theme's tempo, so the glide is not scaled twice.

Smallest restoration

  • motion/animatedLayout.ts reads the curve through readThemeTiming. A zero duration, or an easing the browser cannot play, lands the change at once. The motion's kind is renamed from 'spring' to 'glide'. A disclosure's indicator takes var(--duration-medium-min) and var(--ease-move) as CSS values instead of a sampled linear().
  • A running glide is read as the browser draws it: a straight line between two of its keyframes. The curve bends more than the spring did where a retarget lands, so reading the true curve there left a jump of up to about 0.3 px on an 80 px move. A new glide now starts exactly where the old one shows, and the retarget specs hold that within 0.01 px.
  • AppShell's three checks of the motion's kind, and a comment in SideNav's rail, follow the rename.
  • Reduce Motion is unchanged: a cross-fade under a coarse pointer, an immediate change otherwise.
  • The theme-timing reader keeps a typed copy of the timing tokens' declared values, which a unit test holds equal to the token file's. Importing it runs no StyleX, so the animated-layout module and the server-safe factories that import it stay free of DOM work at import.
  • The theme-timing reader plays an easing that is not a cubic Bézier as the browser plays it: a paused animation with that easing gives its eased progress. A theme's --ease-move can be a linear() or steps() function, which the motion scale's easing option writes, and such a theme got no glide. A jump in such an easing carries no speed, so a retarget at a jump inherits none. The segmented control's selection, the reader's other caller, now plays such an easing too instead of moving at once.
  • The WebKit interaction lane's scope covers theme/tokenReferences.ts: the glide reads its timing through the theme-timing reader, which resolves token references, so the WebKit proofs' bundle of the module carries that file. The lane's routing test requires every bundled source in scope.
  • Merging with fix(core): a calc() duration token times the press release clock #10069: one conflict in themeTiming.ts. Keep firstEasingText with its JSDoc, and leave firstEasing's one-line JSDoc dropped, as fix(core): a calc() duration token times the press release clock #10069 drops it.
  • No public API changes.

Evidence

  • Reproduction before the change: on the base, a follower's glide lasts 734.77 ms in Chromium (springs.settle), whatever the scope's --duration-medium-min and --ease-move.
  • Desktop, with a mouse: in Chromium on Storybook's Neutral theme (Core/Animated layout), main glides the content after the Collapsible and the TreeList in 734.77 ms; this change takes 225 ms, on cubic-bezier(.24, 1, .4, 1). A quarter of the way through the time, the content is 70.5% of the way there on main and 78.8% with this change.
  • Result after the change, measured in Chromium after a mouse click: 310 ms with no theme, and 233, 310 and 465 ms in Storybook's tempo 0.75, 1 and 1.5 rows (Core/Theme motion tempo). Every keyframe sits on the theme's curve: standard cubic-bezier(0.24, 1, 0.4, 1), crisp and expressive. In Storybook's Neutral theme it lasts 225 ms.
  • Representative unchanged path: Reduce Motion by pointer, a retarget of a running glide (the rest of the old curve plus the new move) and FR9's edge reveal. The animated-layout browser spec passes 90 of 90 in Chromium and in WebKit. Every glide in Storybook's Core/Animated layout (Collapsible and TreeList) and in the Core/Theme motion tempo rows has the same durations and keyframes, byte for byte, as at c36b15d, the head card 80 passed.
  • Import safety: a server test imports the reader and the animated-layout module in plain Node. Without the copy both fail with "Unexpected 'stylex.defineVars' call at runtime".
  • A linear() or steps() move easing, red without the reader change on a not-for-landing arm (vjeux@52cd5efb04) in Chromium and WebKit: the FR4 cases for linear(0, 0.8 50%, 1) and steps(4, jump-end) find no glide, the reader's case for both patterns finds no curve, and a click's slide in the segmented control lands at once (1 ms where the theme gives 125 ms). A mutant that ignores the browser's easing (vjeux@cd83d61530) fails the same cases on the curve: −75.79 px where linear() gives −73.26 px at 16.3 ms. All pass with the change.
  • A jump carries no speed: at a jump of steps(4) over 300 ms, a two-sided difference reads about 125 progress per second. The curve now reads 0 there, and the unit cases for it fail without the change.
  • The adopters' edge proofs, which read the module's clock, are on main since test(core): the adopters' edge proofs read the module's clock #10150. They pass against this change in Chromium and WebKit.
  • Regression proof: 6 unit tests are red on the base and green after. The browser spec's 5 curve, tempo and character cases are red on the base ("Expected 310, Received 734.77") and green after. With the 0.01 px bound, the retarget cases fail 11 of 18 runs on a not-for-landing arm without the drawn reading (vjeux@f635a8653d, jumps of 0.013 to 0.055 px) and pass 18 of 18 with it.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm build, pnpm typecheck and CI=true pnpm lint on the changed files.
  • pnpm test:ui: 21,455 passed.
  • The WebKit lane's routing test, ci-interaction-webkit.test.mjs: 21 of 21 (red without the scope line: the animated-layout bundle carries theme/tokenReferences.ts); every test that reads ci.yml, 647 of 647.
  • Interaction specs, Chromium 169 of 169 and WebKit 154 of 154 with 15 skipped by design: animated layout (90), the adopters' edge proofs (6), theme timing (7), theme tempo (6), segmented control (48; 3 skipped in WebKit), SideNav rail motion (12, Chromium only).
  • Accessibility specs: Table row glide (12 of 12), Collapsible close focus, TreeList collapse focus, Table cell menus.
  • Before and after: Core/Animated layout's Collapsible and TreeList, on main's deploy and on this head's.

Writing rubric results

AST-105 is the one record this pull request touches.

  • R1 narration: one sentence, the "Out of coverage" line saying the glide, the swap and the edge reveal still play springs.settle "today" until FR4 moves them, described a change, not the system. It is deleted. Current-state impact keeps its lines about the present state, which that section is for.
  • R2 research: none in the record.
  • R3 evidence: no evidence or verification cell names a pull request, issue or commit.
  • R4 rejected alternatives: two, in DEC-1 and DEC-2, each one line inside its decision.
  • R5 change references: none.
  • R6 open questions: none.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
@cixzhang cixzhang added the ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) label Nov 6, 2026
@vercel

vercel Bot commented Nov 6, 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 6, 2026 9:14am UTC

Request Review

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

astracat-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 6, 2026, 9:32 AM UTC

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 6, 2026
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 3b7f815

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 16bca2c

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
TableBody 3.5 kB (0) · 3.3 kB CSS (0) — ✓ · ✓ · ✗
TableFooter 3.5 kB (0) · 3.3 kB CSS (0) — ✓ · ✓ · ✗
TableHeader 3.5 kB (0) · 3.3 kB CSS (0) — ✓ · ✓ · ✗
17 more components
Component Size LoC Visual · A11y · RTL
TableRow 6.7 kB (0) · 3.9 kB CSS (0) — ✓ · ✓ · ✗
AppShell · Storybook 80.2 kB (−1.4 kB) · 12.8 kB CSS (0) 1,096 (0) ✗ · ✓ · ✓
PowerSearch 268.5 kB (+409 B) · 23.1 kB CSS (0) — — · — · —
SegmentedControl 89.4 kB (+330 B) · 6.8 kB CSS (0) — ✓ · ✓ · ✓
DateTimeInput 194.0 kB (+213 B) · 18.1 kB CSS (0) — — · — · —
SegmentedControlItem 16.0 kB (0) · 4.3 kB CSS (0) — ✓ · ✓ · ✓
SideNavCollapseButton 52.4 kB (0) · 9.4 kB CSS (0) — ✓ · ✓ · ✓
SideNavHeading 66.7 kB (0) · 10.6 kB CSS (0) — ✓ · ✓ · ✓
SideNavItem 61.6 kB (0) · 10.4 kB CSS (0) — ✓ · ✓ · ✓
SideNavSection 6.8 kB (0) · 3.7 kB CSS (0) — ✓ · ✓ · ✓
Table 99.5 kB (0) · 17.3 kB CSS (0) — ✓ · ✓ · —
TableCell 91.3 kB (0) · 17.0 kB CSS (0) — ✓ · ✓ · ✓
TableHeaderCell 90.9 kB (0) · 16.8 kB CSS (0) — ✓ · ✓ · ✓
TableSelectionToolbar 61.1 kB (0) · 12.4 kB CSS (0) — ✓ · ✓ · ✓
Collapsible 24.1 kB (−1.2 kB) · 4.5 kB CSS (0) — — · — · —
TreeList 40.3 kB (−1.2 kB) · 5.4 kB CSS (0) — — · — · —
SideNav 80.1 kB (−1.5 kB) · 10.7 kB CSS (0) — ✓ · ✓ · ✓
Visual changes: 2 snapshots in 1 component

A change is a question for the reviewer, not a failure.

Component · story Before After Diff
AppShell · Playground (neutral, dark) before frame after frame diff frame
AppShell · Playground (neutral, light) before frame after frame diff frame

Package size: @astryxdesign/core 6.71 MB packed (+2.2 kB); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: cli 19 rows, core 21 rows; run pnpm api:report.

Touched components checked; add ci:run-visual or ci:run-rtl to run the full suites.

CI impact: +5.5 job-min per update · pool +4.7% · standard runners · 1 CI action or script file changed (not measured)
Before After Change
Job-minutes per PR update 116.8 122.3 +5.5
Runner pool, runner-minutes a day 53,692 56,224 +2,532 (+4.7%)
Average concurrent jobs 37.3 39 +1.8
Time to green (median → this run) — 20.2 min —
Runs a day 460 460 0

Changed jobs: pr-interaction-webkit changed: standard

Not measured (the jobs that use these keep their recent minutes above): .github/scripts/ci-interaction-webkit.test.mjs.

Assumptions: before is each job's average over the last 20 pull-request runs; after keeps that for unchanged jobs and uses this run's minutes for added or changed ones, weighted by how often each changed job ran in those runs; runs a day averages the seven whole days before today; time to green compares this run with the median of those runs that ran the same jobs.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: one fix stands between this change and approval.

The glide now plays --ease-move over --duration-medium-min, as spec:AST-105 FR4 and DEC-2 require. The new curve, tempo, character, linear(), steps() and retarget cases fail against main and pass here in Chromium and WebKit. An engineering owner's /approve-spec for this head covers the record edit and the mouse and trackpad retiming.

  • Under a theme scope nested between the area and a Collapsible, TreeList or Table row, the chevron's var(--duration-medium-min) and var(--ease-move) (packages/core/src/motion/animatedLayout.ts:3078-3082) resolve at the chevron while the glide reads the area's tokens, so the chevron turns on different timing from the content it reveals (620 ms against a 310 ms glide in a probe), which component:Collapsible FR10 and component:TreeList FR9 rule out; write the duration and easing read at the area instead, and add a nested-scope case to the indicator tests.

[Automated review]

Comment on lines +3080 to +3082
'var(--duration-medium-min)',
);
element.style.setProperty('transition-timing-function', 'var(--ease-move)');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These var() values resolve where the chevron sits, but glideProgress reads --duration-medium-min and --ease-move on the area. Under a theme scope nested between the two, the chevron turns on that scope's timing while the revealed content and the rows after it glide on the area's: with the area at 310 ms and a nested scope at 620 ms on cubic-bezier(0, 0, 0.2, 1), the chevron computes 0.62s cubic-bezier(0, 0, 0.2, 1) against a 310 ms glide. component:Collapsible FR10 and component:TreeList FR9 keep the chevron on the same motion. Writing the duration and the easing text read at the area (as the base wrote the area's sampled values) keeps them together; a nested-scope case in the indicator tests would hold it.

Takes #10094 at e69dd39 (the chevron-timing fix), merged with main.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 16bca2c

@cixzhang
cixzhang marked this pull request as ready for review November 6, 2026 09:22

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved pending CI: CI is still running on this head.

The glide now plays --ease-move over --duration-medium-min, as spec:AST-105 FR4 and DEC-2 require, and the chevron takes the duration and easing read at the area, so it keeps the glide's timing under a nested theme scope. The new chevron cases fail against the previous revision and pass here, and the animated-layout browser spec passes in Chromium and WebKit. An engineering owner's /approve-spec for this head covers the record edit and the mouse and trackpad retiming.

[Automated review]

github-actions Bot added a commit that referenced this pull request Nov 6, 2026
@cixzhang
cixzhang merged commit 8bd8c61 into main Nov 6, 2026
115 checks passed
cixzhang added a commit to vjeux/astryx that referenced this pull request Nov 6, 2026
cixzhang added a commit to vjeux/astryx that referenced this pull request Nov 6, 2026
cixzhang added a commit to vjeux/astryx that referenced this pull request Nov 6, 2026
@cixzhang
cixzhang deleted the reland/layout-glide-move-curve branch November 6, 2026 10:25
cixzhang added a commit that referenced this pull request Nov 6, 2026
…10229)

* fix(core): layout glides play the theme's move curve, not a spring

The animated-layout glide, the in-place swap and the edge reveal played
the settle spring, about 735 ms, so a theme's `character` never reached
them, and on a mouse they kept moving after the motion around them had
landed. They now play the theme's move curve, `--ease-move`, over
`--duration-medium-min`, read where the area is through the shared
theme-timing reader. The tokens carry the theme's `character` and
`tempo`, so nothing scales them twice. A zero duration lands the change
at once. Reduce Motion is unchanged: a cross-fade under a coarse
pointer, an immediate change otherwise.

A running glide is now read as the browser draws it, a straight line
between two of its keyframes, so a new glide starts exactly where the
old one shows. The curve bends more than the spring did where a
retarget lands, and reading the true curve there left a jump of up to
about 0.3 px on an 80 px move.

A disclosure's indicator takes the same curve and duration as CSS
values. The motion's kind is now named for what it is, a glide, in
AppShell and SideNav's rail too, which share it. The record's line that
the glide still plays the spring goes, since FR4 now holds.

The glides of AppShell's content area, Collapsible, Table's row
expansion and TreeList change feel: 225 ms in the Neutral theme and
310 ms with no theme, instead of about 735 ms.

* fix(core): the theme-timing reader runs no StyleX at import

The reader took core's declared timing values from the token file,
which declares its variables through StyleX. Importing it ran
`stylex.defineVars`: DOM work under the test runtime, and an error in a
plain Node process. Since the glide reads the theme's timing, the
animated-layout module and every factory that imports it took that on.

The reader now keeps a typed copy of the timing tokens' declared
values, which a unit test holds equal to the token file's, and imports
only the token names, as types. A server test imports the reader and
the animated-layout module in plain Node.

* fix(core): the glide plays a linear() or steps() move easing

A theme's move easing can be a linear() or steps() function: the motion
scale's easing option writes one. The theme-timing reader read only a
cubic Bézier, so such a theme gave no curve and the change landed at
once. The reader now plays any other easing as the browser plays it: a
paused animation with that easing gives its eased progress. A jump in
such an easing carries no speed, so a retarget at a jump inherits none.
The segmented control's selection reads its timing the same way, so it
plays such an easing too.

* test(core): the glide keeps advancing through a blocked main thread

The animated-layout glide runs on the compositor (AST-094 FR5), but no
proof read it while the main thread is blocked. This one blocks the page
for 700 ms across a glide and judges the screencast against the
harness's compositor control: the content after a Collapsible, after a
TreeList branch and after a Table row keeps moving on the frames the
control moves.

The judgment reads the block while the glide's own keyframes move the
follower by a cast step or more per frame; an easing tail moves under
what the cast can show. Chromium only: Linux WebKit starts the glide on
its compositor 30 to 100 ms late, while Apple's WebKit (the iOS 26.2
Simulator) moves on every frame from the block's start.

* spec(core): AST-094's FR5 verification covers the shared glide

FR5 holds that motion runs on the compositor and keeps advancing through
a main-thread block. Its verification gains a row for the
animated-layout glide's busy-main-thread proof, and the record links the
proof's file, so a lookup on that file finds the claim it verifies.

* spec(core): AST-094's FR5 glide row names its WebKit evidence gap

The row's proof runs in Chromium. WebKit rests on a spot-check on the
iOS Simulator, not an automated run, and the row says so, so FR5 does
not read as fully covered.

* test(core): the busy proof types the glides it reads

The page reads the glides into an array declared with no type, which
the type check rejects as an implicit any. It now holds GlideRead
entries, each keyframe pair typed as a pair.

* Merge main into the glide proof branch (#10236 landed)

* test(core): the busy glide proof decodes each frame once

The busy proof decoded every frame of its screencast twice, once for
the marker and once for the control. On a loaded machine that decoding
ran past the test's 60 s budget. The harness's trackMarkers finds
several colors in one decode of each frame, and the proof decodes only
the frames busyWindowProof reads, up to BUSY_PROOF_LOOKAROUND_MS past
the judged part. The verdict is unchanged. The glide's length is set
through both short medium durations, whichever the module plays.

* Merge branch 'main' into uikit/f1-glide-busy-proof

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
@astracat-bot astracat-bot Bot mentioned this pull request Nov 7, 2026
3 tasks done
cixzhang added a commit that referenced this pull request Nov 7, 2026
…tedLayout handle (#8106)

* feat(List): animate row inserts, removals and moves with animatedLayout

A List that takes the handle useAnimatedLayout returns animates its row
changes on one shared motion, for every pointer. An inserted row enters,
a removed row collapses in place, and a row that crosses rows moving the
other way rides above them. The rows after a change, and the content
after the list, glide to their new places. Rows match across renders by
their itemKey; a row without one changes at once.

The row marks the update module writes (item key, leaving, travelling)
live in their own private-attribute owner modules. A list under the
handle isolates for every pointer, so a sliding row stays below a
page-level sticky header.

The List and ListItem records now say the row updates and itemKey are
implemented, and their verified_by lists the files that verify them.

* Merge main into the branch (#10236 landed)

* fix(core): the layout glide takes --duration-medium

Owner decision, Nov 6 2026 (Cindy): the layout glide takes
--duration-medium. The shared glide played over --duration-medium-min;
it now plays over --duration-medium: 300 ms in the Neutral theme and
410 ms with no theme, instead of 225 ms and 310 ms, both scaled by
tempo. Collapsible, TreeList, Table's row expansion, AppShell's content
area and List share it. AST-105 FR4 names the token, and its DEC-3
records the decision.

* Merge uikit/f1-glide-medium into uikit/c11-list-updates

* fix(List): a change reads every near row before it settles any

Settling a swiped row read the next row's transform after writing the
row before it, and read every row's place after all the writes. One
pass now reads each near row's translation and place, and then the
settles are written. A settled row's place is its box less its own
translation.

* Merge main into the List updates branch (#10261 landed)

* Merge branch 'main' into uikit/c13-list-reorder

* docs(AST-105): DEC-3 names List among the glide's adopters

This pull request makes List adopt the shared layout glide, so DEC-3
names it with Collapsible, TreeList, Table's row expansion and
AppShell's content area. The merge of main took main's side of DEC-3,
which lists only the components already on main.

* fix(List): measuring a change only reads

A row a swipe translated keeps its translation across the commit, so a
change with a row swiped open lays the page out as often as one with
none. A row that leaves settles after the commit, before any place is
read again, so its exit still runs on its own box, in view, at no layout
of its own. The glide its old, translated place gives it lands at once.

* fix(List): a leaving swiped row keeps its move in the list

The module reads a swiped row's old place with its translation on it,
so its glide adds that translation to the row's move in the list. A row
that leaves now keeps only the vertical part of that glide, so a row
removed by an update that also moves it glides with its neighbours
instead of jumping. The swipe-away probe and its comment say what they
check.

* fix(List): a leaving row keeps its place and its glide's record

A second change while a swiped row exits restarted it sideways, because
the List rewrote the glide's frames while the module's record of the
glide kept the swipe's offset. The module's landLayoutGlideSideways now
changes both together. A reorder while a row exits carried that row to
the top of the list, because React moves only its own rows. Each
leaving row now stays before the row that came after it, or last.

* fix(List): a leaving row keeps its old index among the staying rows

A removed row still exiting travelled with the row that came after it
as it left, when that row moved. Each row that leaves, and each row
still leaving, now goes after as many staying rows as came before it,
so it stays where it showed whichever rows move around it. One pass
over the rows replaces the put-back anchor and the later correction.

* Merge main into the List updates branch (#10280 landed)

List.spec.md's verified_by keeps main's ListDividers spec beside this
branch's two List update tests.

* fix(List): a crossing row is marked after the module reads the layout

markCrossings set the travelling mark between the list's row reads and
the module's batched read, so a reorder with a crossing row restyled
and laid out the page again in its commit. It now sets the marks in
the microtask it already queues, after the module's read and before
the frame paints, and marks only a row that has a glide. A mark's end
clears only its own mark, so a second reorder while a row rides keeps
the new mark until the new glide ends.

* fix(List): a travelling row keeps its surface in a list with dividers

The press layer's overDividers, composed after the travelling style,
also sets box-shadow, so a travelling row in a list with dividers and
pressable rows lost its surface, and its label drew over the rows it
crossed. The travelling style now also sets the surface as a private
custom property, and overDividers keeps it in each of its shadows.
Every other row resets the property, so a nested row never inherits it.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>

This branch was successfully deployed

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

Labels

ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant