Skip to content

feat(Item): rows swipe on the shared drag path; a trackpad's swipe follows the fingers (spec:AST-094 FR43, DEC-24; spec:AST-057) - #8690

Draft
vjeux wants to merge 21 commits into
meta:mainfrom
vjeux:uikit/c12-swipe-every-pointer
Draft

vjeux wants to merge 21 commits into
meta:mainfrom
vjeux:uikit/c12-swipe-every-pointer

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #9261 (on #8658, #8655, #8653, #8676 and #8360). This pull request's own commits: vjeux/astryx@980f0d1...eb34463

Status: draft. CI is running at this head. Already passed locally on this code: unit tests (16,236 in Core), lint, tsc and check:repo; the row's interaction proofs in Chromium (4 workers, and the busy-main-thread proofs at 1) and in WebKit (10 repeats at the CI profile, and at 3 CPUs with 1 and 16 workers); the shared drag, animator and gesture proofs in both engines; and the swipe accessibility contracts in Chromium (3 repeats).

User need

A list row with swipe actions is a touch accelerator: a finger, a pen or a trackpad's two-finger swipe drags the row aside to uncover its actions.

  • On a trackpad, the row should move with the fingers and, when they lift, settle at their speed. Today it trails the fingers by 10–14 px and comes to a stop before it settles.
  • On a finger, the row should keep UIKit's feel: a dead zone, a tracking spring, and UIKit's release rule.

Sheets and pagers already run one shared drag path. A row that runs its own copy of that machinery drifts from them, so the row should run the same path and declare only what is its own: its stops and its actions.

Problem and solution fit

  • The row ran its own pan, its own frame loop and its own springs, so the trackpad behavior that spec:AST-094 FR43 gives every followed pan (follow from the first event past 8 px, settle by position and velocity, keep the fingers' speed) never reached it.
  • The row now runs the shared path: the trackpad splitter, the pan, the snap-point drag and the animator (spec:AST-094 DEC-24). It declares its stops (rest, the open width, and out past its edge for a full swipe) and its actions, and the drag draws its travel into the one custom property every part of the panel reads (spec:AST-057 FR2).
  • The finger's UIKit model is the one behavior the shared path does not have, so the row supplies it through the shared drag's model hooks instead of running its own drag.

Current specification

  • spec:AST-094 FR43 and DEC-24: a followed trackpad pan follows the fingers one for one from its first event past 8 px and settles by position and velocity from their last speed; within one gesture an open row only closes; one shared path, and a component declares only its detents and actions.
  • spec:AST-057: FR1 (actions as data), FR2 (the travel as one custom property), FR4 and DEC-7 (a finger, a pen or a trackpad swipes; a mouse drag never does), FR5 (UIKit's release rule; a full swipe by position only; firing once the content leaves its box), FR7 (an open row's closes), AR2.
  • spec:AST-107 FR7: Core uses the public layer except for the known public-API gaps that architecture:react-component-runtime INV13 lists. This pull request names the row's jobs there.

Public delta and caller burden

  • API: none. swipeActions and swipeBehavior keep their types and defaults. Core's declarations were compared from the base to the head over every file reachable from the package's public entry points (153 entries, 567 files): nothing reachable is added or removed, and the one reachable change is the JSDoc text of Item's existing props.
  • Behavior:
    • a trackpad's two-finger swipe moves a row with the fingers and lets it go at their speed;
    • a mouse drag and a mouse wheel's notches never swipe a row;
    • a fast flick from an open row settles open, since speed alone never fires an entry (spec:AST-057 FR5), and FR43's long fast swipe fires only where spec:AST-057 allows a full swipe.
  • Callers: nothing to change.

Design and alternatives

Internal options the shared primitives gain (no public API; they land with this, their first user):

  • createPan, under the public createPanGesture: which pointers and scrolls start a drag, its claim distance, a begin gate and the selection lock; each report says what drove the drag.
  • createSnapDrag, under the public createDragToSnapPoints, which keeps its behavior and tests:
  • A settle-passes-offset callback: settleTo({onPass}) calls back on the first animation frame whose presented value has passed the offset, read on the settle's own curve. It uses no timer, and never fires for a settle that lands short, is replaced, or is destroyed.
  • The animator draws a channel into one registered custom property, with an optional companion: a second custom property drawn from that value by a pure mapping, in the same keyframes, so it cannot drift from the travel.

What stays Item's, and why. useSwipeAction.ts is 675 code lines (comments and blank lines excluded):

  • before the callback and the companion moved into the shared drag, it was 778;
  • those two moves took it to 669, adding 21 lines to the shared drag and 29 to the animator;
  • passing the start option on the row's input-driven settles added 6.

The row builds no pan recognizer, runs no frame loop and solves no spring. What remains:

  • its stops and actions: measuring the entries, and the side geometry the panel's CSS reads;
  • the finger's UIKit model: the dead zone, the tracking spring, the never-past-rest clamp, the confirm point and run-ahead, and UIKit's release rule (swipeRelease.ts);
  • the full swipe's action timing (spec:AST-057 FR5);
  • the open row's closes (spec:AST-057 FR7);
  • the panel itself (ItemSwipeLayer.tsx).

Bundle (INV13), pending spec-owner approval. The row's swipe layer now reaches the gesture core, about 74 KB minified of gestures/ modules.

  • The layer stays lazily loaded, as on main, so the twelve public imports that render Item rows ship none of it until a row has swipe actions.
  • The last commit, on its own so it can be dropped or swapped:
    • amends INV13: a gesture that a prop turns on lives in a layer that the component loads with import() the first time the prop is set;
    • changes the bundle guard: it builds the public imports with every import() left out, so each output holds what its import ships at once, and it names each prop-gated layer that brings gesture code later.
  • Rejected: importing the layer statically, which puts the gesture core in every one of those twelve imports.

Evidence

  • Trackpad, WebKit:
    • the row moves one for one on every wheel;
    • the release keeps the fingers' speed with no stop;
    • it never passes rest in one gesture;
    • a long fast swipe fires the outermost action;
    • a fast flick from open settles open;
    • Reduce Motion lands at once on a fine pointer;
    • a click closes a row a trackpad opened and reaches nothing.
    • Chromium's synthetic wheels carry a fixed wheelDelta, which FR43 reads as a mouse wheel's, so the trackpad cases skip there.
  • A mouse wheel's notch never swipes a row (Chromium).
  • Fingers, by real touch in both engines:
    • the tracking spring and its catch-up, read on the curve the row played;
    • UIKit's settle, its keyframes and every frame drawn;
    • a flick hands the row's speed to the settle;
    • a catch;
    • the release rule;
    • the full swipe;
    • the Reduce Motion cross-fades;
    • FR7's closes.
  • A close held back by a later listener's block starts after the block, and every frame drawn lies on its curve: the row's input-driven settles pass start: 'next-frame'.
  • Unit:
    • the row runs the shared drag and declares its stops there (Item.sharedDrag.test.tsx);
    • the shared drag's internal options, each red first (createSnapDrag.test.ts);
    • a disabled entry stays focusable and inert on an open row (spec:AST-085 FR1, FR2).

Supersedes #8069, #8096, #8132 and #8134

This pull request contains each of them, and they close when it is pushed. Each one's tests, changeset and doc changes were checked against this branch:

  • chore(core): swipe actions' UIKit values and release rule #8069 (UIKit values and release rule):
    • swipeActions.ts, swipeRelease.ts, its MEASUREMENTS.md rows and its motion/README.md row are here.
    • Of its 11 test titles, 4 are here unchanged and 1 is renamed (the lab batch numbers became "UIKit lab, iOS 26.2 simulator, Oct 23 2026"). fix(Item): a swipe row's full swipe confirms, runs ahead and fires like UIKit's #8134 replaced the other 6: the commit point became the confirm point, and one release case was reworded. Those replacements are here.
    • Its changeset is here, reworded for that model.
  • fix(Item): swipe actions on the gesture system #8096 (the gesture system):
    • 14 of its 18 test titles are here unchanged.
    • Its test that other recognizers stay inactive under a fine pointer is updated for spec:AST-094 FR43: gestures attach for every pointer.
    • Its two swipe-layer render tests are here, written for the lazily loaded layer.
    • Dropped on purpose: its pins that the layer imports with the row. The layer stays lazy (see the bundle note above), so main's test that the layer arrives only through a dynamic import stands, as do main's List tests.
    • Its changeset is here, with the trackpad added.
  • fix(Item): an open swipe row closes on a tap on its content or a scroll #8132 (an open row's closes): 11 of its 12 test titles are here unchanged; one is renamed for the focus rule ("inside its open panel"). Its changeset is here, reworded the same way.
  • fix(Item): a swipe row's full swipe confirms, runs ahead and fires like UIKit's #8134 (the full swipe):
    • 19 of its 30 test titles are here unchanged.
    • 4 release-rule tests lost their "(FR5: …)" departure notes, and the describe that grouped them is gone, since spec:AST-057 FR5 now records the measured rule.
    • Its swipeCurves unit tests (one describe, four cases) covered per-part curves. Under spec:AST-057 FR2 every part reads one custom property, so those per-part curves no longer exist; the browser proofs check every part against the travel on every frame.
    • Dropped on purpose: its proof that the slide-out advances through a main-thread block. It needs the compositor translate that fix(Item): swipe actions move on the compositor with UIKit springs #8097 holds, because the travel's custom property animates on the main thread.
    • Its changeset is here unchanged.

#8097 stays open for its compositor change. Its other behavior (UIKit's springs, a flick that opens or closes, Reduce Motion, a catch) is here, with its changeset line minus the compositor sentence. #8375 stays open, to rebuild on this pull request.

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

  • pnpm exec vitest run packages/core, plus the CLI, registry and touch-bundle guards.
  • pnpm check:repo and pnpm check:knowledge.
  • Interaction proofs: packages/core/src/Item/__tests__/ItemSwipeActions.interaction.browser.spec.ts, with the animator, snap-point drag and gesture specs, in Chromium and WebKit.
  • Accessibility contracts: ItemSwipeActions.a11y.chromium.spec.ts and ItemSwipePinchZoom.a11y.chromium.spec.ts.

Writing rubric results

Records edited: architecture:react-component-runtime (INV13's known public-API gaps, and the prop-gated gesture layer), flagged for spec approval.

  • R1 narration: none. Both additions state, in the present tense, which jobs reach the modules and how a prop-gated gesture ships.
  • R2 research: none. The bundle measurement stays in this description.
  • R3 evidence: cites spec:AST-057 AR2, FR2 and FR5; no change references.
  • R4 rejected alternatives: none in the record.
  • R5 change references: none.
  • R6 open questions: none.

@vercel

vercel Bot commented Oct 27, 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 1, 2026 5:39pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 1, 2026, 6:01 PM UTC

@github-actions github-actions Bot added needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval labels Oct 27, 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 needs a spec decision before it can be approved: spec:AST-057 on main lists two-finger trackpad swipes as a non-goal and its AR2 keeps the swipe off pointers that can hover, while the DEC-7 cited here exists only in the open #8646, whose FR4 would also make a pen swipe, which this change forbids. An engineering owner records that decision by commenting /approve-spec 6df8dd02f0913da1c01e72453a6415dffd3b9cec on this pull request, and the author may do so as an engineering owner. Whatever that decision, two changes are needed:

  • Land or retarget the dependency pull requests, so this diff carries only its own 381 lines instead of 53,279.
  • When a trackpad has opened a row, a click on its content should close it without running the row's action, as a finger's tap already does.

[Automated review]

// A trackpad's horizontal scroll swipes the row as a finger does, and
// a mouse wheel's horizontal notches the same way (`spec:AST-057`
// DEC-7): the same contact, moving with the content.
allowedScrollTypes: ['continuous', 'discrete'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: spec:AST-057 on main lists two-finger trackpad swipes as a non-goal (line 138), and its AR2 keeps the gesture's paint, tab order and accessibility tree untouched on a pointer that can hover (lines 250–252). The DEC-7 cited here exists only in the open #8646, so this opt-in waits on an engineering owner's decision on that record.

rootRef,
() =>
new TapGestureRecognizer({
allowedPointerTypes: ['touch'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Now that a trackpad can open a row, this dismissal still takes only a finger. In Chromium, a click on the content of a trackpad-opened row reaches the row's onClick and the row stays open, while a finger's tap closes it and activates nothing (Item.test.tsx:1178). Letting any pointer's press dismiss while the row is open, and swallowing its click through the click guard, would match the finger path; a test for a wheel-opened row would pin it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 08e6916: the dismissing tap now takes every pointer and the click guard swallows the click, so a mouse click on a trackpad-opened row closes it; tests added in both engines.

@github-actions

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

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 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 needs a spec decision before it can be approved: spec:AST-057 on main lists two-finger trackpad swipes as a non-goal and keeps the swipe off pointers that can hover (AR2, DEC-4), while the DEC-7 cited here exists only in the open #8646, whose FR4 would also make a pen swipe, which this change forbids. An engineering owner records that decision by landing the spec:AST-057 amendment with the pen settled, or ruling otherwise, and commenting /approve-spec 08e6916d0e698009bca4d50efdd10c6824a0c381 on this pull request; the author may do so as an engineering owner. Whatever that decision, two changes are needed:

  • Land or retarget the dependency pull requests, so this diff carries its own 458 lines instead of 53,360.
  • Under Reduce Motion on a fine pointer, a trackpad or wheel swipe's settle should complete at once, as design:reduced-motion DR3 requires, instead of cross-fading like a touch release (useSwipeAction.ts:777, :834).

[Automated review]

Comment on lines +775 to +777
// Under Reduce Motion the animator cross-fades or lands at once
// (design:reduced-motion); what a release started always cross-fades.
const touchRelease = cause === 'release';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Under Reduce Motion this treats every release as a touch release, so when a trackpad or wheel gesture ends on a fine primary pointer the row's settle cross-fades for up to 200 ms. design:reduced-motion DR3 requires any other transition on a fine pointer to complete at once (DR2's exception covers only the release of a touch drag), and #8646's FR5 asks the same of a trackpad-started swipe. Passing touchRelease only when a finger's contact ended the drag, here and in the slide-out fade at line 834, with a Reduce Motion test for a wheel-opened row, would fix it.

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

The trackpad, wheel and pen swipe now matches spec:AST-057 on main (FR4, FR5, DEC-7), but approval needs an engineering owner’s decision on two things this head adds without current spec: the new component:BottomSheet element job for the input hold’s shield (BottomSheet.spec.md, lines 232–236), and whether a mouse or pen press on an open row’s own content should close it and activate nothing (useSwipeAction.ts, lines 1308–1332). An engineering owner records that decision by commenting /approve-spec bfef65e146dc5a8089b53dd84dd6c6dfafb1d1bd on this pull request, or by ruling otherwise in a comment; the author may do so as an engineering owner. Whatever that decision, two changes are needed:

  • Land or retarget the dependency pull requests, so this diff carries its own 637 lines instead of 54,294.
  • In the swipeActions doc and JSDoc, say a swipe-only verb is unreachable “by keyboard and by a mouse drag” and that an open row closes on a press elsewhere (Item.doc.mjs:202, Item.tsx:280 and :283).

[Automated review]

rootRef,
() =>
new TapGestureRecognizer({
name: 'item-swipe-dismiss',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: with no pointer gate, a mouse or pen press on an open row’s own content now closes it and its click is swallowed. spec:AST-057 on main states no content-press dismissal for any pointer (FR7 lists a pointer outside, Escape, firing an entry and unmount; DEC-7 covers reveal and fire), so this waits on an engineering owner’s decision.

* the side's panel; a mouse never starts the drag, and a mostly vertical
* drag stays the scroller's. The gesture is
* a touch accelerator and never a row's only path to a verb: a verb
* reachable only by swipe is unreachable by keyboard and by mouse, so every

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 mouse’s horizontal wheel now reaches a swipe-only verb (DEC-7), and a press from any pointer outside closes an open row (FR7). Saying “unreachable by keyboard and by a mouse drag”, as spec:AST-057 asks the docs to, and “on a press elsewhere” on line 283 would match; the same two phrases are in Item.doc.mjs:202.

Comment on lines +232 to +236
- while a modal sheet enters under a finger, the layer's input hold adds a
shield inside the dialog: a transparent cover of the viewport that takes
a tap or a drag landing during the entrance (`spec:AST-094` FR35). It is
removed when the sheet lands. A non-modal sheet adds no shield: it holds
only input on itself, and the page beside it stays live.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: this adds a named element job that the record on main does not have, and its verification row fails an element in the open sheet’s frame with no named job. An engineering owner approves or rejects it, ideally with the pull request that adds the shield.

@vjeux vjeux changed the title feat(Item): a trackpad's horizontal scroll swipes a row as a finger does, and a mouse drag never does (spec:AST-057 DEC-7, spec:AST-094 FR43) feat(Item): a pen or a trackpad's horizontal scroll swipes a row as a finger does, and a mouse drag never does (spec:AST-057 FR4, DEC-7; spec:AST-094 FR43) Oct 27, 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.

The pen, trackpad and wheel swipe matches spec:AST-057 on main (FR4, FR5, DEC-7), but approval needs an engineering owner's decision on two things this head adds without current spec: whether a mouse or pen press on an open row's own content should close it and activate nothing (useSwipeAction.ts, lines 1308–1333), and the new component:BottomSheet element job for the input hold's shield (BottomSheet.spec.md, lines 232–236). An engineering owner records that decision by commenting /approve-spec 90bf79fdcdaa1bba8793bb0ba4d610bae4c3977b on this pull request, or by ruling otherwise in a comment; the author may do so as an engineering owner. Whatever that decision, three changes are needed:

  • Land or retarget the dependency pull requests, so this diff carries its own 644 lines instead of 54,742.
  • In the swipeActions doc and JSDoc, say a swipe-only verb is unreachable "by keyboard and by a mouse drag" and that an open row closes on a press elsewhere (Item.tsx lines 280 and 283, Item.doc.mjs lines 202 and 257).
  • Add the physical-device check spec:AST-057's verification asks for: a real trackpad (a slow swipe and a quick flick) and a real pen on the swipe-actions story.

[Automated review]

rootRef,
() =>
new TapGestureRecognizer({
name: 'item-swipe-dismiss',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: with no pointer gate, a mouse or pen press on an open row's own content now closes it and its click is swallowed. spec:AST-057 on main states no content-press dismissal for any pointer (FR7 lists a pointer outside, Escape, firing an entry and unmount; AR3 keeps the primary's activation), so this waits on an engineering owner's decision.

* the side's panel; a mouse never starts the drag, and a mostly vertical
* drag stays the scroller's. The gesture is
* a touch accelerator and never a row's only path to a verb: a verb
* reachable only by swipe is unreachable by keyboard and by mouse, so every

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 mouse's horizontal wheel now reaches a swipe-only verb (DEC-7). spec:AST-057 asks the docs to say "unreachable by keyboard and by a mouse drag"; the same phrase is in Item.doc.mjs:202 and :257.

* entry must also be reachable through the row's own content (a menu or a
* button in `endContent`).
* button in `endContent`). An open row closes on a tap on its own content,
* which activates nothing and paints no press, on a touch elsewhere, when

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR7 closes an open row on "a pointer landing anywhere outside it", which this head does for every pointer; "on a press elsewhere" would match here and in Item.doc.mjs:202.

Comment on lines +232 to +236
- while a modal sheet enters under a finger, the layer's input hold adds a
shield inside the dialog: a transparent cover of the viewport that takes
a tap or a drag landing during the entrance (`spec:AST-094` FR35). It is
removed when the sheet lands. A non-modal sheet adds no shield: it holds
only input on itself, and the page beside it stays live.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: this names an element job the record on main does not have, and its verification row fails an element in the open sheet's frame with no named job. An engineering owner approves or rejects it, ideally with the pull request that adds the shield.

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:39
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
… the input drivers helper 4c0e0a5) into meta#8690

# Conflicts:
#	packages/core/src/Item/useSwipeAction.ts
#	packages/core/src/motion/README.md

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

The pen, trackpad and wheel swipe matches spec:AST-057 on main (FR4–FR6, DEC-7), but approval needs an engineering owner's decision on three spec deltas this head carries without current authority: a mouse or pen press on an open row's own content closing it without activating anything (useSwipeAction.ts, lines 1300–1324), the new component:BottomSheet element job for the entrance shield (BottomSheet.spec.md, lines 233–237), and the new spec:AST-094 FR41 rule for claimed mouse and pen drags (docs/specs/AST-094-touch-presentations/spec.md, lines 471–475). An engineering owner records that decision by commenting /approve-spec 6c08e005cc6ac6102d2a33aba151779fa96a843a on this pull request, or by ruling otherwise in a comment; the author may do so as an engineering owner. Whatever that decision, three changes are needed:

  • Land or retarget the dependency pull requests, so this diff carries its own 596 lines instead of 54,299.
  • In the swipeActions JSDoc and doc, say a swipe-only verb is unreachable “by keyboard and by a mouse drag” and that an open row closes on a press elsewhere (Item.tsx lines 280 and 283, Item.doc.mjs lines 202 and 257).
  • Add the physical-device check spec:AST-057's verification asks for: a real trackpad (a slow swipe and a quick flick) and a real pen on the swipe-actions story.

[Automated review]

rootRef,
() =>
new TapGestureRecognizer({
name: 'item-swipe-dismiss',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: with no pointer gate, a mouse or pen press on an open row's own content now closes it and its click is swallowed. spec:AST-057 on main states no content-press dismissal for any pointer (FR7 lists a pointer outside, Escape, firing an entry and unmount; AR3 keeps the primary's activation), so this waits on an engineering owner's decision.

* the side's panel; a mouse never starts the drag, and a mostly vertical
* drag stays the scroller's. The gesture is
* a touch accelerator and never a row's only path to a verb: a verb
* reachable only by swipe is unreachable by keyboard and by mouse, so every

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 mouse's horizontal wheel now reaches a swipe-only verb (spec:AST-057 DEC-7), and the record asks the docs to say “unreachable by keyboard and by a mouse drag”; the same phrase is in Item.doc.mjs (the swipeActions description and the guidance).

* entry must also be reachable through the row's own content (a menu or a
* button in `endContent`).
* button in `endContent`). An open row closes on a tap on its own content,
* which activates nothing and paints no press, on a touch elsewhere, when

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FR7 closes an open row on “a pointer landing anywhere outside it”, which this head does for every pointer; “on a press elsewhere” would match here and in Item.doc.mjs.

Comment on lines +233 to +237
- while a modal sheet enters under a finger, the layer's input hold adds a
shield inside the dialog: a transparent cover of the viewport that takes
a tap or a drag landing during the entrance (`spec:AST-094` FR35). It is
removed when the sheet lands. A non-modal sheet adds no shield: it holds
only input on itself, and the page beside it stays live.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: this names an element job the record on main does not have, and its verification row fails an element in the open sheet's frame with no named job. An engineering owner approves or rejects it, ideally with the pull request that adds the shield; the bullet also leaves the shield's accessibility exposure unstated.

Comment on lines +471 to +475
(`spec:AST-017` FR59; DEC-9). A mouse or pen drag that a recognizer claims
takes no new text selection for that drag only. A claim made after the
drag has moved clears the page's selection; a press claimed at its own
pointerdown, before it moves, keeps it. An unclaimed mouse drag keeps its
released behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: FR41 on main says a mouse “otherwise keeps its released behavior”; this adds a rule for drags a recognizer claims (no new selection, a later claim clearing the page's selection, a claim at pointerdown keeping it) and a verification row for it, which waits on an engineering owner's decision, ideally in the pull request that adds the every-pointer host.

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

Approval waits on an engineering-owner decision on three spec deltas this head carries without current authority: a mouse or pen press on an open row's content closing it (useSwipeAction.ts, lines 983–1008; spec:AST-057 FR7 names only a touch tap), the architecture:react-component-runtime INV13 amendments (lines 260–285), and the new spec:AST-094 FR11 and FR19 rules (lines 200 and 315). An engineering owner records that decision by commenting /approve-spec 6cac398ae3644e70229ce8ffdde442d38f7cf7be on this pull request, or by ruling otherwise in a comment; the author may do so as an engineering owner. Whatever that decision, these changes are needed:

  • Land or retarget the six dependency pull requests and split this change, so the diff carries its own work instead of 33,684 lines.
  • Keep the row's settle advancing through a busy main thread, as spec:AST-094 FR5 requires; the custom-property animation runs on the main thread (animator.ts, lines 344–345).
  • Subscribe the open row's outside-press and scroll closes through subscribeInput from the open transition instead of the Effect's own document listeners (useSwipeAction.ts, lines 933–981).
  • Add the physical-device check spec:AST-057 asks for, and say “on a press elsewhere” in the swipeActions docs (Item.tsx line 293, Item.doc.mjs line 202).

[Automated review]

// UIKit's dismissal does: the tap recognizes at the lift, and the host
// keeps the click it would have made from the row (it cancels the touch,
// or where the primary pointer is fine its click guard swallows the
// click). Any pointer's press dismisses, since a trackpad opens a row too

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: with no pointer gate, a mouse or pen press on an open row's own content closes it and its click is swallowed. spec:AST-057 FR7 on main closes an open row on “a touch tap on its own content” and AR3 says the same, so this waits on an engineering owner's decision.

settle(side, restTravel, 'resting');
return;
};
document.addEventListener('pointerdown', onOutside, 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.

These document listeners sit beside subscribeInput (utils/inputStream.ts), which this branch makes the one entry for document-wide input, and the work is caused by opening, which setSwipeState already sees. Subscribing there on open and unsubscribing on close would remove this Effect; its dependencies also leave out state.landing, which isOpen() reads.

* element whose parts all read one value (a row's swipe travel,
* `spec:AST-057` FR2): a `<length>` in px, or a plain `<number>`. The
* property is registered as inherited so it interpolates, and it animates
* on the main thread, not the compositor; every other channel renders as

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 row's settle drawn into this custom property no longer advances through a main-thread block, which spec:AST-094 FR5 requires of every release; main settled the row with a compositor transform transition.

* entry must also be reachable through the row's own content (a menu or a
* button in `endContent`).
* button in `endContent`). An open row closes on a tap on its own content,
* which activates nothing and paints no press, on a touch elsewhere, when

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Any pointer landing outside closes an open row (useSwipeAction.ts onOutside), as spec:AST-057 FR7 says; “on a press elsewhere” would match, here and in Item.doc.mjs.

vjeux added 21 commits November 1, 2026 05:03
meta#8360's change on the internal touch foundation: one click guard shared
by the menu press and the gesture host (its click listener stays once
the menu press arms it, as its record has it, and lasts only while armed
for the gesture host, FR24), the host as the one touch owner (finger
scroll, page holds, attachments, the press tracker hook and a held
control's kept touch, the long press's single source, presented hit
testing on iOS WebKit), its cases in the gesture browser proof (Chromium
and WebKit) and the README. It carries meta#8074, meta#8270, meta#8293, meta#8315, meta#8320
and meta#8356, and folds in meta#8358 (the long press's tolerance comment and an
unused import). Rebased onto main after section 2 as one commit; the
touch-action rule came with meta#8071. Where fingers come from their pointer
events, a claimed finger's touchend is cancelled from its element
(FR21), under a fine primary pointer wherever the browser sends Touch
Events and inside an open shadow root too; at most eight such lifts wait
for their touchend, and each goes once taken or after 500 ms, kept
across the host's uninstall, which can run in the microtask after the
pointerup, before the touchend that comes later in the same task (FR24).
An armed press's page listeners go once the arena has decided its last
touch, a tap that waits on a double tap included, and after its last
detach the host holds no listener, note, frame or timer (FR24).

The animator's busy proof is judged against the harness's compositor-only
control (meta#8906), with a main-thread mutant as its negative, and the
spring's curve is placed against the control's own clock, so the late
frames a loaded runner sends fail nothing.

With the host serving every pointer (meta#8071), this change's own pieces
follow it: a press tracker takes a mouse press as a press, painting at
once and braking no scroll (FR11); a finger's or a pen's hold that does
something keeps text unselected under any primary pointer, while a mouse
keeps its released behavior (FR41); and `perEventPointer`, from before
FR43, changes nothing. These come from meta#8665, which keeps only the
undecided rule for text selection during a mouse or pen drag.

The hover recognizer and the host's hover routing land with hover's
first user.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…finger landed

On iOS WebKit the host resolves moving content at touch-down and asks the
engine what is under the finger. When a busy main thread hands it the touch
later, the content has moved on: the host now asks at the point where what
was under the finger at the touch's timestamp is drawn, so a tap on a moving
sheet's Done still clicks Done (spec:AST-094 FR11). A presented source
reports its offset at a given time for this.

The presented browser proof drives the sheet with Core's animator and a
critically damped spring, up into place and down off the screen over the
scrim: a tap on the drawn Done clicks it once, and a drag on the drawn sheet
catches it where it is drawn, in both directions.
…the finger (FR11, FR19); INV13 names the shared input listener

Where the engine hit-tests an element a compositor animation moves at the
motion's start (iOS WebKit), a touch on that content goes to what was drawn
under the finger at touch-down, and a tap there activates that element with
one click while the browser's own click is cancelled or swallowed (FR11).
While such content around a recognizer that claims moving touches is moving,
the document may carry a non-passive touchmove listener that cancels only
owned moves and comes off when the motion ends; while it is on, a native
scroll waits for a free main thread (FR19). 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
(architecture:react-component-runtime INV13).
…view fixes

A touch the host handles late now also leaves alone content that slid
under the finger after it landed: the touch goes to what was beneath it
then (spec:AST-094 FR11). The input-stream changeset says what moved:
the gesture host's listeners, and the press feedback's scroll and
visibility listeners, since its pointer listeners were already on the
window's capture phase. The coarse-pointer gate reads TOUCH_QUERY from
touchQueries.ts instead of spelling it again.

The WebKit interaction job's scope covers every module the gesture host
now bundles: hooks/isRtlElement and utils' flatTree, hapticSwitch,
inputStream, clickGuard, selectionLock and coarsePointer. The scope
test's example of a util outside it moves from clickGuard, which the
gesture fixture now bundles, to focusReturn.

The presented proof checks that the sheet moves on a spring (its speed
changes along the animator's curve) and no longer compares the drawn box
with the animator's offset: under a 3-CPU cap the two disagree by 3 to
27 px in about half the frames whatever reference is used, while the tap
and drag cases go through the host's real path.
…e origin most frames agree with

Under the CI lane's WebKit profile (four CPUs, one worker), "a spring keeps
advancing every frame through a 1 s main-thread block" failed in two ways,
each about once in a few hundred runs, and a failing run's frames show each
cause:

- The spring drifted 138.6 ms against the control. The control's clock took
  its origin from the first usable frame alone, reading that frame's
  direction from the next usable one: the frame sat at x 210 on its way out,
  the next usable one at x 210 after the turn, and the tie read as "back".
  The clock now lives in controlClock.ts and takes the origin most frames
  agree with. On that run's own frames it reads 6.5 ms (bound 12), and its
  unit case for that layout fails on the old reading (150.7 ms astray).
- The spring "moved on 61 of the 62 frames the control moved". At the
  block's end it was in its tail, under two of the cast's 1 px steps a
  frame, where a frame delivered early shows no step and the next catches
  up two. The harness now reads such a tail at its own pace
  (busyWindowProof), and the proof judges the whole block: on that run's
  own frames the earlier proof fails it and this one passes.

No bound changes.
… (architecture:public-component-api INV23)

Main's lint rule now keeps every private data attribute in its owner module.
This change had moved `data-astryx-haptic-switch` from the haptics module to
utils/hapticSwitch.ts, so the click guard could skip the hidden switch
without importing haptics (architecture:react-component-runtime INV13). The
name now lives in privateAttributes/hapticSwitch.stylex.ts, modelled on
segmentDisabled.stylex.ts: it exports markHapticSwitch, which the haptics
module uses on its hidden switch, and isHapticSwitch, which the click guard
reads. utils/hapticSwitch.ts and the deprecated gestures/hapticSwitch.ts
re-export go, the haptics module's baseline entry goes with its raw name,
and the WebKit interaction job's scope covers the new module. No behavior
changes.
…y the gesture host and the selection lock

The selection lock and the gesture host each kept a copy of two helpers:
the iOS WebKit engine check, and the walk that asks whether a node is
inside an element along the flat tree. The engine check now lives in
utils/touchQueries.ts, the React-free half of touchPresentation.ts, as
IOS_SUPPORTS_QUERY with one isIosWebKit(view) reader; touchPresentation.ts
re-exports the query, so its export is unchanged. The walk is
containsAlongFlatTree in utils/flatTree.ts, beside parentAcrossShadow.
Unit cases cover both in their new homes. No behavior changes.
No Core or Lab component attaches a gesture recognizer yet, so the
input-stream changeset's fix entry told consumers about a change they
cannot meet. It now keeps `[fix]` for what ships, touch press feedback
hearing scrolls and page hides on the window's capture phase, and a new
`[chore] Internal:` changeset carries the recognizers' shared stream and
its passive-unless-cancelled listeners.
…trl-wheel (spec:AST-094 FR43)

meta#8676's reviewed change, rebuilt as one commit on meta#8360's head: a wheel
gesture drives the recognizers that allow its scroll type as a contact
moving with the content, a pan along its own axis once every scroller
between it and the page is at its edge, and a ctrl-wheel drives a pinch.
A screen-edge pan stays a finger's.
…ain input reads as a mouse wheel, and a pan can take a swipe as one step (spec:AST-094 FR43)

The round-4 trackpad behaviors, as a draft for the owner to try each one:

- wheelSegmenter.ts splits the wheel stream into gestures and reads a
  trackpad's lift: once a run of shrinking deltas has lost a fifth of its
  size, momentum began. A pan the host follows ends there at the fingers'
  travel, released at their last move with their speed, and the momentum
  after it never moves the pan. Fingers that stop release still, 180 ms
  (vertical) or 220 ms (horizontal) after their last wheel.
- Input the deltas cannot place reads as a mouse wheel: a delta a hair off a
  whole pixel, a scroll with no legacy wheelDelta (an engine that sends
  none), a legacy wheelDelta that is not three times the delta, or a 120
  tick over a 40 px delta.
- A pan created with wheelStep takes a trackpad swipe as one step: a short
  flick once the swipe travels 24 px, a slow swipe's travel carried across
  pauses, its slow tail absorbed, and one undo back to rest.

A mouse wheel's gesture and a pinch keep their 100 ms end. In Chromium the harness's wheels arrive as notches (wheelDeltaY -120 whatever the delta), which now read as a mouse wheel, so the two trackpad-only browser cases run in WebKit, whose driver sends a trackpad's precise deltas. The lift's
release is stamped at the fingers' last move: stamped at the lift, three
held wheels later, it read as a stop and lost the fling.
…speed until the splitter decides, so it never stalls before a lift (spec:AST-094 FR43)

The splitter reads a lift only after four shrinking deltas, so a pan that
held them stood still for that stretch (about 64 to 130 ms) and then let go
at the fingers' speed: a stop, then a go. Through the undecided stretch the
pan now moves on from where it last was at the fingers' last speed, never
past what that speed projects for the time since. At the lift it keeps that
travel and lets go there, still moving at that speed, and nothing more
moves. When the fingers carry on, their moves absorb what the stretch showed
past their travel or short of it, half a move at a time, so the pan never
steps back and ends on their travel; fingers that stop without a lift end on
their travel where it runs ahead.
…t, and never a mouse wheel (spec:AST-094 FR43, DEC-24)

The merged record decides it: a trackpad's two-finger scroll is direct
manipulation, so a pan follows it where its record allows it; a mouse
wheel's notches are a scrolling control, so a pan takes none, and a scroll
without the trackpad signature, Shift+wheel included, counts as a wheel's.

- A pan's allowedScrollTypes takes only 'continuous', and none by default,
  as UIKit's allowedScrollTypesMask: the sheet, the pager and swipe rows opt
  in. The host never feeds a wheel's notches to a pan, whatever a caller
  passes. ScrollType keeps 'discrete' for the pinch, which takes a
  ctrl-wheel from a trackpad or a mouse.
- A scroll with Shift held reads as a mouse wheel's.
- wheelStep goes: DEC-24 rejects a discrete step per trackpad swipe for rows
  and paging, and nothing used it.
- The splitter's verification streams: ten swipes that each start in the last
  one's momentum, ten whose momentum runs out, and a slow drag with 150 ms
  pauses count ten, ten and one gestures.
The public animator (spec:AST-107 FR3, FR6): createAnimator(element) over
the internal animator, with animate, decay, set, get, getVelocity, stop and
destroy, from the package root; and useAnimator(ref), which takes the
element's ref like the gesture hooks, so a gesture hook can share the
element, and returns the animator. After every commit the hook follows the
element behind the ref. The internal factory is renamed
createMotionAnimator, so no internal factory shares a public factory's
name. Docs, a changeset, and playing-cards templates in React and plain
JavaScript, with their registrations.
…s (spec:AST-107 FR1, FR6)

The public pan: `createPanGesture(element, {axis?, onPan}) → {destroy}` and
`usePanGesture(ref, {axis?, onPan})` over it, reporting
`{state, translation, velocity}` as a drag begins, changes, ends or is
cancelled, under a finger, a mouse and a pen; the translation leaves out the
travel the drag needed to begin, so an element that follows it does not
jump. Docs pages for both, with a playing-card example in plain JavaScript
and in React, the route lock and the sandbox's routes.
…element on snap points

`createDragToSnapPoints(element, {axis, snapPoints, flickThreshold?,
onSettle}) → {settleTo, destroy}` and `useDragToSnapPoints(ref, …)` over it
(spec:AST-107 FR2, FR6): a drag moves the element along its axis under every
pointer, and the release settles it where Core's one release policy says
(the nearest point; the next one past the flick speed, 1008 px/s by default;
back to where the drag started when the release moves back toward it), on
the settle spring from the release velocity. Under Reduce Motion a touch
release cross-fades to its point and any other lands at once. Docs pages and
playing-card examples for both, the route lock and the sandbox's routes.
…n an input event's own task

A caller handling an input event (a key, a click, a pointer, a touch) runs
in that event's own task, never in a frame callback, so the first frame
whose callbacks run after its call is the one that commits the call's
animations, however long the task ran. The animator cannot tell that from
a call a page's own frame callback makes before a busy task, whose browser
start is right, so the caller says so with the internal option
`start: 'next-frame'`; a call from rest made with it starts at that frame,
and a later listener of the same event that holds the main thread delays
the motion instead of letting it skip into its curve.

- Chromium, which tells the page the start a frame or more after the
  compositor takes it: such a call starts at its first frame, as a call
  whose first frame comes within LATE_START_MS already does, and also when
  an unrelated task delayed that frame.
- WebKit, which dates the start at its last frame time before the task: the
  task's end is read at the call's first read, not at its first microtask,
  which runs before a later listener's block, so the dated start moves past
  the block. The first read stands for the task's end, so a first frame
  that merely comes late (more than LATE_START_MS after the call, with no
  block) moves the dated start to that frame too: nothing was painted
  before it, so the motion is delayed by that slow frame instead of
  skipping into its curve.

A call without the option, a call with an explicit start, and a call that
continues motion keep the rule they had. Internal only: the public animator
does not take it.
…al options)

The public pan and snap-point drag keep their behavior (spec:AST-107 FR1,
FR2) and now run on internal factories a Core component can use for a job
the public layer does not expose (spec:AST-107 FR7):

- createPan, under createPanGesture: the recognizer's own options (which
  pointers and scrolls start the drag, its claim distance, a begin gate, a
  selection lock), and what drove the drag and its travel from where the
  touch began in each report.
- createSnapDrag, under createDragToSnapPoints: a model of where a drag
  draws its element (one for one, a spring, or nothing yet inside a dead
  zone, the first target catching a settle in flight) and where a release
  or a cancel lands; a custom property to draw into; settleTo with a
  spring, a velocity and a touch release, returning the settle's motion.
- The animator draws x into a registered custom property (a length or a
  number), on the main thread, for a component whose parts all read one
  value.

architecture:react-component-runtime INV13 names these jobs among the known
public-API gaps.
…set, and a companion follows its travel (internal)

Two more internal options on the shared drag path, for a component whose
own work is timed by its drag (spec:AST-107 FR7):

- settleTo({onPass: {offset, call}}) calls back once, on the first
  animation frame whose presented value, read on the settle's own curve at
  that frame's time, has passed the offset in the settle's direction (on
  the next frame when it is there already). No timer: a settle that lands
  short, that another settle or a drag replaces, or that a destroy ends
  never calls back; a crossing during a drag is the caller's to read from
  the drag.
- The animator's custom-property channel takes a companion: a second
  custom property drawn from the travel by a pure mapping, written in the
  same keyframes and writes, so it has one start and one curve with the
  travel, follows every retarget, and takes whatever Reduce Motion does to
  the travel.
…llows the fingers (spec:AST-094 FR43, DEC-24; spec:AST-057)

A row's swipe runs the one shared path every followed pan runs: the
trackpad splitter, the pan, the snap-point drag and the animator. The row
declares its stops (rest, the open width, and out past its edge for a full
swipe) and its actions; the drag draws the row's travel into one custom
property that every part of the panel reads (spec:AST-057 FR2).

- A trackpad's swipe moves the row one for one with the fingers from its
  first event past the pan's 8 px start, and its release settles to the
  nearest stop by position and velocity (Core's one release policy) on the
  shared settle spring, from the fingers' speed at their last move, so the
  row never stops before it settles. Within one gesture an open row only
  closes, never passing rest to its other side.
- A finger's or a pen's drag stays on spec:AST-057's UIKit model: UIKit's
  25 px dead zone, its tracking spring, its release rule and settle spring
  from the row's own speed.
- A full swipe confirms by position only and fires the outermost action at
  any speed; speed alone never fires an entry (spec:AST-057 FR5), so a fast
  flick from an open row settles open.
- A mouse drag and a mouse wheel's notches never swipe a row; a tap or a
  click on an open row's content closes it and reaches nothing.

Interaction proofs: trackpad cases in WebKit (Chromium's synthetic wheels
read as a mouse wheel's), a mouse wheel's notch in Chromium, fingers by
real touch in both engines; the swipe actions' UIKit constants and their
measurements (UIKit lab, iOS 26.2 simulator, Oct 23 2026).
…s at the frame after it

The row's settles asked for in an input event's own task pass the
animator's start: 'next-frame' through the shared drag (settleTo's start):
Escape, a press outside the row, the dismissing tap on its content, and an
entry's activation. Each then starts at the first frame after that task, so
a later listener of the same event that holds the main thread delays the
close instead of letting it skip into its curve. A scroll's close and every
settle a drag's release starts keep the default.
…he touch bundle guard counts what an import ships at once

architecture:react-component-runtime INV13: a gesture that a prop turns on
(a row's swipe actions, spec:AST-057) lives in a layer that the component
loads with import() the first time the prop is set; the layer imports the
touch modules statically, so a page that never sets the prop ships none of
them.

The bundle guard follows: it builds the public imports with every import()
left out of the build, so each output holds what its import ships at once;
it names each prop-gated gesture layer that brings the gesture code later
(the root built with code splitting); and it pins that a layer an import
loads later is not counted while a static import still is.

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

Approval waits on an engineering owner's decision on three spec changes this branch carries without current authority: a mouse or pen press on an open row's content closing it (spec:AST-057 FR7 and AR3 name only a touch tap), the architecture:react-component-runtime INV13 additions, and the new spec:AST-094 FR11 and FR19 rules. An engineering owner records that decision by commenting /approve-spec eb3446362c7c4e197b483e1d20e580aff152e54c (the author may, as an engineering owner) or rules otherwise. These changes are needed either way:

  • Land or retarget the six dependency pull requests so this diff is only the row change, and split the own 8,203 lines where they already divide.
  • The row's settle now animates a custom property on the main thread, so it stalls during a busy main thread, which spec:AST-094 FR5 forbids.
  • The open row's own document pointerdown and scroll listeners should go through subscribeInput from the open transition (useSwipeAction.ts:933-981).
  • Add the physical-device check spec:AST-057 asks for, reword .changeset/item-swipe-gesture-system.md (a finger already swiped on any device), and say “on a press elsewhere” in Item.tsx and Item.doc.mjs.

[Automated review]

// UIKit's dismissal does: the tap recognizes at the lift, and the host
// keeps the click it would have made from the row (it cancels the touch,
// or where the primary pointer is fine its click guard swallows the
// click). Any pointer's press dismisses, since a trackpad opens a row too

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spec hold: with no pointer gate, a mouse or pen press on an open row's own content closes it and its click is swallowed. spec:AST-057 FR7 on main closes an open row on “a touch tap on its own content”, and AR3 says the same, so this waits on an engineering owner's decision.

settle(side, restTravel, 'resting');
return;
};
document.addEventListener('pointerdown', onOutside, 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.

These document listeners sit beside subscribeInput (utils/inputStream.ts), which this branch makes the one entry for document-wide input. The work is caused by opening, which setSwipeState already sees. Subscribing there on open and unsubscribing on close would remove this Effect; its dependencies also leave out state.landing, which isOpen() reads.

* element whose parts all read one value (a row's swipe travel,
* `spec:AST-057` FR2): a `<length>` in px, or a plain `<number>`. The
* property is registered as inherited so it interpolates, and it animates
* on the main thread, not the compositor; every other channel renders as

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 row's settle drawn into this custom property no longer advances through a main-thread block, which spec:AST-094 FR5 requires of every release; main settled the row with a compositor transform transition.

'@astryxdesign/core': patch
---

[fix] Swipe actions on `Item` and `ListItem` run on the gesture system. A row claims a sideways touch at 8 px, before a browser commits to scrolling it. Its swipe begins at UIKit's 25 px, from 0, so a short wobble no longer moves the row. A finger or a pen swipes the row whatever the primary pointer, a touchscreen laptop's included, and a mouse drag never does. A second finger that lands before the claim leaves the touch to the browser's pinch; once the row has claimed its finger, a second finger no longer cancels the swipe. The click a browser synthesizes after a swipe is cancelled at its source instead of being swallowed by a 400 ms timer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On main a finger already swiped the row whatever the primary pointer: the gesture gated only on pointerType === 'touch', and the layer rendered for every pointer. As a [fix] line, “a touchscreen laptop's included” tells release-note readers it was broken. “A pen now swipes the row as a finger does” would match what changed.

* entry must also be reachable through the row's own content (a menu or a
* button in `endContent`).
* button in `endContent`). An open row closes on a tap on its own content,
* which activates nothing and paints no press, on a touch elsewhere, when

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Any pointer landing outside closes an open row (useSwipeAction.ts onOutside), as spec:AST-057 FR7 says. “on a press elsewhere” would match, here and in Item.doc.mjs.

* Item.tsx when `swipeActions` is set, so a row without them never loads
* the gesture. Tested through Item.test.tsx and Item.source-build.test.mjs.
* through which the root reaches the gesture's key handler
* @position Internal to Item, imported by Item.tsx with the row (no lazy

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Item.tsx still loads this layer with lazy(() => import('./ItemSwipeLayer')), the rule the INV13 amendment and the bundle guard pin, so “no lazy chunk” is the reverse of what ships. The swipeMotion.ts named at lines 36, 90 and 92 (and Item.tsx:352) does not exist.

This branch was successfully deployed

1 active deployment
Preview — eb344636 Deployed Nov 1, 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.

1 participant