Skip to content

[preview, do not merge] FR43 trackpad rescope: swipe rows, one step per swipe - #9173

Draft
cixzhang wants to merge 2420 commits into
mainfrom
preview/8676-step-rows
Draft

cixzhang wants to merge 2420 commits into
mainfrom
preview/8676-step-rows

Conversation

@cixzhang

Copy link
Copy Markdown
Contributor

Preview build only, for a live trackpad check of the AST-094 FR43 rescope proposed on #8676. Do not merge, review or land; it closes after the check.

Rescope: the pager and swipe rows take one discrete step per two-finger trackpad swipe. A sheet keeps a trackpad pull that releases when the fingers lift (momentum onset). An idle end releases still, so a slow drag springs back, and only a far pull or a real fling closes. Form and required sheets rubber-band and bounce as under a finger. The mouse wheel drives no pan.

vjeux added 30 commits October 27, 2026 05:11
@typescript-eslint/promise-function-async: the poll's callback returns a
promise.
…, which this pull request already carries; landedOn moves above eventTime's comment) into the every-pointer host
…; the drag selection case reads a frame later) into the wheel input
… s of wall clock

The two proofs that a Reduce Motion dip held before its first frame
still lands (after the hold is finished, and after it is resumed) polled
the box for 2 s of wall clock. On a loaded machine the page's main
thread ran the dip's next step late and the poll ran out first: one
WebKit run read {opacity: 0, translate: 'none'} with four WebKit
containers on the box, then passed in 530 ms on a rerun. They now wait
until the box runs no animation, on the animations' own finished
promises (including the fade back in the dip starts when its fade out
ends), and then assert the landed state. The test's own timeout still
bounds them.

With a 2.5 s main-thread block induced right after the release, both
proofs failed before this change (Timeout 2000ms exceeded while waiting
on the predicate) and pass after, 3 runs each in Chromium.
… element moves from under the pointer (spec:AST-094 FR43)

The wheel listener sat on the recognizer's element only, so once a pan
moved its element out from under a still cursor, the next steps landed on
what was under it, went unprevented, and the gesture ended early: of six
16 px trackpad steps, two moved the card. Fails until a wheel gesture is
latched to the recognizer that took it, as a browser latches a scroller.
… it (spec:AST-094 FR43, FR24)

As a browser latches a scroller: from the claim until the gesture ends,
a capture wheel listener on the document hands the host every step first,
wherever it lands, so a pan whose element moves from under a still cursor
keeps the gesture and every step stays prevented. It goes when the gesture
ends (100 ms after its last wheel), and at once when every recognizer that
took it has left: a detached, disabled or cancelled taker gives the rest
of the gesture back to the page. A page listener only during a gesture.
…gh the composed path) into the input hold
…input drivers) into the base of the every-pointer follow-up

# Conflicts:
#	packages/core/src/motion/README.md
) into the base of the every-pointer follow-up
…s attach for every pointer, and the pan's allowed pointer, a finger, is what keeps a mouse and a pen off (spec:AST-094 FR43, spec:AST-057 FR6, AR2)
…rag (real Chromium) or a pen never moves it, a finger's pointer events come with its touches, and the dismissing tap's click goes to the click guard (spec:AST-094 FR43, spec:AST-057 FR6, AR2)
…first lift event, its pointerup or its touchend, whichever the gesture host releases on (spec:AST-094 FR43)
…oes, and so does a mouse wheel's horizontal notch (spec:AST-057 FR4, DEC-7)
…r under the pointer could scroll for it (spec:AST-094 FR43, FR19)

A pull to refresh sits on the content, inside the scroller it pulls: the
host checked only the scrollers between the pointer and the pan's element,
so with scroll input allowed it would take any vertical wheel over the
content, mid-scroll or scrolling down from the top, and break native
scrolling. A finger's scroll-edge rule reads every scroller under it, the
page included, and so must a wheel's. And touch-action narrows only a
finger's pan: content with touch-action none still scrolls its owner with
a wheel. Both cases fail until the wheel reads the same chain.
…er under the pointer (spec:AST-094 FR43, FR19)

The wheel gesture now reads the scrollers under the pointer as a finger's
probe does, its own scroll owner and the page included, with
overscroll-behavior ending the chain, but without touch-action, which
narrows only a finger's pan (createNativeScrollProbe takes
readsTouchAction: false). beginWheel leaves a wheel to the page when a
scroll-edge pan's scrollers could still scroll for it, before anything is
prevented, and the wheel contact carries the probe, so the pan's own
scroll-edge check reads the same chain at its first move. Other pans keep
the rule for the scrollers between the pointer and their element.
…ss (spec:AST-094 FR43, FR24)

A pan with no pointer types and scroll input allowed, such as a pull's
wheel-only pan beside its finger pan, still armed its element for a press
where the primary pointer is fine: a pointerdown listener for a recognizer
no pointer can start. Fails until arming counts only recognizers that take
a pointer.
…t for a press (spec:AST-094 FR43, FR24)

Where the primary pointer is fine, an element is armed for a press while an
enabled recognizer or consumer on it could take one. A recognizer whose
allowedPointerTypes is empty takes only scroll input, so it no longer arms
one: its element keeps the wheel listener alone.
…e pointer, and no menu observes the pointer query (useMenuPress FR9; spec:AST-094 FR24)

Where the primary pointer is fine, a finger held on a DropdownMenu trigger
keeps the released behavior, whose tolerance is 10 px on each axis: an
(8, 8) settle opened the menu, and the long press's distance rule now
cancels it. And every useMenuPress caller, each menu and sub-menu,
subscribed to (pointer: coarse) though only the trigger reads it, when its
press starts. Both fail until the trigger takes its tolerance at the press.
…rts, and no menu observes the pointer query (spec:AST-094 FR29, FR24; useMenuPress FR9)

Every useMenuPress caller, each menu and sub-menu, subscribed to
(pointer: coarse) through useSyncExternalStore, though only the trigger's
held finger reads it. The trigger now reads the pointer as its press
starts and hands useLongPress that press's tolerance (start's new
tolerance argument): FR29's 13px drift where the primary pointer is
coarse, the long press's own 10px where it is fine, as the released
behavior keeps (on each axis, with #8358's measure). A changeset declares
the 13px a finger now gets on phones and tablets.
…entation says

useLongPress documents its tolerance as movement past moveCancelPx on
either axis, a released contract, and ContextMenu's measured hold and the
menu trigger's released behavior keep it. This pull request had changed it
to a distance, which cancels a (9, 9) drift the contract keeps. Fails until
the measure is the documented one again.
…ocumented; only the defaults have one source

useLongPress documents movement past moveCancelPx on either axis, and its
callers keep that: ContextMenu's measured hold is per axis, and the menu
trigger's released behavior keeps the hook's tolerance (useMenuPress FR9).
The defaults, 500ms and 10px, still come from the long-press
recognizer's table, which measures its own tolerance as a distance, as
UIKit's does. The changeset now says what this is: one source for the
numbers, no behavior change; useLongPress.doc.mjs was right all along.
…and a scroller inside a shadow tree from what the wheel landed on (spec:AST-094 FR43)

scrollsBy compared Math.abs(scrollLeft) against left-to-right bounds, so at
a right-to-left scroller's start (scrollLeft 0) it read a scroll right as
the scroller's and a scroll left as nobody's, handing the pan the wheels
the scroller should get. And the wheel's target at a shadow host's
listener is the host itself, so a scroller inside its shadow tree, between
the pointer and the pan, went unseen. Both fail until the host reads them.
…nd the scrollers under what it landed on (spec:AST-094 FR43)

scrollsBy reads scrollLeft against the scroller's own range: from 0 to the
overflow left to right, from minus the overflow to 0 right to left, where
a scroll right moves it toward 0. beginWheel walks, probes and records the
wheel from landedOn, the composed path's first element, so a scroller
inside a shadow tree between the pointer and the pan counts.
@astracat-bot

astracat-bot Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 31, 2026, 10:05 PM UTC

A slow swipe carries its travel across short gaps and commits at 24 px;
the step flick starts the settle at about 2500 px/s; a sheet a swipe
cannot close rubber-bands past its resting place from the first movement.

@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 preview cannot be approved until an engineering owner decides on the spec. It intentionally departs from AST-094 FR43 (a pan takes wheel and trackpad scrolling) and AST-057 FR4 and DEC-7 (a horizontal wheel, a mouse's included, moves a row as a finger does), and it carries two new spec sentences (FR41's claimed-drag selection rule and BottomSheet's entrance shield). An engineering owner, the author included as one, records that decision either by commenting /approve-spec with the full head commit SHA or by amending those records. These still need changes whatever is decided:

  • Split the roughly 53.8k authored lines: land the carried #8690 stack through its own pull requests and put the 233-line rescope up on top.
  • Update .changeset/gestures-wheel.md, .changeset/item-swipe-every-pointer.md, Item.doc.mjs and the PR description, which still promise mouse-wheel swiping or describe sheet and pager behavior this head does not wire.
  • Update the wheel.test.ts and Item.test.tsx cases that assert the old wheel contract, and add tests for the step and the momentum release.
  • Reuse the shared selection lock and input hold instead of sheetSelectionLock.ts and sheetSelfHold.ts, and attach the sheet body's touch listeners in its callback ref rather than an Effect.

[Automated review]

* `allowedScrollTypes: []` opts out.
*/
protected override get defaultScrollTypes(): ReadonlyArray<ScrollType> {
return [];

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 makes no pan take wheel or trackpad scrolling by default, which contradicts current AST-094 FR43 ("a pan also takes wheel and trackpad scrolling"). The doc comment above it (lines 105-109) and GestureRecognizer.ts:91-92 still describe the old default.

// contact, moving with the content.
allowedPointerTypes: ['touch', 'pen'],
allowedScrollTypes: ['continuous'],
wheelStep: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With allowedScrollTypes: ['continuous'] and wheelStep, a mouse's horizontal wheel no longer swipes the row, and a trackpad steps once instead of following. Current AST-057 FR4 and DEC-7 require both inputs to move the row as a finger does. The comment at 1247-1252 still says a mouse wheel's notches swipe "through the pan's default scroll input".

'@astryxdesign/core': patch
---

[feat] A pen swipes an `Item` or `ListItem` swipe-action row as a finger does, and so do a trackpad's horizontal scroll and a mouse wheel's horizontal notch. A mouse drag still never swipes a row.

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 release note still says a mouse wheel's horizontal notch swipes a row. At this head it does not (useSwipeAction.ts:1254-1255). .changeset/gestures-wheel.md:5 and Item.doc.mjs:202 say the same.

import {IOS_SUPPORTS_QUERY} from '../utils/touchPresentation';

/** Inputs whose text a held finger edits (gestures/selectionLock.ts). */
const TEXT_ENTRY_TYPES = new Set([

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

TEXT_ENTRY_TYPES, isTextEntry, isIosWebKit and the non-iOS selectstart refusal below copy utils/selectionLock.ts (lockSelection, lines 44-52, 87-104, 285-313). Calling lockSelection (with an option, if the selectable-control target must be skipped) would keep one path.

node == null
? null
: {start: onTouchStart, move: onTouchMove, end: onTouchEnd};
setBodyMounts(mounts => mounts + 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The callback ref bumps bodyMounts only to trigger the Effect at 2752 that attaches touch listeners to this node. The callback ref can attach and remove them itself (react-component-runtime INV2, INV6), which drops the state and the Effect.

@cixzhang cixzhang closed this Oct 31, 2026
@cixzhang
cixzhang deleted the preview/8676-step-rows branch October 31, 2026 19:09
@cixzhang
cixzhang restored the preview/8676-step-rows branch October 31, 2026 19:16
@cixzhang cixzhang reopened this Oct 31, 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 preview can't be approved until an engineering owner decides on the spec: it intentionally changes how swipe rows take trackpad and mouse-wheel input, which contradicts AST-057 FR4, AR2 and DEC-7 and the row clause of AST-094 FR43, and it carries new FR41 and BottomSheet entrance-shield text. An engineering owner (the author can, as one) records that decision by commenting /approve-spec <full head SHA> on this pull request or by amending those records. Whatever is decided, these need changes:

  • Restore WHEEL_END_MS as the end-timer fallback at GestureHost.ts:2071, where a mouse-wheel pan and a ctrl-wheel pinch now end at 180 ms and two wheel tests fail.
  • Play a motion role instead of the literal spring at useSwipeAction.ts:798 (AST-094 FR4).
  • Split the roughly 54k authored lines so the ~500-line rescope lands alone on top of the carried #8690 stack.
  • Update the changeset, Item.doc.mjs, the code comments and the six tests that still describe the old wheel contract, add tests for the step, the undo and the classifier, and drop the preview-only probe story.

[Automated review]

},
gesture.step != null && !gesture.step.committed
? WHEEL_STEP_CARRY_MS
: gesture.follow?.endMs ?? WHEEL_FOLLOW_END_V_MS,

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 fallback now applies to every gesture that is not a followed trackpad pan, so a mouse-wheel pan and a ctrl-wheel pinch end 180 ms after their last wheel. The quiet-gap check (:2001) and the release stamp in endWheel (:2448) still use WHEEL_END_MS, so the end arrives 80 ms after the time it carries. wheel.test.ts:514 and :624 fail on it and pass with gesture.follow?.endMs ?? WHEEL_END_MS here.

const motion = springTo(
layout,
travel,
fast ? {mass: 1, stiffness: 600, damping: 49} : SWIPE.settle,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A literal spring for the trackpad settle. AST-094 FR4 says a component "MUST read touch motion through these roles and MUST NOT play a literal", and FR2 wants a source or a guess label for the value. Play SWIPE.settle, or add a named trackpad-step settle role.

// (DEC-7) through the pan's default scroll input (FR43): the same
// contact, moving with the content.
allowedPointerTypes: ['touch', 'pen'],
allowedScrollTypes: ['continuous'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With allowedScrollTypes: ['continuous'] and wheelStep, a mouse's horizontal wheel no longer swipes the row and a trackpad steps once instead of following. AST-057 FR4 and DEC-7 say a horizontal wheel gesture MUST move the row as a finger does, so this waits on an engineering owner's decision. The comment just above still describes the old contract.

'@astryxdesign/core': patch
---

[feat] A pen swipes an `Item` or `ListItem` swipe-action row as a finger does, and so do a trackpad's horizontal scroll and a mouse wheel's horizontal notch. A mouse drag still never swipes a row.

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 release note still says a mouse wheel's horizontal notch swipes a row. At this head it does not (useSwipeAction.ts:1262-1263), and a trackpad takes one step per swipe. Item.doc.mjs:202 says the same.


type Row = {g: number; start: string; phase: string; gap: number; mode: number; dx: number; dy: number; wdx: number | null; wdy: number | null; kind: string};

function classify(e: WheelEvent): string {

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 copies scrollTypeOf from GestureHost.ts:454-480 (the header says it "Mirrors the classifier"), and its mount Effect adds the wheel listener a callback ref could own. As a preview-only probe it should come out before any merge.

…he fingers' speed (stamped at their last move); unit case

@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 preview can't be approved until an engineering owner decides on the spec: it intentionally changes how swipe rows take trackpad and mouse-wheel input, which contradicts AST-057 FR4, AR2 and DEC-7 and the row clause of AST-094 FR43, and it carries new FR41 and BottomSheet entrance-shield text. An engineering owner (the author can, as one) records that decision by commenting /approve-spec <full head SHA> on this pull request or by amending those records. Whatever is decided, these need changes:

  • Play a motion role instead of the literal spring at useSwipeAction.ts:798 (AST-094 FR4).
  • Split the roughly 54k authored lines so the ~530-line rescope lands alone on top of the carried #8690 stack.
  • Update the changeset, Item.doc.mjs, the code comments and the six failing tests that still describe the old wheel contract, add tests for the step, the undo and the classifier, and drop the preview-only probe story.
  • Reuse the shared selection lock and input hold instead of the sheet's copies, and move the three flagged Effects into callback refs or the handlers that cause them.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — 1543eea8 Deployed Oct 31, 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 needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants