Skip to content

chore(core): UIKit's paging values and math, measured on iOS 26.2 - #8145

Merged
cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c23-paging-values
Nov 2, 2026
Merged

cixzhang merged 1 commit into
meta:mainfrom
vjeux:uikit/c23-paging-values

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

User need

Paging under a finger should match iOS: the same page choice at the lift, the same settle. This adds UIKit's paging values and math as pure functions for the pager.

Problem and solution fit

  • motion/uikit/paging.ts: UIScrollView paging on iOS 26.2, each value with its measurement:
    • the settle: per 1 ms step v = (v + (target − x) · 0.000284858) · 0.970815; x += v, whole steps per frame;
    • the thresholds the pager's page rule reads: 300 pt/s, half a page, and at most one page from the drag's start page;
    • the settle starts at the release velocity every gesture reads (the shared tracker, spec:AST-100, spec(AST-100): one velocity tracker and one release filter for every gesture #8312), zeroed below 225 pt/s, a guess inside the measured (200, 249] bracket (@measure M-PG13);
    • the 85 ms catch, setContentOffset(_:animated:)'s 0.3 s sinusoid, the rubber band's dimension, the pan's axis ratio.
  • motion/paging.ts: pagingSettleVelocity (the floor), solvePaging (the integrator as a curve the animator plays) and solveAnimatedPageScroll; the pager (chore(core): a pager that pages like a UIKit paging scroll view #8146) picks the page with these thresholds on the release policy, resolveSnapPoint.
  • The spec owners' amendment rides with these values: FR3 and FR6 are narrowed for paging (DEC-25: a threshold, not a projection; a release under 225 pt/s starts at rest), FR4 records the paging motion as an implementation gap, and Lightbox's gallery follows the same rule. Main's unused projection rule (motion/selectPage.ts, scroll.paging) is deleted, so one paging rule remains.

Current specification

spec:AST-094 FR1–FR3: measured, sourced UIKit values, kept as internal constants (no theme role). This PR changes two records with the spec owners' text:

  • spec:AST-094: FR3 and FR6 appends, FR4's implementation-gap sentence, DEC-25 (FR37 unchanged), the FR3 and FR6 pager rows, the FR36–FR40 row's paging note, and the shared pager's trackpad line (Lightbox's viewer under every pointer, Carousel where the primary pointer is coarse);
  • component:Lightbox FR7: a gallery release lands by FR3's paging rule (DEC-25).

They need a spec owner's /approve-spec <head sha>.

Public delta and caller burden

No behavior change: the new math is internal and unused so far, and the deleted selectPage had no caller. The changeset is [chore].

Design and alternatives

  • The integrator, not a spring: it fits UIKit's frames within 0.13–0.22 pt RMS; integrated continuously rather than per millisecond, it misses by about 1 pt RMS.
  • Not WebKit's snap: it counts from the release position, not the start page.

Evidence

No UI change: values and math only. Recorded from UIKit on the iOS 26.2 simulator, frame by frame: releases at rest, an overscroll return, flicks and animated scrolls, plus the advance speed bracketed at 298.6–310.7 pt/s.

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

  • New in paging.test.ts: solvePaging and solveAnimatedPageScroll replay UIKit's recorded frames (settles within 0.35 pt RMS, animated scrolls within 0.5); pagingSettleVelocity zeroes a release below the floor and keeps a steady 249 pt/s one; the animated scroll lasts 0.3 s whatever the distance. uikit.test.ts checks every new entry's source and its measurement date, and that M-PG12 and M-PG13 are listed in MEASUREMENTS.md. selectPage.test.ts goes with selectPage, and values.test.ts drops the retired scroll.paging.snap entry.
  • Local gates at 070b242: check:knowledge; core tsc 0; jsdom motion, hooks, gestures, Lightbox and Carousel 2,137 passed; eslint with no warnings on the changed sources; Prettier on the changed files; check:motion-roles; the code is the pre-reviewed e4bb202's, and this head changes only the record text
  • No .github files: git diff --stat 5a455f5fdd55a07dac50ac661b6f53714839c0a5...HEAD -- .github is empty against main at 5a455f5.
  • Size: 13 files, 767 lines added, 112 removed (the deleted projection rule is 2 files, about 90 lines).
  • Records checked: spec:AST-094 FR1–FR4, FR6, FR36–FR40, FR43 and DEC-25 (new here); component:Lightbox FR7 and FR11; spec:AST-100 FR2, FR3 and DEC-1 (one release filter, thresholds per component); architecture:knowledge-contracts INV29 (comments: no record ids, dates or quotes; short sentences; a header of about 12 lines); and the global records pnpm specs:for --diff lists for these paths. No public API change. The owner-gated paths are docs/specs/AST-094-touch-presentations/spec.md and docs/specs/components/Lightbox/Lightbox.spec.md (change-scope.cjs --classify-paths); pnpm check:knowledge passes.

Writing rubric results

Run over the record text this PR adds to spec:AST-094 and component:Lightbox (the spec owners' wording, pasted as given):

  • R1 narration: none. FR3, FR4 and FR6 state what the pager does, and Lightbox FR7 where a gallery release lands; DEC-25 cites the owner's preview verdict as its basis.
  • R2 research: none in the records.
  • R3 evidence: the FR3 and FR6 rows name tests and states, not changes; FR7's basis cites FR3 and DEC-25.
  • R4 rejected alternatives: one Rejected: line in DEC-25.
  • R5 change references: none.
  • R6 open questions: none added.

Stack: this PR → #8146 → #8153 (#8676 and #8360 have merged), one commit each, no merges.

@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 2, 2026 10:20pm UTC

Request Review

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review 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
🟢 Reviewed (for maintainers only) Nov 2, 2026, 10:26 PM UTC

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

Approval waits on an engineering-owner decision: against main this pull request adds the draft spec:AST-094 (FR1–FR23, DEC-1, DEC-2), the only authority for the paging math and the motion stack under it, so an engineering owner needs to approve, narrow or reject it by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner). That decision has to settle whether FR3, which says a release picks its rest point by projecting its velocity, covers the measured 300 pt/s paging threshold, and the proposed amendments of theme-tokens INV3, user-states DR1, the buttons family's press feedback and AST-025 FR14. Separately from that decision, these changes are needed:

  • Land the predecessor pull requests first: against main this one carries 13,835 authored lines, while its own commit is 1,053.
  • Join the new pr-interaction CI job to an existing owner (AST-030 FR11 and FR7); today a failing interaction proof fails no required check.
  • Under Reduce Motion, a move that starts during a fade leaves the element at its mid-fade opacity, or at 0 with reducedMotion: 'instant' (packages/core/src/motion/animator.ts lines 1346 and 1578).
  • paging.release.velocityFloor (250 pt/s) zeroes the 249 pt/s settle velocity its own measurement says UIKit keeps (packages/core/src/motion/uikit/paging.ts line 121), and handle.started reports the call time after a second call from rest takes over (animator.ts line 1075).
  • Add ## Writing rubric results for the new record to the description.

[Automated review]

* `scrollViewWillEndDragging` velocity reads 0 for steady releases at
* 175 and 200 pt/s and keeps 0.249 pt/ms (2026-10-23)
*/
velocityFloor: 250,

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 floor zeroes a 249 pt/s settle velocity, but the @source above says the scrollViewWillEndDragging velocity "keeps 0.249 pt/ms". Put the floor inside the measured bracket (above 200, at most 249 pt/s), tagged GUESS with a measurement task until it is pinned, and update the 249 case in paging.test.ts line 309.

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 f4e9c59: velocityFloor is now 225 pt/s, inside the measured (200, 249] bracket, and the 249 pt/s case keeps its velocity (paging.test.ts, which failed before).

Comment thread .github/workflows/ci.yml
# them, so a change to an instrument is proven by its negative controls
# before a component spec relies on it. Chromium only on these runners;
# see the harness README for running WebKit.
pr-interaction:

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.

No join needs pr-interaction (test at line 887, build at line 1264), so a failing interaction proof fails no required check. AST-030 FR11 asks new coverage inside an existing surface to join that surface's owners without a separately routed lane, and FR7 that an owned lane's failure fails its join: list it in the test join and in ci-test-join.mjs, or admit the lane through an owner-approved FR10 amendment.

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.

Not this PR's code: the pr-interaction job comes from the interaction-harness foundation below this commit in the stack, and is routed to its owners.

Comment thread packages/core/src/motion/animator.ts Outdated
): Partial<Record<MotionProperty, Segment | null>> => {
const next: Partial<Record<MotionProperty, Segment | null>> = {};
const opacityNow = valueAt('opacity', time);
const opacityTo = targets.opacity ?? opacityNow;

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.

When the call names no opacity, this ends the Reduce Motion dip at the opacity on screen rather than where a running fade is going. A fade from 0 to 1 interrupted at 0.3577 by animate({x: 120}) leaves the element at 0.3577, or at 0 with 'instant' because animate deletes the segment before its valueAt fallback (lines 1577–1578). Ending the dip at finalValue('opacity') and leaving opacity alone in 'instant' mode fixes both.

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.

Not this PR's code: the animator belongs to the motion foundation below this commit in the stack, and the Reduce Motion fade case is routed to its owner.

@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

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

  • Release velocity. core/src/motion/paging.ts:42 estimates the finger's speed at lift with a local estimator. A shared velocity module is being specified so every gesture reads one velocity; please don't add another estimator here.

This is about where the behavior lives, not about the PR's intent.

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

Approval waits on an engineering-owner decision: against main this pull request adds the draft spec:AST-094 (FR1–FR23, DEC-1, DEC-2), the only authority for the paging math and the motion stack under it, so an engineering owner needs to approve, narrow or reject it by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner). That decision has to settle whether FR3, which says a release picks its rest point by projecting its velocity, covers the measured 300 pt/s paging threshold, and the proposed amendments of theme-tokens INV3, user-states DR1, the buttons family's press feedback and AST-025 FR14. The settle-velocity floor is fixed; separately from the owner decision, these changes are needed:

  • Conform the Reduce Motion substitution (FR8, the 500 ms timing.reducedMotionCrossFadeMs default, presentation-spring fades, throws that keep moving) to design:reduced-motion DR2, current on main, which allows only an opacity change of 200 ms or less, also after a touch release, or propose that record's amendment in FR8.
  • Land the predecessor pull requests first: against main this one carries 13,837 authored lines, while its own commit is 1,055.
  • Join the pr-interaction CI job to an existing owner (AST-030 FR11 and FR7), and fix the Reduce Motion fade that leaves an element at its mid-fade opacity (animator.ts lines 1346 and 1578) and the handle.started takeover (animator.ts line 1075).
  • Add ## Writing rubric results for the new record, and tag velocityFloor as an estimate with a measurement task.

[Automated review]

Comment on lines +107 to +111
place, and any other move fades through. A navigation push or pop
cross-fades over 0.5 s on the ease-in-out curve; a sheet, a full-screen
modal or an alert fades on its own presentation spring and drops its travel
and scale; menus open and close on UIKit's Reduce Motion spring. Motion that
follows a finger, such as a throw, MUST keep its motion. **Proposed; pending

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.

design:reduced-motion, current on main since this stack was written, requires every such transition on touch to change opacity only and finish in 200 ms or less (DR2), including one a touch release starts, which "cross-fades as DR2 says, because a finger started it". The 0.5 s navigation cross-fade, the presentation-spring fades and the throw that keeps moving contradict it, and the note below names only the draft design:motion DR5, which now defers to that record. Conform FR8 to it, or propose its amendment here for its owner to rule on.

* Cross-Fade Transitions on (opacity CABasicAnimation 0.5 s,
* (0.42, 0, 0.58, 1)), Oct 23 2026
*/
reducedMotionCrossFadeMs: 500,

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.

Under current design:reduced-motion DR2 a substituted cross-fade on touch must finish in 200 ms or less. As the animator's default fade (animator.ts lines 1366–1393) this 500 ms value, like the presentation-spring fades (404–588 ms) and the travel that 'keep' and decay keep after a release, would put every touch presentation built on the animator over that limit. Cap the substituted fade at 200 ms, or get the record amended, before a component adopts it.

* `scrollViewWillEndDragging` velocity reads 0 for steady releases at
* 175 and 200 pt/s and keeps 0.249 pt/ms (2026-10-23)
*/
velocityFloor: 225,

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.

225 sits inside the measured bracket and the 249 pt/s case now keeps its velocity. The comment calls it "the bracket's middle until a finer sweep pins it", but the only tag is @source measured:, so uikit.test.ts counts it as measured and no MEASUREMENTS.md row asks for the sweep. Per the folder's README, add @source GUESS — the middle of the measured (200, 249] bracket and an @measure task listed in MEASUREMENTS.md.

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 e7e2c50: velocityFloor is tagged @source GUESS — the middle of the measured (200, 249] bracket with @measure M-PG13, and M-PG13 is listed in MEASUREMENTS.md (uikit.test.ts checks both).

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation.

@vjeux
vjeux force-pushed the uikit/c23-paging-values branch from f4e9c59 to e7e2c50 Compare October 24, 2026 23:35
@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Pushed in e7e2c50: pagingReleaseVelocity is gone; pagingSettleVelocity keeps only the floor, and the pager (#8146, 974edc7) reads the pan's velocity from the shared tracker. UIKit's recorded hold-then-lift release stays in the pager's tests as an expected failure until #8312's release filter lands.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 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.

Approval waits on an engineering-owner decision: current spec:AST-094 says a release picks its rest point by projecting its velocity (FR3), hands its velocity to the motion that follows while a new target keeps the current velocity (FR6), and plays touch motion through motion roles (FR4, with spec:AST-108 FR3), while this paging records a measured 300 pt/s page threshold, starts the settle at rest below 225 pt/s, drops the velocity in the animated page scroll and keeps its motion out of the roles. An engineering owner can record that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner) and then amend AST-094 or add the pager's own record to match, or say which part must change. Separately, these changes are needed:

  • Land the predecessor pull requests (#8360, #8076, #8446) and rebase, or rebase the 8 paging files onto main alone: against main this diff carries 27,972 authored lines, while its own change is 742.
  • Undo the three edits that still assume scroll.paging is gone: the M-PG1 row in MEASUREMENTS.md, the uikit/scroll.ts row in the motion README, and the scroll.paging.snap entry in values.test.ts.

[Automated review]

@vjeux
vjeux force-pushed the uikit/c23-paging-values branch from 7df00ca to fc47044 Compare November 1, 2026 09:47
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 1, 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.

Approval waits on an engineering-owner decision: the measured paging here departs from current spec:AST-094 FR3 (a speed threshold instead of a projected rest point), FR6 (the settle and the animated page scroll drop the release velocity) and FR4 (paging motion outside the motion roles, with no pager record), and the branch also carries new FR11 and FR19 text and an INV13 sentence from #8360. An engineering owner can record that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), then amend AST-094 or add the pager's record, or say which part must change. Separately, these changes are needed:

  • Land #8360 and #8676 and rebase, so the diff is the 8-file paging change rather than 20,147 authored lines against main.
  • Have useFingerScroll call useAttachment instead of copying its attach and release Effects.
  • Move the page-rule and FR37 test claims in the description to #8146, where that code and those tests live.
  • Restore scroll.paging in the M-PG1 row, the motion README and values.test.ts, fix the usePager header, and make the INV13 sentence match the document touchmove and wheel listeners.

[Automated review]

Comment on lines +48 to +76
const attachedRef = useRef<{node: Node; detach: () => void} | null>(null);
// Follows the node on every commit: a ref that now points at another
// element moves the watch to it.
useIsomorphicLayoutEffect(() => {
const node = isDocument(scroller) ? scroller : scroller.current;
if (attachedRef.current?.node === node) {
return;
}
attachedRef.current?.detach();
attachedRef.current =
node == null
? null
: {
node,
detach: watchFingerScrolls(
getGestureHost(isDocument(node) ? node : node.ownerDocument),
node,
),
};
});
// Synchronizes with the gesture host's finger-scroll watch: unmounting
// releases the watch this hook attached.
useIsomorphicLayoutEffect(
() => () => {
attachedRef.current?.detach();
attachedRef.current = null;
},
[],
);

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 two Effects repeat useAttachment (gestures/useAttachment.ts lines 68–98): attach after each commit, release a replaced node, release at unmount. Let useAttachment take a Document target (or a node getter) and hand back the attached node, then call it here once, so a fix to node replacement or StrictMode replay reaches every gesture hook.

small glue in its own build over these modules. The gesture host attaches for
every pointer, with no touch-only query; under a fine pointer only a component that
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The modules are not public exports and are not
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The gesture host, press feedback, page hold, finger-scroll watch and click guard hear document-wide input through one capture-phase listener per event type on the window, present while a subscriber needs it. They run in subscription order, before any document or element listener; it is passive unless a subscriber cancels the event. The modules are not public exports and are not

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 gesture host also registers a non-passive document touchmove while a claiming element moves (GestureHost.ts lines 1633–1638) and a non-passive document-capture wheel while a wheel gesture is latched (lines 2468–2476), outside the window stream. Either route both through subscribeInput or name them in this sentence.

Comment thread packages/core/src/motion/paging.ts Outdated
* and `solveAnimatedPageScroll` (`setContentOffset(_:animated:)`). The page
* a release settles on is the pager's own rule, on the motion foundation's
* release policy (resolveSnapPoint.ts)
* @position Pure math; no DOM, no React. Read by hooks/usePager.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.

hooks/usePager.ts does not exist at this head; say the pager in #8146 reads this.

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

Approval waits on an engineering-owner decision: the measured paging here departs from current spec:AST-094 FR3 (a 300 pt/s threshold rather than a projected rest point), FR6 (the settle under 225 pt/s and the animated page scroll start from rest) and FR4 (no motion role and no pager record), and the branch carries new FR11, FR19 and INV13 text from #8360. An engineering owner (the author included) can record that decision by commenting /approve-spec followed by the full head SHA, then amend AST-094 or add the pager's record, or say which part must change. Independently of that ruling, please:

  • Land #8360 and #8676 and rebase, so this diff is the 8-file paging change instead of 20,230 lines.
  • Have useFingerScroll call useAttachment instead of repeating its Effects, and walk the flat tree (parentAcrossShadow) in presentedOffsetOf.
  • Fix the INV13 sentence, which omits the gesture host's document touchmove and wheel listeners, move the two FR37 test claims to #8146's description, and restore the scroll.paging measurement row, the motion README row and the scroll.paging.snap spring check.

[Automated review]

for (
let node: Node | null = element;
node != null;
node = node instanceof ShadowRoot ? node.host : node.parentNode

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 walks parentNode and ShadowRoot.host, while the gesture host matches movers along the flat tree (containsAlongFlatTree in utils/flatTree.ts, which follows assignedSlot). For slotted content inside a moving shadow-tree element, presentedTarget retargets the touch but presentedRect adds no offset for that mover, so a press tracker judges the finger against a box the content has left. Walking parentAcrossShadow here keeps one flat-tree walk; a slotted case in presentedHitTesting.test.ts would pin 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.

Approval waits on an engineering-owner decision: the measured paging here departs from current spec:AST-094 FR3 (a 300 pt/s threshold rather than a projected rest point), FR6 (the settle under 225 pt/s and the animated page scroll start from rest) and FR4 (no motion role and no pager record), and the branch carries new FR11 and FR19 text and an INV13 sentence from #8360. An engineering owner can record the decision by commenting /approve-spec followed by the full head SHA, then amend AST-094 or add the pager's record to match; the author may do so as an engineering owner. Independently of that ruling, please address these before merge:

  • Land #8360 and #8676 and rebase, so the diff against main is this pull request's 8-file change.
  • Have useFingerScroll call useAttachment instead of repeating its two Effects.
  • Move the two page-rule tests named under Testing, and the FR37 sentence, to #8146's description.
  • Make the INV13 sentence match the gesture host's document touchmove and wheel listeners, and fix the scroll.paging leftovers, the usePager pointer and the new verification row.

[Automated review]

| FR42 | Default-theme geometry tests under a coarse and a fine pointer, and hit-testing between adjacent targets | every component at phone width with a finger and with a mouse; a dense toolbar; a chip and its remove button; a Switch beside a label link | a default-theme box that grows under touch or at phone width other than with its text, or, where no component or module record sets the hit area, a touch target under 44 px where neighbors allow it or a hit area that overlaps a neighbor's target, fails |
| FR43 | Browser tests with a mouse, a trackpad, a pen, a finger, and the keyboard, and resource counts | a pan by drag and by wheel; a slider's thumb under a wheel and a trackpad scroll; a ctrl-wheel pinch; a long press, a right click, and Shift+F10; a mouse press; a handle drag; a row swipe with a mouse and a trackpad; a component with no gesture | a gesture missing under a mouse or pen, a mouse-drag swipe action, a pen pinch, an edge pan under a mouse, a trackpad, or a pen, a press delay under a fine pointer, a wheel or trackpad scroll that changes a thumb's value or does not reach the scroll under it, an idle listener in a component with no gesture, or added stylesheet bytes, fails |
| FR43 trackpad | Unit tests on timed wheel streams; WebKit browser tests with trackpad streams (Chromium's driver wheels read as a mouse wheel) | a form and a required sheet pulled 30 to 240 px by a finger and by a trackpad; mouse lines, notches, ticks and accelerated deltas over a sheet, a pager and a row; ten swipes each starting mid-momentum, ten whose momentum ends, and a slow drag with 150 ms pauses; a trackpad flick and a slow stopped drag on an info sheet; a fling, a slow 60 px swipe, a 42 px spike and back-to-back swipes on a pager; a row opened and swiped back within one gesture, and a long fast swipe; a pinch ending | a trackpad stretch that differs from a finger's for the same travel (120 px gives 54 px on the reference sheet), a mouse wheel that moves a sheet, a pager or a row, splitter counts other than ten, ten and one, momentum that moves a surface, a flick released with no velocity, a pager that travels past one page or pages on a spike, an info sheet a flick leaves open or a slow stopped drag closes, an open row that reveals its other side within one gesture, or a pinch that ends after 100 ms fails |
| FR11, FR19 iOS | Presented tap/drag proof: CI WebKit (`pr-interaction-webkit`) and the iOS Simulator | owner phone check of the preview on 10/31 | evidence gap: no recorded physical-iPhone take yet (#9310). |

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 row puts a dated owner check in the states column and an evidence gap with an issue number in the failure column, so it states no failure condition and keeps history as standing content (docs/contributing/spec-writing.md R3, R5). Name the scenarios and the failure (for example, a tap on a sliding sheet that activates what sat under the motion's start fails), and keep the missing physical-iPhone take in the pull request.

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

Approval waits on an engineering-owner decision: the measured paging here departs from current spec:AST-094 FR3 (a 300 pt/s threshold rather than a projected rest point), FR6 (the settle under 225 pt/s and the animated page scroll start from rest) and FR4 (no motion role and no pager record), and the branch carries new FR11 and FR19 text and an INV13 sentence from #8360. An engineering owner can record the decision by commenting /approve-spec followed by the full head SHA, then amend AST-094 or add the pager's record to match; the author may do so as an engineering owner. Independently of that ruling, please address these before merge:

  • Land #8360 and #8676 and rebase, so the diff against main is this pull request's 8-file change.
  • Have useFingerScroll call useAttachment, and have the action-group tracker arm the shared click guard, instead of keeping their own copies.
  • Move the two page-rule tests named under Testing, and the FR37 sentence, to #8146's description.
  • Make the INV13 sentence match the listeners kept outside the window stream, and fix the scroll.paging leftovers and the usePager pointer.

[Automated review]

Comment on lines +48 to +76
const attachedRef = useRef<{node: Node; detach: () => void} | null>(null);
// Follows the node on every commit: a ref that now points at another
// element moves the watch to it.
useIsomorphicLayoutEffect(() => {
const node = isDocument(scroller) ? scroller : scroller.current;
if (attachedRef.current?.node === node) {
return;
}
attachedRef.current?.detach();
attachedRef.current =
node == null
? null
: {
node,
detach: watchFingerScrolls(
getGestureHost(isDocument(node) ? node : node.ownerDocument),
node,
),
};
});
// Synchronizes with the gesture host's finger-scroll watch: unmounting
// releases the watch this hook attached.
useIsomorphicLayoutEffect(
() => () => {
attachedRef.current?.detach();
attachedRef.current = null;
},
[],
);

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 two Effects repeat useAttachment (gestures/useAttachment.ts lines 80–98): attach after each commit, release a replaced node, release at unmount. A fix to node replacement or StrictMode replay in one will miss the other. Let useAttachment take a Document target (or a node getter) and hand back the attached node, then call it here and delete these Effects.

* @output Exports armClickGuard (and its ClickGuardOptions),
* disarmClickGuard, isClickGuardArmed, withoutClickGuard and
* CLICK_GUARD_WINDOW_MS
* @position The one guard for the browser's click after a gesture that

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.

hooks/actionGroupTracker.ts swallowNextClick (lines 180–197) still keeps its own window-capture click, pointerdown and keydown swallower for the same click, with different rules (no 400 ms bound, a key disarms it, untrusted clicks pass). Have it arm this guard (with a disarm-on-key option if needed), or say here why action groups keep their own.

small glue in its own build over these modules. The gesture host attaches for
every pointer, with no touch-only query; under a fine pointer only a component that
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The modules are not public exports and are not
uses a gesture listens (`spec:AST-094` FR19, FR24, FR43). The gesture host, press feedback, page hold, finger-scroll watch and click guard hear document-wide input through one capture-phase listener per event type on the window, present while a subscriber needs it. They run in subscription order, before any document or element listener; it is passive unless a subscriber cancels the event. The modules are not public exports and are not

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 gesture host also adds a non-passive document touchmove while a claiming element moves (GestureHost.ts lines 1623–1625) and a non-passive document-capture wheel while a wheel gesture is latched (lines 2461–2464), and press feedback keeps its window keydown on the bubble phase (pressFeedback.ts line 709). Route the two host listeners through subscribeInput, or name all three as exceptions here.

Comment thread packages/core/src/motion/paging.ts Outdated
* and `solveAnimatedPageScroll` (`setContentOffset(_:animated:)`). The page
* a release settles on is the pager's own rule, on the motion foundation's
* release policy (resolveSnapPoint.ts)
* @position Pure math; no DOM, no React. Read by hooks/usePager.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.

hooks/usePager.ts does not exist at this head; say the pager in #8146 reads this.

| M-S3 | Bounce-back spring | Same list: drag 120 pt past the top, hold still, release; a flick into the top edge at 3000 pt/s | contentOffset(t) after release and after edge contact; spring response and damping ratio; velocity hand-off | C | `scroll.bounce` |
| M-S5 | Rubber band | Same list on an 852 pt view: drag 100, 300, 600 pt past the top; a horizontal pager | displayed overflow against finger overflow; confirm c = 0.55 and d = the view's size on the axis | C | `scroll.rubberBand` |
| M-PG1 | Paging | Photos full-screen swipes; a probe paging scroll view | page chosen per release position × velocity (thresholds; at most one page?); contentOffset(t) onto the page | B, C | `scroll.paging` |
| M-PG1 | Paging | Photos full-screen swipes; a probe paging scroll view | page chosen per release position × velocity (thresholds; at most one page?); contentOffset(t) onto the page | B, C | `paging` (measured; see M-PG12) |

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.

scroll.paging.projectionRate and scroll.paging.snap still cite M-PG1 (uikit/scroll.ts lines 91 and 97), and selectPage.ts reads the first; keep scroll.paging in this row.

@vjeux
vjeux force-pushed the uikit/c23-paging-values branch from 5018b58 to 347debf Compare November 2, 2026 20:18
@github-actions github-actions Bot removed the needs:design-review Affects visuals — Design should review label Nov 2, 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.

Approval waits on an engineering-owner decision: this pull request writes new paging text into the current spec:AST-094 (an FR3 speed threshold instead of a projection, an FR6 settle that starts from rest under 225 pt/s, an FR4 implementation gap and DEC-25), and no owner has approved it at this head. An engineering owner can record the decision by commenting /approve-spec followed by the full head SHA, or say which part must change; the author may do so as an engineering owner, and the ruling should also say whether FR43's tenth-of-a-page spike guard and FR36–FR38 still apply to paging. Independently of that ruling, please address these before merge:

  • Land #8676 and rebase, so the diff against main is this change's 8 files.
  • Have the wheel pre-filter ask the pan's own direction rule instead of panTakesMove, which already swallows the first wheels of a diagonal trackpad scroll on a pan with axisRatio: 2.
  • Reword the changeset, since only the settle and setContentOffset(animated:) are replayed against recorded frames.
  • Retire scroll.paging.snap now that the settle is measured, and record Carousel's impact and gap (or leave Carousel out of the FR3 paragraph).

[Automated review]

}

/** Whether a pan's `direction` takes a contact moving by (cx, cy). */
function panTakesMove(

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 is a second copy of the pan's direction rule (PanGestureRecognizer.classify), without its axisRatio. For a horizontal pan with axisRatio: 2, a 35° trackpad scroll has its first two wheels prevented while nothing moves, then the pan fails and the rest of the stream scrolls the page. Could the host ask the pan's own classification through the recognizer internals instead, and drop this function?

Comment thread .changeset/uikit-paging-values.md Outdated
'@astryxdesign/core': patch
---

[chore] Internal: UIKit's paging values and math for touch presentations. The thresholds that pick the page a release settles on, the settle that carries it there and `setContentOffset(animated:)` are measured on iOS 26.2 and replayed against the recorded frames to under half a pixel. Not exported from the package entry point.

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.

Only the settle and setContentOffset(animated:) are replayed against recorded frames here; nothing in this change reads advanceSpeed, commitFraction or maxPagesFromStart. Suggested: "The settle and setContentOffset(animated:) replay UIKit's recorded frames to under half a pixel; the page thresholds are recorded values the pager reads."

measures (0.55 for a scroll view and a sheet), and a release MUST pick its
rest point by projecting its velocity the way its UIKit counterpart does:
with a deceleration rate, or with a measured projection time.
A paging scroll view (the pager, and Carousel with `hasSnap` under a

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.

Carousel with hasSnap pages with the browser's scroll-snap-type: x mandatory today, and the Current-state impact line for the shared pager names only a trackpad swipe. Please add the finger page rule and an implementation gap there, or leave Carousel out of this paragraph until the pager replaces its scroll-snap.

| M-S3 | Bounce-back spring | Same list: drag 120 pt past the top, hold still, release; a flick into the top edge at 3000 pt/s | contentOffset(t) after release and after edge contact; spring response and damping ratio; velocity hand-off | C | `scroll.bounce` |
| M-S5 | Rubber band | Same list on an 852 pt view: drag 100, 300, 600 pt past the top; a horizontal pager | displayed overflow against finger overflow; confirm c = 0.55 and d = the view's size on the axis | C | `scroll.rubberBand` |
| M-PG1 | Paging | Photos full-screen swipes; a probe paging scroll view | page chosen per release position × velocity (thresholds; at most one page?); contentOffset(t) onto the page | B, C | `scroll.paging` |
| M-PG1 | Paging | Photos full-screen swipes; a probe paging scroll view | page chosen per release position × velocity (thresholds; at most one page?); contentOffset(t) onto the page | B, C | `scroll.paging`, `paging` |

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.

M-PG1 now feeds both scroll.paging and paging, so the guessed scroll.paging.snap spring stays beside the measured settle for the same use. Consider retiring scroll.paging.snap (and its values.test.ts entry) in this change.

@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main: 13 files.

@cixzhang

cixzhang commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

/approve-spec e4bb202

@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: an engineering owner has approved this head's paging text, but two sentences in the records now contradict text they leave unchanged. Please fix both; the fix commit needs a fresh /approve-spec.

  • Name FR36 and FR38 in DEC-25's narrowing, or add a paging sentence to each. FR36 still decides by projected travel and FR38 still starts the settle at the release velocity, which contradicts the new FR3 and FR6 paging paragraphs that FR36–FR38 are said to govern.
  • Limit "where the primary pointer is coarse" in the shared-pager impact line to Carousel. component:Lightbox FR11 and DEC-2 keep trackpad paging in Lightbox's viewer under a fine pointer.

[Automated review]

of a page, not by a projection, and starts a slow release's settle from rest.
The owner tried this paging on the pager and the paging Carousel on
2026-11-01, with a trackpad and on a phone: "trackpad is good", "phone good".
This narrows FR3 and FR6 for paging only; every other release keeps them. A

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.

DEC-25 narrows only FR3 and FR6, but FR36 (a decision by projected travel) and FR38 ("The commit or the spring-back MUST start at the release velocity") stay as written, and the FR36–FR40 row still fails "a settle that does not start at the release velocity". If FR36–FR38 apply to paging, they contradict the new FR3 paragraph (a 310.7 pt/s release at 10 % advances while a 298.6 pt/s one at 40 % stays, so no projection fits) and the FR6 paragraph (a release under 225 pt/s starts from rest). Suggest naming FR36 and FR38 here: a paging release decides by FR3's paging rule, and its settle starts as FR6's paging paragraph says.

moves it. Implementation gap: a finger drags it one for one below rest.
- The shared pager (Carousel's paging presentation, Lightbox's viewer): a
trackpad swipe follows FR43's pager rule.
trackpad swipe follows FR43's pager rule where the primary pointer is coarse.

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 now limits Lightbox's viewer too, but component:Lightbox FR11 says "A mouse, a pen, and a trackpad also use FR7's drag paging" and DEC-2 gives fine pointers the gestures FR43 gives every pointer. Suggest scoping the limit to Carousel, for example: "a trackpad swipe follows FR43's pager rule in Lightbox's viewer under every pointer (component:Lightbox FR11), and in Carousel's paging presentation where the primary pointer is coarse."

UIKit's paging values for touch presentations: the thresholds that pick the page a release settles on, the settle that carries it there and setContentOffset(animated:), measured on iOS 26.2 and replayed against the recorded frames to under half a pixel. Not exported from the package entry point.
@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 070b242, with the spec owners' wording:

  • DEC-25 now says "A paging release decides by FR3's paging rule, and its settle starts as FR6's paging paragraph says; FR37 applies unchanged." The FR36–FR40 row's failure cell notes that a paging release starts its settle per FR6's paging paragraph (DEC-25).
  • The impact line now reads: "a trackpad swipe follows FR43's pager rule in Lightbox's viewer under every pointer (component:Lightbox FR11), and in Carousel's paging presentation where the primary pointer is coarse."

@cixzhang

cixzhang commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 070b242

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

Recommend approval. An engineering owner approved the new paging text in spec:AST-094 and component:Lightbox at this head, and both record fixes from the earlier review landed: DEC-25 now says how a paging release picks its page and starts its settle, and the shared-pager impact line keeps trackpad paging in Lightbox's viewer under every pointer. The paging math replays the recorded UIKit settles and page scrolls to under half a pixel, and nothing reaches users until the pager plays it. Nothing else needs to change here.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — 070b242a Deployed Nov 2, 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