Skip to content

fix(Carousel): under snap paging, scroll calls move through the paging - #10384

Merged
cixzhang merged 8 commits into
meta:mainfrom
vjeux:uikit/c08-carousel-snap-buttons
Nov 7, 2026
Merged

cixzhang merged 8 commits into
meta:mainfrom
vjeux:uikit/c08-carousel-snap-buttons

Conversation

@vjeux

@vjeux vjeux commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

User impact

Under snap paging on iOS, a touch during an edge button's move leaves the row between items: 1–48 px off a page in 5 of 6 trials. UIKit settles a caught setContentOffset(animated:) on a page.

Problem and solution fit

At rest, the edge buttons and handleRef's scroll calls ran a native smooth scroll, which a still touch in Safari stops between stops. Moved by the paging, the press is the pager's own move: a touch catches it as a settle (spec:AST-094 FR16), and the release rests on a page.

Expected behavior and authority

component:Carousel FR7: an edge button steps about one view, landing on the nearest stop. FR8, as the owners approved, names Carousel's "moves of the row from Carousel's own scroll calls", and takes their line: "Under snap paging, the edge buttons, the edge fades and the handle's canScrollPrev()/canScrollNext() follow the held row, one state."

Smallest restoration

  • moveSnapPaging takes the row over from rest, as a flick does, and plays a held move's goTo. Carousel's scrollBy and scrollToIndex call it; a wheel stays native.
  • The pager catches it as a settle about 85 ms after the touch; a follow-up stops it at touch-down, as UIKit does.
  • The move keeps the pager's setContentOffset(animated:) curve (spec:AST-094 FR45's gap).
  • The press reads the row once, before its first write (architecture:react-component-runtime INV18), so its task lays out nothing. Each item's animator owns its layer.
  • scrollTo(index) targets the item's snap stop inside the scroll padding, so the hand-back pulls nothing back.

Evidence

Safari, iOS Simulator, 4x slower. Top: the base. Bottom: this change.

  • Reproduction before the change: Safari, iOS 26.2 Simulator, a press, then a still 400 ms touch 60, 120 or 200 ms after its lift, twice each: rests 1, 37, 13.3, 5, 48 and 0.7 px off a stop.
  • After the change: 6 of 6 rest on a stop (within 0.03 px).
  • After a press, the start button comes back within two frames of the row leaving the start, while held: one frame in Chromium, WebKit and Safari (iOS Simulator, 3 of 3).

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.

Writing rubric results

component:Carousel FR8, FR1's and FR8's verification rows, verified_by: R1, R2, R4–R6 none; R3: each new row names its spec. Out of scope keeps the fades' look uncontracted; their state follows FR8.

Testing

  • Rebased onto main 143a5de, after fix(Carousel): under snap paging, the edge buttons follow the held row #10359 and test(ci): the WebKit routing test expects both Carousel interaction specs #10469 merged. The four earlier commits carry the same diff as before; the press proof, the records commits and the routing line follow.
  • The WebKit routing test (.github/scripts/ci-interaction-webkit.test.mjs) expects Carousel's folder specs; this PR adds the buttons spec to its list. Without the line it fails on this branch (three specs found, two expected); with it, 23/23.
  • Red-first against Storybooks built from main 72a3558 (before the rebase) and from this change, three-up snap paging story in paging mode:
    • "after a press on the end button, the start button comes back within two frames of the held row leaving the start": on main the button comes back while the row is not held, 3/3 (main's press is a native smooth scroll). With this change it passes 15/15 in Chromium (×5 with the file's flick cases) and 9/9 in WebKit (CI-profile container, --cpus=4, LP_NUM_THREADS=4).
    • "a scroll-right press at rest moves the row through the paging, onto the next page": on main the row is never held. With this change it passes, ×3 with the buttons spec in Chromium.
  • "a press reads the row before its first write, so its own task lays out nothing": a Chromium trace of the click's own task. Layouts: main 0, the previous head 18, this change 0. WebKit skips it: its rendering traces are Chromium-only.
  • SnapPaging.test.tsx: scrollNext, scrollPrev and scrollTo(index) reach the paging's move and never the row's native scrollTo or scrollBy. Both cases fail on main. "moves a scroll to an item onto its snap stop, inside the scroll padding": the previous head targets 200 px (the border edge), this change 184 px. The Carousel, SnapPaging and scroll-overflow suites pass 83/83.
  • The four Carousel and SnapPaging interaction specs at this head (on main 143a5de): Chromium 33 passed, 5 skipped; WebKit (CI-profile container) 37 passed, 1 skipped.
  • Safari on the iOS 26.2 Simulator: real HID touches timed in process, a per-frame page sampler, builds alternated (main, change, change, main, main, change). A press on "Scroll right": the start button comes back one frame after the row leaves the start in all 6 takes; with this change the paging holds the row at that frame and hands it back 13–16 frames later, on main the press is a native smooth scroll. The clip was recorded at the previous head, whose code commits are unchanged here; every frame watched, it shows no markers.
  • Core UI suite (pnpm test:ui) at this head: 910 files, 22,252 tests pass. Core typecheck: 0 errors. pnpm check:knowledge passes. CI=true pnpm lint passes (0 errors). Prettier is clean on the changed files.

Review notes

@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 7, 2026 7:45am 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 7, 2026, 7:58 AM UTC

@github-actions github-actions Bot added 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 · 307b5b3

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
Carousel · Storybook 67.0 kB (+51 B) · 10.8 kB CSS (0) 605 (+6) ✓ · ✓ · ✓
TabList 41.6 kB (+7 B) · 6.4 kB CSS (0) — — · — · —

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

API changes: none in this pull request

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

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

CI impact: 0 job-min per update · pool 0% · time to green −1.5 min · standard runners · 1 CI action or script file changed (not measured)
Before After Change
Job-minutes per PR update 112.4 112.4 0
Runner pool, runner-minutes a day 58,103 58,103 0 (0%)
Average concurrent jobs 40.3 40.3 0
Time to green (median → this run) 17.1 min 15.6 min −1.5 min
Runs a day 517 517 0

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

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 2c2c426

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

@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 needed, plus one spec decision.

The FR8 rewrite is approved for this head, but one behavior still needs an engineering owner's decision before approval: under snap paging the edge fades now follow the held row, and component:Carousel leaves edge fades uncontracted, so an owner should confirm it and name the clause that records it.

Blocking:

  • Each edge-button press now interleaves layout reads with style writes during the takeover (SnapPaging.ts:204-209 and :339, then pagingStep in Carousel.tsx), which runs 6 layouts in one click task in Chromium against 0 on main; please do every read before the takeover's first write (architecture:react-component-runtime INV18).
  • With padding, scrollTo(index) targets the item's border edge rather than its snap stop (Carousel.tsx:590-595), so the row overshoots by the scroll padding and jumps back at the hand-back (16 px at padding={4}); please target the nearest stop instead.

The red-first unit tests and the edge buttons following the held row look good.

[Automated review]

Comment thread packages/core/src/Carousel/Carousel.tsx Resolved
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 105b193

@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 needed, plus one spec decision.

The FR8 rewrite is approved for this head, and both earlier blocking findings are fixed: a press now does no layout in its click task, and a padded scrollTo(index) lands on its stop. One behavior still needs an engineering owner's decision before approval: under snap paging the edge fades now follow the held row, and component:Carousel lists edge fades as uncontracted. An engineering owner should accept or reject that and name the clause that records it; a record change is approved by commenting /approve-spec <full head SHA> on its head, which the author may do as an engineering owner.

Blocking:

  • Miss: every edge-button press and scrollNext, scrollPrev or scrollTo call now leaves will-change: transform on the items the takeover layered (SnapPaging.ts:229-231), because handBack removes it (:246) before the animator's demotion restores it as the element's own value, so six items keep their own compositing layer after one press and all nine after three (main does this only after a gesture the pager takes over, such as a touch flick); please leave the promotion to the animator, or restore the row's own value after the demotion, and assert in the press test that no item keeps will-change after the settle.

[Automated review]

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

An engineering owner approved the FR8 rewrite for this head, which also settles the edge fades following the held row, and the earlier layout and scrollTo(index) findings stay fixed. One defect remains before approval.

Blocking:

  • Miss: every edge-button press and scrollNext, scrollPrev or scrollTo call under snap paging leaves will-change: transform on the items the takeover layered (SnapPaging.ts:229-231), because handBack removes it (:246) before the animator's demotion writes it back as the element's own value, so six items keep their own compositing layer after one press and all nine after three; please leave the promotion to the animator and assert in the press test that no item keeps will-change after the settle.

[Automated review]

@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: the pull request is a draft, and since its branch is in a fork, a maintainer adds the ci:run label once it is ready so CI runs.

The FR8 rewrite carries an engineering owner's approval for this spec content, and the defect left by the last review is fixed: no item keeps will-change after a press or a flick settles. A press still does no layout in its click task, and edge-button rests match main in every snap-paging story checked.

Landing this also lands the two commits of #10359, which this branch contains.

[Automated review]

@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec abe44ef

@vjeux
vjeux force-pushed the uikit/c08-carousel-snap-buttons branch from abe44ef to 3cb24f4 Compare November 7, 2026 06:22
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 7, 2026
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 3cb24f4

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

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 7be7158

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

@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: the pull request is a draft, and since its branch is in a fork, a maintainer adds the ci:run label once it is ready so CI runs.

The Carousel record changes (FR8's rewrite and its held-row sentence, the edge-fades line, and the FR1/FR8 verification rows) have an engineering owner's /approve-spec for this head. Under snap paging, edge-button presses and scrollNext, scrollPrev and scrollTo now move through the paging and rest where they rested before, the press lays out nothing in its own task, and no item keeps a layer once the row rests. The new unit cases and the press browser tests fail on main and pass here; the iOS catch itself rests on your simulator trials, which Linux browsers cannot reproduce.

No blocking findings.

[Automated review]

vjeux added 3 commits November 6, 2026 22:58
At rest, Carousel's edge buttons and its handle's scroll calls scrolled
the row with a native smooth scroll. A still touch on iOS stopped that
scroll between two items, and nothing put the row back on a page. Under
snap paging, the paging now takes the row over from rest, as a flick
does, and plays the move on its own animator. A touch then catches the
move as it catches a settle, and the release rests on a page.
Carousel's scroll calls now move the row through the snap paging value
from rest too, not only a row the value holds. FR8 names those moves.
Under snap paging an edge button press read layout between its own
style writes: 18 forced layouts in one click task in Chromium, 0 on
main. Every read now comes before the takeover's first write: the
target, the stops and the view width. The pager learns its page while
the row is at rest, a child already in place gets no write, and a held
row's edges come from where it is shown. Each child's animator is made
at attach, so its stylesheet read runs in a later task. The press now
lays out nothing in its own task, as on main.

scrollTo(index) targeted the item's border edge. With scroll padding
the row overshot and the hand-back's snap pulled it back 16 px. It now
targets the item's snap stop.
vjeux added 5 commits November 6, 2026 22:58
The takeover wrote will-change: transform on the items near the view.
Each item's animator recorded that as the item's own value, the
hand-back removed it, and the animator's demotion wrote it back. So
after an edge-button press, or a scroll call, the items kept a layer of
their own. Each child's animator already gives it a layer while it
moves and takes it away after the motion, so the takeover writes no
will-change of its own now.
Under snap paging a press on the end button moves the row through the
paging. The edges spec now proves that the start button comes back
within two frames of the row leaving the start, while the paging still
holds the row.
FR8 takes the spec owners' line: under snap paging, the edge buttons,
the edge fades and the handle's canScrollPrev()/canScrollNext() follow
the held row, one state. FR1's and FR8's verification rows name the
real-browser proofs, and verified_by lists both snap paging specs.
Out of scope keeps the fades' look uncontracted and points their state
to FR8, as the spec owners asked.
A change to Carousel's code runs its folder's interaction specs, and
this pull request adds the snap paging buttons spec there, so the
routing test's expected list names it too.
@vjeux
vjeux force-pushed the uikit/c08-carousel-snap-buttons branch from 7be7158 to 307b5b3 Compare November 7, 2026 07:37
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 307b5b3

@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: the pull request is a draft, and since its branch is in a fork, a maintainer adds the ci:run label once it is ready so CI runs.

The Carousel record changes (FR8's moves of the row from Carousel's own scroll calls and its held-row sentence, the edge-fades boundary line, and the FR1 and FR8 verification rows) carry an engineering owner's spec approval for this head, and the code matches them. Under snap paging, edge-button presses and handleRef scroll calls now move through the pager, rest where main rests, leave no layer behind once the row rests, and do no layout in the press's own task. The new unit and browser cases fail on main and pass here, and the WebKit routing test now expects the new buttons spec.

No blocking findings.

[Automated review]

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 7, 2026
@vjeux
vjeux marked this pull request as ready for review November 7, 2026 09:02
@cixzhang
cixzhang merged commit 78df0c2 into meta:main Nov 7, 2026
122 checks passed
@vjeux
vjeux deleted the uikit/c08-carousel-snap-buttons branch November 7, 2026 10:50

This branch was successfully deployed

1 active deployment
Preview — 307b5b32 Deployed Nov 7, 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 A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) 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.

2 participants