Skip to content

feat(core): a pan takes wheel and trackpad scrolling, and a pinch a ctrl-wheel (spec:AST-094 FR43) - #8676

Draft
vjeux wants to merge 1755 commits into
meta:mainfrom
vjeux:uikit/f2-wheel
Draft

vjeux wants to merge 1755 commits into
meta:mainfrom
vjeux:uikit/f2-wheel

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8665, with the web input drivers (#8666) merged in for the proofs until they land. Review only this pull request's change: vjeux/astryx@c6c02fc...f9e99b8 (the harness files are #8666's).

Why

On a laptop, a trackpad swipe or a mouse wheel over an Astryx pan, such as a row with swipe actions, never reaches it: the page scrolls instead. The approved gesture spec says "On the web, a pan also takes wheel and trackpad scrolling; a pinch takes a ctrl-wheel" (spec:AST-094 FR43).

What led to it

#8665 made gestures attach for every pointer; this adds the web's scroll input, as iPadOS gives a UIPanGestureRecognizer a trackpad's scroll.

Options and tradeoffs

  • Let each component listen for wheel: each would repeat latching, scroller edges and notch handling.
  • Feed wheel input to the gesture host as a contact, so every pan and pinch gets it through one arena.

Why this choice

One host decides once per wheel gesture: a pan takes it only along its own axis and only after every scroller under the pointer has reached its edge, so a list inside a sheet still scrolls first. A gesture stays latched to the pan that took it, as a browser latches a scroller. A pan opts out with allowedScrollTypes: []; the screen-edge pan and any watcher take none (FR43, FR20). A first wheel the page already prevented, or one that cannot be cancelled, stays the page's. Mouse-wheel notches release with no speed, never stamped after the press that ends them; a trackpad keeps its momentum. A wheel nobody takes is never prevented.

How to check it

  1. With a trackpad, swipe two fingers sideways over a swipeable list row. Expected: the row reveals its actions; the page does not scroll sideways.
  2. Scroll vertically over the same row. Expected: the page scrolls.
  3. Over a sheet with a scrolled list, scroll up. Expected: the list reaches its top first, then the sheet moves.
  4. Give three quick wheel notches over a sheet. Expected: it rests on a detent, never flung closed.
  5. Pinch on a trackpad over a pinchable element. Expected: it scales, the page does not.

Evidence

  • Real wheel input in Chromium and WebKit: a pan follows a scroll along its axis with the page held still, a wheel across it scrolls the page, and a ctrl-wheel pinches.
  • Unit cases for each rule fail before the change, the review's three included: watcher pans, a prevented or uncancelable first wheel, a notch release's time.
  • Still open for the spec's owner: when a wheel gesture ends (here, 100 ms after its last wheel) and how macOS momentum is read.

vjeux and others added 30 commits October 26, 2026 20:13
…ta#8521) into meta#8270

# Conflicts:
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
…g) into meta#8293

# Conflicts:
#	packages/core/src/motion/animator.ts
The note's reword joined its next sentence onto one long line; it now
wraps like the rest of the file. No code change.
cixzhang's meta#8589 keeps the host's touchend passive, and its contextmenu
and click listeners off, until a claimer is attached
(syncClaimingListeners). Ported onto this pull request's input stream with
no change to what it does: install() subscribes the touchend passive, and
syncClaimingListeners swaps it for a cancelling subscription and adds the
contextmenu and click subscriptions while a claimer is attached. A group
press brings its own click subscription only while the host has none, and
the host's takes it over. Her watcher test reads those listeners on the
window, where the input stream registers them; her new group test (a
finger, then a mouse, under a fine pointer) sits before the detach test.
The test file and the bare fixture import both her
ScreenEdgePanGestureRecognizer and this layer's selection lock from utils.
Here the click guard (utils/clickGuard.ts) owns clicks, so cixzhang's
meta#8589 syncClaimingListeners, ported at meta#8356, switches only the touchend's
cancelability and the contextmenu subscription; the group press subscribes
no click of its own. Her meta#8072 5c59af1 (a finger on an action group
under a fine pointer reaches its recognizers) arms the click guard where it
armed the swallower. Her watcher test reads the touchend and the
contextmenu on the window, and no click listener: the guard's comes only
once a gesture acts.
… the lift, under real touch input

spec:AST-094 FR20's "follows the scroll to the lift" had only a jsdom
test with a synthetic takeover; the FR18-FR22 verification row asks for
real touch input. In Chromium with injected touches, a vertical drag on a
list a watcher pan is attached to scrolls the list natively, no touchmove
is cancelled, and the watcher goes from began to ended at the lift.
… at the edge stays the page's

spec:AST-094 FR10: two fingers before a claim are the page's pinch. The
scroll-edge hold (DEC-18) counted any finger, so with two fingers down it
cancelled the pinch's moves before any drag was claimed. It now holds only
while a single finger is down. The new two-finger test fails on the
previous head.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…s watcher browser test) into the arena fixes
…dge pan holds a lone finger only; meta#8589's watcher browser test) into meta#8293
… is down off the sheet (spec:AST-094 FR10)

The lone-finger hold counts only the fingers the pan tracks, so a second
finger on plain page content left the sheet's finger held, and the moves
of a pinch across the sheet's edge were cancelled. This test fails until
the host counts every finger on the screen.
meta#8665's review fixes: an armed press under a fine pointer brings the
claiming listeners, keeps a kept press's finger and goes when the page
hides; a press claimed at its own pointerdown keeps the page's selection;
FR41 says what a claimed drag does with the selection; meta#8360's
input-stream notes. The merge is clean.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pull request is on hold for an engineering owner's decision on two readings: spec:AST-094 FR43 does not say when a wheel gesture ends or what its release carries (here, 100 ms after the last wheel, with a trackpad's speed and macOS momentum kept and a mouse wheel's notches released still), and the carried FR41 rewrite lets a claimed mouse or pen drag take no new text selection and clear the page's, which FR41 as it stands rules out for a mouse. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may record it too, as an engineering owner. Whatever the decision, these changes are needed:

  • Reduce the diff to this change: 28,409 of its 30,450 authored lines are the open #8665 stack (land it with #8666 and #8535 first, or retarget onto #8665), and the astryxStylex fix belongs in its own pull request.
  • Give a notch gesture's release zero velocity explicitly: a press within 40 ms of the last notch now releases it with the notches' speed, against the gestures README.

[Automated review]

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
This pull request now sits on meta#8665, so a stack carrying it and meta#8665's
other dependants (meta#8676) needs no second GestureHost resolution. Two
hunks in GestureHost.ts's pointerdown, both this pull request's:
- the second-finger check where fingers come from pointer events stays,
  followed by meta#8665's rule that a right click, a pen's barrel button or
  a macOS ctrl-click starts no press;
- the check that runs again once the new finger is delivered stays,
  followed by meta#8665's selection lock call, which no longer takes the
  window.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
This pull request now sits on meta#8665, so a stack carrying it and meta#8665's
other dependants (meta#8676) needs no second GestureHost resolution. Three
hunks in GestureHost.ts, resolved in this pull request's own code:
- the element's touchend listener for a claimed lift stays; meta#8665's
  removal of the fine-pointer action group's own pointerdown listener
  stands, in the listener sync and at detach, since meta#8665's host
  attaches for every pointer;
- a finger's target gets a blocking listener only for an enabled
  participant, as here, and for a kept press under any pointer, as in
  meta#8665.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
…the wheel's and the pen lift's

No behavior change. The touchend listener's registration field, its
sync and its removal now come before the touchmove listener's, and the
claimed lift is recorded before the pointer is forgotten, so this pull
request and its siblings on meta#8665 (meta#8676's wheel listener, meta#8670's pen
lift record) no longer edit the same lines of GestureHost.ts, and a
stack that carries them together merges without a resolution.
@vjeux
vjeux marked this pull request as draft October 29, 2026 22:38
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
…k's fixes) into uikit/c21-wheel-fr43

The two harness files that conflicted take the clean merge of meta#8340 and
meta#8676, the heads that carry them.
vjeux added 2 commits October 29, 2026 16:32
…st's clock reads

Reported against a sheet's tests on this pull request's head: three
quick 100 px notches at the content's top released at 6,250 px/s, so the
sheet was thrown away. The host schedules a wheel gesture's end on its
clock, while the arena reads the wheels' own timestamps, which need not
share the clock's base (the sheet's tests fake the page's clock). The
case fails on this head: the release velocity is 6,250, not 0.
…END_MS after its last wheel

The host stamped every wheel gesture's release at the earlier of
WHEEL_END_MS after its last wheel and the host's clock when it ended, so
a release is never later than an input that ends it (FR16). On its own
timer, though, the clock need not share the wheels' time base: where the
page's clock reads behind the wheels' timestamps, the release collapsed
onto the last notch and three quick notches threw a sheet away at 6,250
px/s. A gesture that ends on its own (its timer, a taker leaving, a
detach) is now stamped WHEEL_END_MS after its last wheel, as before that
change; only an input that ends it sooner caps the stamp, with its own
time on the wheels' base (spec:AST-094 FR43).
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
The harness browser spec takes meta#8676's version (35445d0), which already
carries meta#8666 and meta#8665; the merge had dropped meta#8676's spec timing change.
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
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
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
meta#8676 makes `locksSelectionFor` take the pointer kind alone: a finger or
a pen locks the selection under any primary pointer, a mouse never does
(spec:AST-094 FR41, spec:AST-017 FR59). The pager still passed the view
as a second argument, so the merged tree failed to type-check
(TS2554 at usePager.ts:976) and the build stopped there.

The pager's catch now asks with the kind that caught the settle. A test
pins the mouse case beside the finger's: a mouse press stops a settle
after the catch delay, as a finger's does (FR43), and the text stays
selectable throughout.
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

Choose a reason for hiding this comment

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

This pull request is on hold for an engineering owner's decision on two readings: spec:AST-094 FR43 does not say when a wheel gesture ends or what its release carries (here, 100 ms after the last wheel, with a trackpad's speed and macOS momentum kept and a mouse wheel's notches released still), and the carried FR41 text lets a claimed mouse or pen drag take no new text selection and clear the page's, which FR41 as it stands rules out for a mouse. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may record it too, as an engineering owner. Whatever the decision, these changes are needed:

  • Reduce the diff to this change: 28,409 of its 30,490 authored lines are the open #8665 stack (land it with #8666 and #8535 first, or retarget onto #8665), and the astryxStylex fix belongs in its own pull request.
  • Give a notch gesture's release zero velocity explicitly: a press within 40 ms of the last notch releases it with the notches' speed, against the gestures README.
  • Date a release that no input ends no later than the current wheel or the last notch: a detached pan's notch release is now stamped 100 ms ahead, which moves the next press's clock.

[Automated review]

Comment on lines +2172 to +2173
: endTime == null
? end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When no input ends a notch gesture, this dates its release WHEEL_END_MS after the last notch, but two callers reach endWheel() before that moment: the detach at lines 763-770 and the next wheel after every taker left (lines 1921-1928). arena.ended then moves the arena's clock to that future time (GestureArena.ts:949-953), so a press within the rest of those 100 ms is dated at the stamp and its long press fires up to 100 ms late, which spec:AST-094 FR16 rules out. Could those two paths end at the current wheel's time or at gesture.last instead, leaving last + WHEEL_END_MS to the timer?

This branch was successfully deployed

1 active deployment
Preview — f9e99b82 Deployed Oct 30, 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