Skip to content

fix(gestures): a gesture that cancels itself on a finger's move keeps that finger until it lifts (spec:AST-094 FR14) - #8641

Closed
vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/f2-arena-cancel-keeps-moving-touch
Closed

vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/f2-arena-cancel-keeps-moving-touch

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

User impact

On a phone, a gesture that claims its finger and then cancels itself on one of that finger's moves hands the rest of the drag to the page. The visible case: a touch ContextMenu trigger held past its long press's commit, then dragged past the press's tolerance before the menu is due. The press cancels itself and the page scrolls with the finger, a drag that changes hands midway.

Problem and solution fit

  • Why does it happen? A recognition that claimed its touches and is then cancelled gives them back to the browser unless another recognition owns them, so that a finger still resting there can pinch-zoom the page.
  • Why does that hurt? A touch no recognizer owns keeps its moves (spec:AST-094 FR19), so the host stops cancelling that finger's touchmove, and a finger that is dragging becomes the page's scroll.
  • Why does it matter? FR14: who gets a drag is decided once, when it begins. UIKit's scroll view never takes a touch another recognizer had.

The fix keeps the release for a resting finger and stops it for a moving one.

Expected behavior and authority

spec:AST-094 FR14 (current). The case was raised on #8058 as a question (issuecomment-6408527888): should a gesture that cancels itself on a finger's move keep that finger claimed until it lifts, with the release kept for a resting finger?

Smallest restoration

GestureArena: a cancel during a finger's move keeps that finger claimed until it lifts. A resting finger is still given back when its gesture is cancelled (the pinch case), and a waiting recognition that fails still gives its claim back. One arena change, its test and a changeset.

Evidence

  • Reproduction before the change: the touch ContextMenu's browser test a drag that cancels the press after the commit does not scroll the page either (a hold past the commit, then a 200 px drag) scrolls the page 190 px in Chromium on every run.
  • Result after the change: the same test passes 5 of 5 runs.
  • Representative unchanged path: a resting finger is still given back on a cancel, and a waiting recognition that fails still releases its claim; the gestures suite passes.
  • Regression test: the new arena.test.ts case fails on main (the dragging finger reads unclaimed right after the cancel) and passes with the fix.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm exec vitest run --project ui packages/core/src/gestures: 113 passed on main bb24260 with this commit; the new arena case fails without it.
  • tsc --project packages/core/tsconfig.json --noEmit, ASTRYX_STRICT_LINT=1 eslint packages/core/src/gestures/GestureArena.ts packages/core/src/gestures/__tests__/arena.test.ts, pnpm check:changesets: clean.

… that finger until it lifts (spec:AST-094 FR14)

A recognition that claimed its touches and was then cancelled gave them
back to the browser unless another recognition owned them, so a finger
resting there could pinch-zoom the page. A gesture that cancels itself on
a finger's move, a long press dragged past its tolerance after it began,
gave back a finger that was dragging: the host stopped cancelling its moves
and the page scrolled the rest of the drag, a hand change FR14 forbids and
UIKit never makes (its scroll view never takes a touch another recognizer
had). A cancel during a finger's move now keeps that finger claimed until
it lifts; a resting finger is given back as before, and a waiting
recognition that fails still gives its claim back.

The new arena case fails on the previous code: the dragging finger reads
unclaimed right after the cancel.
@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 Oct 27, 2026 3:12am 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
🟢 Reviewed (for maintainers only) Oct 27, 2026, 3:55 AM UTC

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

Recommending approval: keeping a finger claimed until it lifts when its gesture is cancelled during that finger's move follows spec:AST-094 FR14, and the resting-finger and waiting-recognition releases stay as they were. The new arena test fails without the change and passes with it, and the gestures suite, core typecheck, lint and changeset check pass. Optionally, the releaseClaim comment could describe the rule as a gesture cancelled during a finger's move: a cancel() made from a listener during the move keeps the moving finger too, while the comment still lists a gesture the app cancels among those that give their touches back.

[Automated review]

* cancels) gives them back to the browser unless another recognition owns
* them, so a finger still resting there can pinch-zoom the page.
* them, so a finger still resting there can pinch-zoom the page. A gesture
* that cancels itself on a finger's move (a long press past its tolerance)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional wording: the keep follows the move being delivered, not who cancels, so a cancel() made from a changed listener during the move keeps the moving finger too, while line 550 still lists a gesture "the app cancels" among those that give their touches back. "A gesture cancelled during a finger's move" would match the code.

@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

No new or modified components detected.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.3KB 34.3KB 0B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…er's move keeps that finger until it lifts) into the experimental touch exports
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…ouch decides at 15 px on an element within the band and at 10 px beyond it, DEC-22; meta#8641 bec2283) into meta#8270
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…8641's moving-touch claim kept on this line's internals delivery) into meta#8315

# Conflicts:
#	packages/core/src/gestures/GestureArena.ts
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…8589's watcher test reads its end; the gestures-table gate retired) into meta#8360
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
's base

fZ carries meta#8076 e6ab0b9 (DEC-22's 10 px rule and meta#8641's arena fix),
the meta#8589 watcher-test fix and the meta#8358 gate retirement; taken at this
stack's real push (LANES 01:13).
@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 30, 2026
…or, and Lab's route through core's exports

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles, paint and hooks; the animator, which starts a motion from rest at
its commit, holds the value on screen inline while a curve runs, and lets a
call wait for a presented frame; and Lab reaching Core only through its
exports, internal entries included. It absorbs meta#8073 (closed: the press
tracking this rests on), meta#8562 (the internal entries) and meta#8340 (the
animator's start and hold fixes), with the held dip's load fixes, and keeps
meta#8641's rule: a gesture that cancels itself while its finger moves keeps
that finger until it lifts.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…or, and Lab's route through core's exports

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks; the animator, which starts a motion from rest at
its commit, holds the value on screen inline while a curve runs, and lets a
call wait for a presented frame; and Lab reaching Core only through its
exports, internal entries included. It absorbs meta#8073 (closed: the press
tracking this rests on), meta#8562 (the internal entries) and meta#8340 (the
animator's start and hold fixes), with the held dip's load fixes, and keeps
meta#8641's rule: a gesture that cancels itself while its finger moves keeps
that finger until it lifts. Press tracking is built by createPressTracker
(from meta#8763), and a gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…mator

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks, built by createPressTracker; and the animator, which
starts a motion from rest at its commit, holds the value on screen inline
while a curve runs, and lets a call wait for a presented frame. It absorbs
meta#8073 (closed: the press tracking this rests on) and meta#8340 (the animator's
start and hold fixes), with the held dip's load fixes, and keeps meta#8641's
rule: a gesture that cancels itself while its finger moves keeps that
finger until it lifts. A gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…mator

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks, built by createPressTracker; and the animator, which
starts a motion from rest at its commit, holds the value on screen inline
while a curve runs, and lets a call wait for a presented frame. It absorbs
meta#8073 (closed: the press tracking this rests on) and meta#8340 (the animator's
start and hold fixes), with the held dip's load fixes, and keeps meta#8641's
rule: a gesture that cancels itself while its finger moves keeps that
finger until it lifts. A gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…mator

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks, built by createPressTracker; and the animator, which
starts a motion from rest at its commit, holds the value on screen inline
while a curve runs, and lets a call wait for a presented frame. It absorbs
meta#8073 (closed: the press tracking this rests on) and meta#8340 (the animator's
start and hold fixes), with the held dip's load fixes, and keeps meta#8641's
rule: a gesture that cancels itself while its finger moves keeps that
finger until it lifts. A gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…mator

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks, built by createPressTracker; and the animator, which
starts a motion from rest at its commit, holds the value on screen inline
while a curve runs, and lets a call wait for a presented frame. It absorbs
meta#8073 (closed: the press tracking this rests on) and meta#8340 (the animator's
start and hold fixes), with the held dip's load fixes, and keeps meta#8641's
rule: a gesture that cancels itself while its finger moves keeps that
finger until it lifts. A gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…mator

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks, built by createPressTracker; and the animator, which
starts a motion from rest at its commit, holds the value on screen inline
while a curve runs, and lets a call wait for a presented frame. It absorbs
meta#8073 (closed: the press tracking this rests on) and meta#8340 (the animator's
start and hold fixes), with the held dip's load fixes, and keeps meta#8641's
rule: a gesture that cancels itself while its finger moves keeps that
finger until it lifts. A gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…mator

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks, built by createPressTracker; and the animator, which
starts a motion from rest at its commit, holds the value on screen inline
while a curve runs, and lets a call wait for a presented frame. It absorbs
meta#8073 (closed: the press tracking this rests on) and meta#8340 (the animator's
start and hold fixes), with the held dip's load fixes, and keeps meta#8641's
rule: a gesture that cancels itself while its finger moves keeps that
finger until it lifts. A gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…mator

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks, built by createPressTracker; and the animator, which
starts a motion from rest at its commit, holds the value on screen inline
while a curve runs, and lets a call wait for a presented frame. It absorbs
meta#8073 (closed: the press tracking this rests on) and meta#8340 (the animator's
start and hold fixes), with the held dip's load fixes, and keeps meta#8641's
rule: a gesture that cancels itself while its finger moves keeps that
finger until it lifts. A gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
…mator

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks, built by createPressTracker; and the animator, which
starts a motion from rest at its commit, holds the value on screen inline
while a curve runs, and lets a call wait for a presented frame. It absorbs
meta#8073 (closed: the press tracking this rests on) and meta#8340 (the animator's
start and hold fixes), with the held dip's load fixes, and keeps meta#8641's
rule: a gesture that cancels itself while its finger moves keeps that
finger until it lifts. A gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
…mator

Core's internal touch foundation on the gesture recognizers (meta#8061) and the
gesture host (meta#8071 + meta#8072): UIControl press tracking with its press
profiles and hooks, built by createPressTracker; and the animator, which
starts a motion from rest at its commit, holds the value on screen inline
while a curve runs, and lets a call wait for a presented frame. It absorbs
meta#8073 (closed: the press tracking this rests on) and meta#8340 (the animator's
start and hold fixes), with the held dip's load fixes, and keeps meta#8641's
rule: a gesture that cancels itself while its finger moves keeps that
finger until it lifts. A gesture that ends on an unmount or a replaced node
calls no listener from the effect.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
cixzhang pushed a commit that referenced this pull request Oct 31, 2026
…mator (#8076)

Behavior-module PR: internal touch foundation, animator, press tracking

Stacked on #8071, one commit on `main`, with no merge commits: review only this pull request's two commits, vjeux@0ddfa8937e and its test vjeux@91d49e1438.

## Why

Touch components need Core's animator, press tracking and gesture host without making them public.

## What it absorbs, and why

One solution, the internal touch foundation, so these close with a link here:

- #8073 (closed): UIControl press tracking, its press profiles and the hooks. Everything else here rests on it.
- #8340: the animator's start and hold. No frame shows where a motion is going, a call can wait for a presented frame, and in Safari a start from rest moves past a block of over two frames in the task that created it. It brings the held dip's load fixes from #8360 too.
- #8641's rule (approved): a gesture that cancels itself while its finger moves keeps that finger until it lifts.

## Left out

- Nothing here rests on the undecided mouse or pen drag selection (#8665), wheel ending (#8676) or an open owner ruling.
- #8446's harness changes land with #8554.
- The StyleX `defineConsts` build fix and the stylesheet's undefined-variable check stay in #8075.
- `RotationGestureRecognizer` and `HoverGestureRecognizer` are not exported: nothing uses them, and rotation's threshold is not measured.
- `paintPress` and its held state land with the action-group tracker, `useAnimator` with Lab's SearchField, and Core's internal entry points for Lab (#8562's change) with the first Lab component that uses them: each with its first user.

## What changes

- Press tracking follows UIControl and is built by `createPressTracker`: the press clocks and profiles, `usePressTracker`, and release velocity on UIKit's filter (`spec:AST-100` FR1, FR2).
- The animator holds the value on screen inline while a curve runs, starts a motion from rest when the browser commits it, offers `start: 'presented'` and `'commit'`, additive reversals and `handle.curve`. A browser proof counts no Layout or Paint while a spring plays, on the harness's rendering trace.
- The motion modules and recognizers load in plain Node, with no StyleX module in their graph.

## How to check it

1. Run `pnpm exec vitest run scripts/motion-node-load.test.mjs`: the motion modules and recognizers load in a plain Node process.
2. In a component using `usePressTracker`, change `highlightDelay` while mounted: the next touch uses the new delay.
3. Run `pnpm test:interaction animator.interaction.browser.spec.ts` in Chromium and WebKit: a motion started right before a blocked main thread plays from its start after the block.

## Evidence

- Local gates at 91d49e1: build, type check, `pnpm test:ui` (16,704), the node suites, lint, Storybook, and the motion and gesture interaction specs in Chromium (103) and WebKit (96); the held-dip, block and no-Layout-or-Paint proofs pass six times each at eight workers.

## Credit

Co-authored-by: Cindy Zhang (her commits in #8073 and #8076).

## Scope

- [x] Unrelated fixes, cleanup, and policy changes were removed or split.
- [x] Public text and artifacts contain no internal Meta context.



Behavior-module PR: LOC split waived per maintainer rule.
@vjeux

vjeux commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Closing: carried by #8076, merged.

@vjeux vjeux closed this Oct 31, 2026
@vjeux
vjeux deleted the uikit/f2-arena-cancel-keeps-moving-touch branch October 31, 2026 07:22

This branch was successfully deployed

1 active deployment
Preview — bec2283d Deployed Oct 27, 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant