Skip to content

feat(List): animate row inserts, removals and moves through the animatedLayout handle - #8106

Draft
vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c11-list-updates
Draft

vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c11-list-updates

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #8106 → #8139 → #8143 → #8163

Stacked on #10261 until it lands; review only this PR's change: vjeux/astryx@8982806...b79fde9

Batch: a behavior module, 2,429 authored lines: tests 1,473 (unit 661, browser 812), product code 767, stories 152, docs and records 37.

User need

On iOS, when a table's rows change, the others slide to their new places, so you see what changed. A List's rows jump in one frame.

Problem and solution fit

With an animatedLayout handle and an itemKey on each row (component:List FR12), an inserted row enters and a removed row collapses in place, inert and hidden from assistive technology. A row crossing others rides above them. The rows after a change and the content after the list glide, on one frame of the shared motion, for every pointer. Without an itemKey or a handle, rows change at once. The first keyed row of a list that had none enters too.

Current specification

component:List FR12; spec:AST-105 FR1–FR5; spec:AST-103 DEC-4. List's readiness row and component:ListItem now list the row updates and itemKey as shipped; the start and end content motion is not yet.

Public delta and caller burden

<List animatedLayout={useAnimatedLayout({containerRef})}> with <ListItem itemKey={id}>, both optional.

Design and alternatives

  • The shared module (chosen), not a motion per list, where content after the list jumped.
  • The list reports the rows in view and its last row, so the module's walk stays near the view.
  • A swiped row's translation moves onto its children, so no element is added.

Evidence

iOS 26.2 simulator, touch, at an earlier head (0c59447):

Removal Insert Reorder
Removal Insert Reorder
Swipe away Reduce Motion
Swipe away Reduce Motion

A mouse in Chromium, without the handle (main) and with it:

Before After
Before After

Size (gzip, main → head): List +9,791 B JS and +143 B CSS; ListItem +254 B JS and +81 B CSS. Components on List add 8.4–9.7 KB JS, the shared animated layout module TreeList already ships.

Writing rubric results

R1 narration: none. R2 research: none. R3 evidence: both verified_by lists add their new test files. R4 rejected alternatives: none. R5 change references: none. R6 open questions: none.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

  1. Open Storybook's List "Animated changes" story and tap "Remove the second row": it conceals in place while the rows below, the caption and the buttons glide up.
  2. Tap "Insert a row second": the new row enters while the rows below glide down, together.
  3. Tap "Move the last row first": it glides to the top above the rows it crosses.
  4. In "Animated changes swipe", swipe Sent fully left: it exits with its action showing, and the rows below glide up.
  5. With a mouse, the same motion. With Reduce Motion, a phone cross-fades, and a mouse changes at once.

What was run at this head: test:ui (885 files, 21,802 tests) with no unhandled errors, including four List cases that fail without this head's fixes. They cover the first keyed row of a list with none (it played no entrance), a row moved from first to last (it slid beneath the rows it crossed), and a row inserted under StrictMode (React's replay of its detach left it at opacity 0). A fifth removes a row under StrictMode: it keeps what it showed, and fails if the detach keeps nothing. A sixth re-renders while rows glide, with a new ref on each row: no row is left with its motion written on it. A seventh changes a list with two swiped rows apart: it reads every row's place before it settles either swipe. Also: List's browser specs in Chromium on a 2-core pin (45 passed) and in the CI WebKit profile (34 passed, its Chromium-only cases skip); the core type check, the Storybook build, review:pre and check:repo. At an earlier head of this change: strict lint (0 errors), both builds, Storybook's type check, test:node (914 files), the accessibility audit on both new stories (no new violations), Item's swipe accessibility contracts (8 passed), and the new spec at 3 CPUs and 16 workers in WebKit and ×10 in the CI WebKit profile (90 passed).

@vercel

vercel Bot commented Oct 24, 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 1:37pm UTC

Request Review

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

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🔄 In progress (for maintainers only) Nov 6, 2026, 1:49 PM UTC

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

This pull request needs an engineering owner's decision before it can be approved: no current spec governs List's new hasAnimatedChanges and followingContent props, and the draft AST-094 FR8 (a move fades through under Reduce Motion) conflicts with the code keeping the slide (REDUCED_MOTION = 'keep'). An engineering owner can provide or identify the governing List spec, settle FR8 against 'keep', and record the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. Independently of that decision, these need changes:

  • The diff carries the whole stack (17,607 authored lines); once the predecessor pull requests land it should contain only this pull request's two commits (998 lines).
  • With followingContent="moves", content below the list that layout did not move (for example a footer held by margin-top: auto) is shifted by the list's height change and slides back (contentBelow in rowMotion.ts).
  • Row positions are recorded only on List renders, so a layout change between renders (a rotation, a row growing on its own) replays as motion at the next render.
  • With hasAnimatedChanges off, every List still subscribes to (pointer: coarse) and reads every row's layout on each render.

[Automated review]

Comment thread packages/core/src/utils/rowMotion.ts Outdated
Comment on lines +314 to +321
// Below, not beside: a flex or grid neighbour does not move when the
// component changes height.
const top =
next.getBoundingClientRect().top - presentation(next, 'y', motionNow());
if (top < bottom - 1) {
continue;
}
below.push(next);

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.

This keeps any in-flow element whose top is below the list, but never checks that layout moved it. When an ancestor absorbs the list's height change (a footer held at the bottom by margin-top: auto, the block after a grid row kept open by a taller column, a fixed-height or overflow: hidden parent), useListUpdateMotion still shifts it by -grew and springs it back, so content that did not move jumps and slides. Recording each candidate's position at every commit, as the rows' are, and springing it by its own measured shift would avoid that.

Comment on lines +90 to +94
const before = placesRef.current;
placesRef.current = places;
const height = element.offsetHeight;
const grew = height - heightRef.current;
heightRef.current = height;

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.

The baseline is only refreshed here, after a List render. A rotation that rewraps rows, a row that grows from its own state, or a late font moves rows without rendering List, and the next unrelated render replays that change as motion for the rows and the content below. Refreshing the places and height when layout changes without a commit (for example from a resize observation held only while hasAnimatedChanges is on) would stop the replay.

enabled: boolean,
followingContent: 'moves' | 'stays' = 'moves',
): void {
const animates = useMotionPresentation() === 'uikit' && enabled;

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.

List calls this hook for every instance, so with hasAnimatedChanges off each List still subscribes to (pointer: coarse) here and, below, reads every row's offsetParent/offsetTop and its own offsetHeight on every render before the enabled check. Returning before the measurements when the prop is off (clearing placesRef), and reading the pointer inside the effect instead of subscribing, keeps the opt-in free for lists that do not use it.

@github-actions

github-actions Bot commented Oct 24, 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
Contributor

Choose a reason for hiding this comment

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

This pull request needs an engineering owner's decision before it can be approved: no current spec governs List's new hasAnimatedChanges and followingContent props, and the draft AST-094 FR8 (a move fades through under Reduce Motion) conflicts with the code keeping the slide (REDUCED_MOTION = 'keep'). An engineering owner can provide or identify the governing List spec, settle FR8 against 'keep', and record the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. Independently of that decision, these need changes:

  • The diff carries the whole stack (18,305 authored lines); once the predecessors land, or after retargeting and rebasing onto main, it should contain only this pull request's two commits (1,191 lines).
  • With followingContent="moves", content below the list that layout did not move (for example a footer held by margin-top: auto) is shifted by the list's height change and slides back.
  • The same transient translate re-anchors position: fixed elements inside that content, so a fixed action bar jumps up the screen for about half a second.
  • Row positions are recorded only on List renders, so a layout change between renders replays as motion at the next render.
  • With hasAnimatedChanges off, every List still subscribes to (pointer: coarse) and reads every row's layout on each render.

[Automated review]

Comment thread packages/core/src/utils/rowMotion.ts Outdated
Comment on lines +292 to +299
if (
style.position === 'absolute' ||
style.position === 'fixed' ||
style.position === 'sticky' ||
style.display === 'none'
) {
continue;
}

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.

contentBelow checks only the candidate's own position, but the translate it then writes makes the candidate the containing block of any position: fixed element inside it. In a fixture with an action bar fixed 16px above the bottom of a 720px viewport, inside a section after the list, removing a row moved the bar from 660px to 344px from the top, and it stayed re-anchored until releaseMotion cleared the translate about 500 ms later. Skipping candidates that contain fixed-position descendants, or defaulting followingContent to 'stays', would avoid it.

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

This pull request needs an engineering owner's decision before it can be approved: no current spec governs List's new hasAnimatedChanges and followingContent props, and the draft AST-094 FR8 (a move fades through under Reduce Motion) conflicts with the code keeping the slide (REDUCED_MOTION = 'keep'). An engineering owner can provide or identify the governing List spec, settle FR8 against 'keep', and record the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner. Independently of that decision, these need changes:

  • The diff carries the whole stack (18,321 authored lines); once the predecessors land, or after retargeting onto the stack's previous branch, it should contain only this pull request's two commits (1,191 lines).
  • With followingContent="moves", content below the list that layout did not move (for example a footer held by margin-top: auto) is shifted by the list's height change and slides back.
  • The same transient translate re-anchors position: fixed elements inside that content, so a fixed action bar jumps up the screen until the motion ends.
  • Row positions are recorded only on List renders, so a layout change between renders replays as motion at the next render.
  • With hasAnimatedChanges off, every List still subscribes to (pointer: coarse) and reads every row's layout on each render.

[Automated review]

@vjeux

vjeux commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. This pull request merges after #8061, #8076, #8446, #8360, #8890, #8077, #8085 and #8088, in that order. The compare link at the top of the description shows only its 12 files, and the size line is base against head for this change.

On the size of the List change itself (2,067 lines):

  • 1,203 lines are its tests: 755 in the browser spec and 448 in the unit tests. They ship with the feature, as the template's scope list asks.
  • 153 lines are the two Storybook stories that the browser spec and the evidence run against.
  • The implementation is about 700 lines: listLayoutUpdates.ts and its wiring in List, ListItem and their docs.

docs/contributing/pull-requests.md asks to split a separable delta, and I don't see one left. Removals, inserts, moves, the swiped row's exit and the crossing raise are all part of component:List FR12's one behaviour, and FR12's start- and end-content part is already in #8803. The largest candidate to split, the swiped row's exit, would leave this pull request putting a swiped row back at rest for its exit instead of as it was on screen.

@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. The List change follows component:List FR12, the latest push's private doc entry for the touch band's divider width is correct, and your breakdown of the List change's 2,067 lines answers the earlier request to trim it, so no trim is needed once it stands alone. What remains is the stack: please land #8061, #8076, #8446, #8360, #8890, #8077, #8085 and #8088 first, so this diff is the List change alone (2,067 of its 35,031 authored lines) and its size line reports base against head for every component it changes. This branch also carries #8360 at a commit 18 commits behind that pull request, so merging it now would ship its superseded code.

[Automated review]

@vjeux

vjeux commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. This pull request now carries #8360's current head, 045f666, and still merges after #8061, #8076, #8446, #8360, #8890, #8077, #8085 and #8088. The compare link at the top of the description shows only its own change.

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

This needs an engineering owner's decision before it can be approved: this head rewords component:TreeList FR8, which is still proposed, so that the rows after a collapsing group hold until its rows have slid under the parent. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or FR8 can be settled in #8088 first. The List change itself follows component:List FR12; what remains after the decision is the stack: please land #8061, #8076, #8446, #8360, #8890, #8077, #8085 and #8088 first, so this diff is the List change alone (2,067 of its 35,786 authored lines) and its size line reports base against head for every component it changes.

[Automated review]

| FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged |
| FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `<li>` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent |
| FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged |
| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only (`spec:AST-094` FR4–FR5); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2): as an expanding group appears and, under a coarse primary pointer, as a collapsing group's rows finish sliding under the parent, until which they hold their places. | UIKit outline list (`UICollectionView`), iOS 26.2, whose rows after a closing group hold until its rows have slid under the parent (0.24–0.37 s measured); `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval |

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.

This rewords FR8, which is still "Proposed; awaiting an engineering owner's approval" on main: without a handle, the rows after a collapsing group now hold until its rows have slid under the parent, and the verification row at L279 fails rows that move earlier. It needs an engineering owner's decision (an exact-head /approve-spec), or it can be settled in #8088 first. It also leaves open where focus goes when a collapsing row holds it.

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

This needs an engineering owner's decision before it can be approved: this head carries #8088's rewording of component:TreeList FR8, still proposed on main, so that under a finger the rows after a collapsing group hold until its rows have slid under the parent. An engineering owner records that decision by commenting /approve-spec 2aa3b3b332491c3a07850657b2bfd6e9d064d5e6 on this pull request (the author may do so as an engineering owner), or FR8 is settled through #8088 first. The List change itself follows component:List FR12; after that decision, two changes remain:

  • Land the eight predecessor pull requests first, so this diff is the 2,067-line List change rather than 35,919 authored lines.
  • Report the base-against-head size of every component the diff changes, or let the existing size line cover it once the predecessors land.

[Automated review]

| FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged |
| FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `<li>` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent |
| FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged |
| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only (`spec:AST-094` FR4–FR5); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2): as an expanding group appears and, under a coarse primary pointer, as a collapsing group's rows finish sliding under the parent, until which they hold their places. | UIKit outline list (`UICollectionView`), iOS 26.2, whose rows after a closing group hold until its rows have slid under the parent (0.24–0.37 s measured); `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval |

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.

This rewords FR8, still "Proposed; awaiting an engineering owner's approval" on main: without a handle, the rows after a collapsing group now hold until its rows have slid under the parent, and the verification row (L279) fails rows that move earlier. An engineering owner records the decision by commenting /approve-spec with this head's full SHA on the pull request (the author may, as an engineering owner), or FR8 is settled through #8088 first.

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

This needs an engineering owner's decision before it can be approved: this head carries #8088's rewording of component:TreeList FR8, still proposed on main, so that under a finger the rows after a collapsing group hold until its rows have slid under the parent. An engineering owner records that decision by commenting /approve-spec 8134a0f642fbb6979aefb81bf356a21d065714cb on this pull request (the author may do so as an engineering owner), or FR8 is settled through #8088 first. The List change itself follows component:List FR12; after that decision, two changes remain:

  • Land the eight predecessor pull requests first, at their current heads (#8360 has moved past the commit this branch carries), so this diff is the 2,067-line List change rather than 36,093 authored lines.
  • Report the base-against-head size of every component the diff changes, or let the existing size line cover it once the predecessors land.

[Automated review]

| FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged |
| FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `<li>` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent |
| FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged |
| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only (`spec:AST-094` FR4–FR5); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2): as an expanding group appears and, under a coarse primary pointer, as a collapsing group's rows finish sliding under the parent, until which they hold their places. | UIKit outline list (`UICollectionView`), iOS 26.2, whose rows after a closing group hold until its rows have slid under the parent (0.24–0.37 s measured); `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval |

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.

This rewords FR8, still "Proposed; awaiting an engineering owner's approval" on main: without a handle, the rows after a collapsing group now hold under a coarse primary pointer until its rows have slid under the parent, and the verification row (L279) fails rows that move earlier. An engineering owner approves or rejects this wording by commenting /approve-spec with this head's full SHA on the pull request, or it is settled through #8088 first.

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

This needs an engineering owner's decision before it can be approved: this head carries #8088's rewording of component:TreeList FR8, still proposed on main, so that under a finger the rows after a collapsing group hold until its rows have slid under the parent. An engineering owner records that decision by commenting /approve-spec 0535acf7004d0cbc538e836f2344030f1557d6bc on this pull request (the author may do so as an engineering owner), or FR8 is settled through #8088 first. The List change itself follows component:List FR12, and the latest push's animator and press-release fixes come with tests that fail before them; after that decision, two changes remain:

  • Land the eight predecessor pull requests first, so this diff is the 2,067-line List change rather than 36,239 authored lines.
  • Report the base-against-head size of every component the diff changes, or let the existing size line cover it once the predecessors land.

[Automated review]

| FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged |
| FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `<li>` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent |
| FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged |
| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only (`spec:AST-094` FR4–FR5); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2): as an expanding group appears and, under a coarse primary pointer, as a collapsing group's rows finish sliding under the parent, until which they hold their places. | UIKit outline list (`UICollectionView`), iOS 26.2, whose rows after a closing group hold until its rows have slid under the parent (0.24–0.37 s measured); `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval |

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.

This rewords FR8, still "Proposed; awaiting an engineering owner's approval" on main: without a handle, the rows after a collapsing group now hold under a coarse primary pointer until its rows have slid under the parent, and the verification row (L279) fails rows that move earlier. It also ties the hold to the slide, while under Reduce Motion those rows fade instead and the code holds the rows after the group until the fade ends. An engineering owner can approve this wording with /approve-spec and this head's full SHA, or FR8 can be settled through #8088 first.

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

This needs an engineering owner's decision before it can be approved: this head carries #8088's rewording of component:TreeList FR8, still proposed on main, so that under a finger the rows after a collapsing group hold until its rows have slid under the parent. An engineering owner records that decision by commenting /approve-spec 0c59447f97aade3afceb1291c6ed40f0b2a0cc9a on this pull request (the author may do so as an engineering owner), or FR8 is settled through #8088 first. The List change itself follows component:List FR12, and the latest push's animator and press-overlay fixes come with tests that fail before them; after that decision, two changes remain:

  • Land the eight predecessor pull requests first, so this diff is the 2,067-line List change rather than 36,338 authored lines.
  • Report the base-against-head size of every component the diff changes, or let the existing size line cover it once the predecessors land.

[Automated review]

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 5787619

@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. With a handle, the List change covers removals, inserts and moves under component:List FR12, but three things need fixing first:

  • Miss: adding the first row to a list with no keyed rows (an empty list, or one with only unkeyed rows) plays no entrance and the content after the list jumps, because measure returns early at packages/core/src/List/listLayoutUpdates.ts L372-374, while FR12 asks an inserted row to enter and the content after it to glide.
  • Regression: docs/specs/components/List/ListItem.spec.md still says itemKey is specified but not shipped (L39) and lists it as a known gap (L76-78), so update that record in this pull request now that ListItem accepts itemKey.
  • spec:AST-105 FR5 asks each adopting pull request to show mouse behavior before and after, and the description shows touch recordings only, so please add mouse recordings of the Animated changes story without and with the handle.

Two optional comment cleanups are noted inline. CI has not run on this head yet.

[Automated review]

Comment on lines +372 to +374
if (rows.size === 0) {
return;
}

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.

Miss: with no keyed rows this returns before anything is measured, so the first row added to an empty list (or the first keyed row among unkeyed ones) plays no entrance, and the content after the list jumps. That breaks component:List FR12. Report a target anyway (the list's last child, or the list itself when it is empty) and keep an empty measurement so the layout effect plays the entrance. A 0-to-1 case in listLayoutUpdates.test.tsx would cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 09cd1f2. With no keyed rows, the list now reports its last child, or itself when empty, so the first keyed row enters and the content after the list glides. Two new unit tests fail without the fix.

* `animatedLayout`. A row without one changes at once. It is separate from
* React's `key`, which stays React's own.
*/
itemKey?: string | number;

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.

Regression: docs/specs/components/List/ListItem.spec.md still says itemKey is "specified but not shipped" (L39) and lists "ListItem has no itemKey yet" as a known gap (L76-78). Please update that record here: list the prop as shipped, drop the gap and add the new tests to verified_by.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated in 09cd1f2: itemKey is in the public contract, the gap is gone, and verified_by lists listLayoutUpdates.test.tsx.

Comment on lines +18 to +43
// How it runs, step by step, through the one size-change module.
// - Before a commit that changes the rows, LayoutChangeSnapshot calls
// `measure`, before React changes the DOM. It reports each row in view,
// since any of them may leave and the module exits only what it measured.
// It also reports the last row, so the content after the list glides when
// no row is in view. The module's walk takes the siblings on both sides of
// each reported row, up to its band around the visible part. So the rows
// near the view and the content after the list glide from their old
// places. A long list's rows far from the view are never read. Rows sit in
// order down the list, so halving finds the rows in view. When only one row
// is in view and the last row is far from it, no reported row's walk
// reaches the last row. So an insert above the last row moves it at once.
// - After the commit, rows are matched by their `itemKey`. A row that left
// comes back where it was and exits in place. It gives up its space, is
// inert and hidden from assistive technology, and conceals upward
// (`holdLayoutExit`), then it goes. A row that appeared enters
// (`playLayoutChange`). The rows that stay glide. All of it plays on the
// shared motion role and starts in the same frame.
// - A row that slides up across rows moving down rides above them for its
// glide. The list's surface is behind it, as an opaque UIKit cell passes
// over its neighbors.
// - It runs for every pointer. Reduce Motion follows the module's policy.
// - Without a handle nothing is measured, and the list changes at once. A row
// without an `itemKey` has no identity to match. Its own insertion, removal
// or move changes at once, though it glides like any content when a keyed
// row's change moves it.

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.

Non-blocking (comment style): this block narrates the code below it. Keep only the constraints, for example: "The module exits only rows it measured, so every row in view is reported. The last row is reported too, so content after the list glides when no row is in view."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taken in 09cd1f2.

Comment thread packages/core/src/List/ListItem.tsx Outdated
Comment on lines +545 to +546
// The row's identity for the list's animated layout
// (List/listLayoutUpdates.ts).

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.

Non-blocking (comment style): this restates itemKeyProps(itemKey); it can go.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in 09cd1f2.

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 502832a

@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: this head fixes the three earlier items (the first keyed row, the ListItem record and the mouse evidence), but two defects in the row updates remain.

  • Miss: under React <StrictMode> in development, ListItem's unmount Effect (packages/core/src/List/ListItem.tsx L419-436) replays while an inserted row's entrance plays, and keepPresentation commits the entrance's first frame inline, so the row stays at opacity: 0 after the entrance ends, against architecture:react-component-runtime INV3's rule that an Effect is safe under StrictMode replay.
  • Miss: markCrossings (packages/core/src/List/listLayoutUpdates.ts L326-331) raises only rows gliding up, so a row moved from first to last slides under the four rows it crosses and is hidden for most of its move, while component:List FR12 says a row crossing rows moving the other way rides above them.

Please add a StrictMode insert case and a "move the first row last" case with the fixes. One optional comment-style suggestion is inline. CI has not run on this head yet, and the fix commit will need a fresh /approve-spec for the spec-owner check.

[Automated review]

Comment thread packages/core/src/List/ListItem.tsx Outdated
// present inline while it is still in the document. A parent's layout
// cleanup runs before React removes the row, so the removal's collapse
// shows the row as it was on screen.
useIsomorphicLayoutEffect(() => {

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.

Miss: under <StrictMode> (development), React replays this Effect for a newly inserted row right after the commit, while its entrance plays. keepPresentation then commits the entrance's first frame inline (opacity: 0, clip-path: inset(0 0 100%)), and the row stays invisible after the entrance ends. Record what the animations show without writing it to the row (for example saveAnimations from motion/carryAnimations.ts, applied in putBack), or keep the presentation only of rows the current commit measured, and add a StrictMode insert case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6e95066. The cleanup now keeps the presentation only of a row its list reported for the commit in progress, the only rows that can exit. A StrictMode replay on a new row runs after the list's layout effect clears that set, so it writes nothing. The new StrictMode insert case fails without the fix.

}
if (to > from) {
lowestDown = Math.max(lowestDown, to);
} else if (lowestDown > to) {

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.

Miss: only a row gliding up is raised. Moving the first row last raises the four rows moving up (z-index: 2, opaque surface fill), and the moved row passes beneath them, hidden for most of its move, while component:List FR12 says a row crossing rows moving the other way rides above them. Raising the row with the longest glide among those it crosses covers both directions; a "move the first row last" case would pin it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6e95066. Of two rows that cross, the longer glide rides above (on a tie, the row gliding up). One pass each way finds the rows that cross any other, and only those are compared, so a removal still costs one pass. The new "moving the first row last" case fails without the fix.

Comment on lines +18 to +43
// How it runs, step by step, through the one size-change module.
// - Before a commit that changes the rows, LayoutChangeSnapshot calls
// `measure`, before React changes the DOM. It reports each row in view,
// since any of them may leave and the module exits only what it measured.
// It also reports the last row, so the content after the list glides when
// no row is in view. The module's walk takes the siblings on both sides of
// each reported row, up to its band around the visible part. So the rows
// near the view and the content after the list glide from their old
// places. A long list's rows far from the view are never read. Rows sit in
// order down the list, so halving finds the rows in view. When only one row
// is in view and the last row is far from it, no reported row's walk
// reaches the last row. So an insert above the last row moves it at once.
// - After the commit, rows are matched by their `itemKey`. A row that left
// comes back where it was and exits in place. It gives up its space, is
// inert and hidden from assistive technology, and conceals upward
// (`holdLayoutExit`), then it goes. A row that appeared enters
// (`playLayoutChange`). The rows that stay glide. All of it plays on the
// shared motion role and starts in the same frame.
// - A row that slides up across rows moving down rides above them for its
// glide. The list's surface is behind it, as an opaque UIKit cell passes
// over its neighbors.
// - It runs for every pointer. Reduce Motion follows the module's policy.
// - Without a handle nothing is measured, and the list changes at once. A row
// without an `itemKey` has no identity to match. Its own insertion, removal
// or move changes at once, though it glides like any content when a keyed
// row's change moves it.

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.

Non-blocking (comment style): this block still narrates the code below it. Keep only the constraints, for example: "The module exits only rows it measured. A row without an itemKey has no identity, so its own change lands at once."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taken in 6e95066.

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 6e95066

@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 has not run on this head yet, because the pull request is a draft from a fork; mark it ready for review and have a maintainer add the ci:run label so CI runs.

This head fixes both defects raised earlier: an inserted row now stays visible under React <StrictMode>, and a row moved down now rides above the rows it crosses, each with a unit case that fails before the fix. With a handle and an itemKey on each row, inserts, removals and moves animate as component:List FR12 describes, and the new browser spec passes in Chromium.

[Automated review]

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec fab2359

@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 defect in the row updates needs fixing first.

  • With a handle, a ListItem whose ref changes during a commit (for example a callback ref written inline) has its running glide and entrance written inline by keepPresentation (packages/core/src/List/ListItem.tsx L445), so re-rendering the list while rows glide leaves them 21 px off their places and the new row half-faded and clipped, against component:List FR12; keep the presentation only for a row that actually leaves, and add a case where a row's ref changes mid-glide.

Everything else follows component:List FR12, and the new unit and Chromium browser cases pass on this head. CI has not run on this head yet. One optional comment-wording suggestion is inline.

[Automated review]

(onClick != null || href != null || interactiveRef != null) && !isDisabled;
const hitAreaRef = useTouchHitArea(isTouchTarget);
const mergedRef = useMergedRefs(ref, hitAreaRef);
const mergedRef = useMergedRefs(ref, hitAreaRef, keepPresentationOnDetach);

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.

This cleanup also runs when the merged ref changes, not only when the row leaves: useMergedRefs returns a new ref whenever the caller's ref (for example ref={row => …} written inline) or the hit-area ref changes. React runs the old cleanup in the commit's mutation phase, while reportedRows still holds the row, so keepPresentation commits the row's running glide and entrance inline. In Chromium, an insert followed 120 ms later by a status update left Sent, Drafts and Archive at translate: 0px -21px and the new row at opacity: 0.43 for good. Recording what the animations show without writing it (and writing it in putBack), or an unmount-only cleanup as the previous revision had, keeps this to rows that leave.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 08652ac. The detach now only records what the row's animations show: it reads them through commitStyles, then restores each part's style. The exit writes the record, so a row that stays is left untouched. The new case, a re-render mid-glide with a new ref on each row, fails without the fix.

Comment thread packages/core/src/List/ListContext.tsx Outdated
Comment on lines +68 to +70
* The list's `animatedLayout` handle, for its rows. Internal: a row's own
* layout changes, such as its start and end content, report to the same
* handle.

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.

Non-blocking (comment style): nothing reads this field at this head, so the second sentence describes behavior that does not exist yet. "The list's animatedLayout handle, for its rows." is enough until the start and end content motion lands.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taken in 08652ac.

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.

@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 has not run on this head yet, because the pull request is a draft from a fork. Once it is marked ready, a maintainer adds the ci:run label so CI runs.

With an animatedLayout handle and an itemKey on each row, List animates inserts, removals and moves as component:List FR12 describes, and a row whose ref changes while rows glide now ends at its place; the new unit case covers it. The unit tests and the browser spec pass on this head in Chromium.

  • Non-blocking: component:List FR12 does not say where focus goes when a removed row held it, so a keyboard user who removes a row from a control inside it lands on the page body; the record could state it.

[Automated review]

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 08652ac

@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 has not run on this head yet, because the pull request is a draft from a fork. Once it is marked ready, a maintainer adds the ci:run label so CI runs.

With an animatedLayout handle and an itemKey on each row, List animates inserts, removals and moves as component:List FR12 describes. The merge of main moves those glides to the theme's move curve, and the unit tests and the browser spec pass against it in Chromium.

  • Non-blocking: the recordings and test runs the description marks "at this head" predate that merge, when the glide was a spring; a fresh mouse recording would show the motion that now ships.
  • Non-blocking: component:List FR12 does not say where focus goes when a removed row held it, so a keyboard user who removes a row from a control inside it lands on the page body; the record could state it.

[Automated review]

vjeux added 2 commits November 6, 2026 02:42
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.
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec fe1f020

@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: the List row updates follow component:List FR12, and the glide-duration change carried from #10261 has an engineering owner's approval at this head, but two things need fixing first.

  • Newly live check: measure in listLayoutUpdates.ts (L421-429) reads each near row's computed transform and rect after writing the previous rows' settles, which architecture:react-component-runtime INV18 rules out; read every near row's transform and rect in one pass, then write the settles, and take a settled row's top from its rect minus its own translation.
  • The diff is 2,513 authored lines because it carries #10261's separable 84-line glide-duration change; land #10261 first and merge main, so this pull request is the 2,429-line List change, which its decomposition already clears.

CI has not run on this head because the pull request is a draft from a fork; once it is marked ready, a maintainer adds the ci:run label so CI runs.

[Automated review]

Comment on lines +421 to +429
for (let index = near[0]; index <= near[1]; index++) {
const done = settleTranslation(elements[index]);
if (done != null) {
settled.push(done);
}
}
for (let index = near[0]; index <= near[1]; index++) {
tops.set(order[index], elements[index].getBoundingClientRect().top);
}

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.

Newly live check: settleTranslation reads a row's and its children's computed transform and then writes them, one row at a time, so every row after a translated one is read after a write, and the tops loop reads every rect after those writes. Current architecture:react-component-runtime INV18 fails "a read interleaved with writes". Read every near row's transform and rect in one pass first, then write the settles, and take a settled row's top from that rect minus the row's own translation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b79fde9: one pass reads every near row's translation and place, then the settles are written, and a settled row's top is its rect less its own translation. A new unit case fails at fe1f020 ("settle read read read settle") and passes now.

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.
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec b79fde9

@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: the List row updates follow component:List FR12 and the reads between settles are fixed, but two things need fixing first.

  • Miss: measure in listLayoutUpdates.ts still reads layout after writing the settles, in rowsMeeting(elements, 0, height) (L468) and each target's measureBefore (L478-481), so a change with a swiped row near the view lays the page out once more before the commit, which architecture:react-component-runtime INV18 rules out; take the rows in view from the first pass, call measureBefore there, then write the settles.
  • The diff is 2,633 authored lines because it carries #10261's separable 84-line glide-duration change; land #10261 first and merge main, so this pull request is the List change alone.

CI has not run on this head because the pull request is a draft from a fork.

[Automated review]

// The module exits only rows it measured, so every row in view is
// reported. The last row is reported too, so content after the list
// glides when no row is in view.
const shown = rowsMeeting(elements, 0, height);

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.

Miss: this reads every row's rect after the settles above (L461-463) have written row and child transforms, and the measureBefore calls below (L478-481) read after them too. So a change with a swiped row near the view lays the page out once more before the commit: a Chromium trace of a move with two translated rows shows 2 Layouts in the click task instead of 1. Current architecture:react-component-runtime INV18 fails "a read interleaved with writes". Take the rows in view from the first pass's rects (top and bottom less the translation already read), call each target's measureBefore in that pass, then write the settles.

Comment on lines +243 to +244
* translation moves. `readTranslation` reads it, and `settleTranslation`
* moves it, so a list reads every row before it writes any. Returns the

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.

Non-blocking (INV29): this comment still describes moving the translation, which settleTranslation does, and says "a list reads every row before it writes any", which rowsMeeting at L468 contradicts. Suggested: "Reads a row's own translation and its children's transforms. Returns null when the row has no translation of its own."

This branch was successfully deployed

1 active deployment
Preview — b79fde91 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

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants