Skip to content

feat(core): tap and long-press recognizers with UIKit's measured thresholds - #8059

Merged
cixzhang merged 23 commits into
meta:mainfrom
vjeux:wip/uikit-pr-g1b
Oct 26, 2026
Merged

cixzhang merged 23 commits into
meta:mainfrom
vjeux:wip/uikit-pr-g1b

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8058 (and its predecessors); review the last commit.

Primary intent

Add the first two recognizers on the gesture core: tap and long press, with UIKit's measured thresholds.

User or maintainer impact

Taps, double taps and long presses are re-implemented per component today, with their own slops and timers. A long press can fire late when the main thread is busy, and a single tap can fire alongside a double tap. These recognizers give every component UIKit's numbers. A long press begins at exactly touch-down + 0.5 s, even when the callback runs late.

Problem and solution fit

  • Why does it happen? Each component keeps its own timers and slops.
  • Why does that hurt? Taps fail or fire twice, and long presses start late or at the wrong distance.
  • Why does it matter? These are the commonest gestures on a phone.

Deltas:

  • TapGestureRecognizer:
    • taps and fingers;
    • recognized at the lift;
    • fails past its allowable movement (a distance), on extra fingers, a hold past the maximum, or a late or far next tap.
  • LongPressGestureRecognizer:
    • the press duration as a deadline on the event clock;
    • the movement tolerance as a distance;
    • taps before the press;
    • a zero duration begins on the touch itself, for selection tracking over a group of actions.
  • The arena with these real recognizers: tap against double tap with and without require-to-fail, delegate requirements, delivery and deferred clicks, cancellation before and after a long press began.

Authority

spec:AST-094 (FR15–FR17), proposed; pending owner review. Thresholds come from motion/uikit/gestures.ts with their sources.

Change and evidence

Read from UIKit's recognizers in a reference app on the iOS 26.2 simulator, confirmed with HID touches:

  • tap: 45 pt of movement (Euclidean); taps 0.35 s and 45 pt apart; a 1.5 s maximum hold;
  • long press: 0.5 s and 10 pt as a distance (a 7, 7 drift begins, 7.2, 7.2 fails).

Mobile Safari, which runs UIKit's tap and double-tap recognizers, agrees:

  • a click up to 45 px of travel;
  • a double tap up to about 350 ms and 45 px apart.

The tests use explicit timestamps only.

Scope

  • This PR has one primary intent.
  • Independently removable deltas were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm exec vitest run --project ui packages/core/src/gestures packages/core/src/motion: 184 tests passed.
  • tsc --project packages/core/tsconfig.json --noEmit: clean.
  • eslint packages/core/src/gestures: clean.
  • pnpm check:changesets, node scripts/release/changeset-coverage.mjs pr --base origin/main: clean.
  • Size: 7 files, 821 lines added.

Stack (merge in this order): #8052 spec → #8053 constants table → #8058 gesture core → #8059 tap and long press → #8060 pan → #8061 other recognizers → #8054 math → #8055 animator → #8056 animator curves; the gesture DOM host, press tracking and browser proof follow the animator.

vjeux added 4 commits October 23, 2026 20:53
Springs, scroll physics, timing curves, navigation values and gesture
thresholds for the touch presentations of core components, each with its
source (Apple documentation, a WWDC session, WebKit's UIKit-matched source,
or a measurement of UIKit on the iOS 26.2 simulator) or labelled a guess
with its measurement task. A test enforces the source rules on every domain
file, in their JSDoc and typed forms, and checks the list of simulator
measurements still owed.
…rena

An internal gesture system for touch presentations, not exported from the
package. GestureRecognizer carries UIKit's states and allowed transitions,
discrete and continuous recognizers, require-to-fail, simultaneous
recognition on request, shouldBegin and shouldReceiveTouch,
cancelsTouchesInView and the touch delays. GestureArena settles each touch
sequence without the DOM: participants, resolution order, claims, delivery
to the view, click dispositions (allow, suppress, defer and replay) and
timers as deadlines on the event clock. Its rules are tested with scripted
recognizers, independent of any real one.
…sholds

Tap recognizes at the lift within 45 pt of movement, taps 0.35 s and 45 pt
apart, and a 1.5 s hold; long press begins at exactly touch-down + 0.5 s on
the event clock within 10 pt, even when the main thread was blocked, and a
zero duration begins on the touch itself. The arena tests with these real
recognizers cover tap against double tap, delegate requirements, deferred
clicks and cancellation.
@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 26, 2026 3:51am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 26, 2026, 4:03 AM UTC

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold for a spec decision: the tap and long-press recognizers, the gesture core and the UIKit constants table rest only on draft spec:AST-094, so an engineering owner needs to approve FR1, FR2, FR12 and FR15–FR17 (approving the record as written also accepts FR4, FR11 and FR14's changes to theme-tokens, user-states and AST-025) by commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner, or the change waits until #8052 lands the record. Independently of that decision, please address these points:

  • A two-finger double tap fails at its second tap because TapGestureRecognizer.ts:93-101 compares the next tap's first finger with the previous tap's last-lift position, so compare centroids (or matching fingers) and add a two-finger double-tap test.
  • After a long task, if the arena's own timer runs before a queued touchend, GestureArena.ts:831-836 begins a long press whose finger already lifted, so yield to queued input before running a late deadline and test that order.
  • The diff carries 5,065 lines because #8052, #8053 and #8058 ride along, so land them first to make this pull request its own 821-line commit, and rerun the Lint job, which aborted out of memory.

[Automated review]

Comment on lines +93 to +101
const first = touches[0];
if (
this.firstTapLocation != null &&
distance({x: first.startX, y: first.startY}, this.firstTapLocation) >
this.allowableSeparation
) {
this.setState('failed');
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A two-finger double tap fails here: touches[0] is the next tap's first finger, but firstTapLocation (line 145) is the previous tap's last-lift position, or its centroid when the fingers lift together. The same two-finger tap repeated at the same two points 100 px apart fails at the second tap's first finger. Compare the next tap's centroid once its required fingers are down with the first tap's centroid (or each finger with the nearest first-tap finger), and add a two-finger double-tap test.

Comment on lines +831 to +836
const delay = Math.max(0, earliest - this.clock.now());
this.realTimer = this.clock.setTimeout(() => {
this.realTimer = null;
this.realTimerDeadline = Number.POSITIVE_INFINITY;
this.advanceTime(Math.max(earliest, this.clock.now()));
}, delay);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After a long task the browser may run this timer task before a queued touchend. advanceTime(Math.max(earliest, this.clock.now())) then runs the long-press deadline first, so a finger that lifted at 300 ms during a 100–900 ms block still begins a press at 500 and ends it at 900 instead of failing at 300. Consider yielding once to queued input before running a late deadline, and add a test whose clock fires the timer before the queued lift.

@vjeux
vjeux deleted the wip/uikit-pr-g1b branch October 26, 2026 04:42
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
# Conflicts:
#	packages/core/src/gestures/LongPressGestureRecognizer.ts
#	packages/core/src/gestures/TapGestureRecognizer.ts
#	packages/core/src/motion/MEASUREMENTS.md
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
No conflicts. Brings main with meta#8059 landed, through meta#8060 and meta#8061.
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
…listeners only for an enabled consumer) into UIControl press tracking
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
The touch foundation's fold, mergeable against main cd4aca6: her chain
on main (meta#8058, meta#8054, meta#8059, meta#8060) and meta#8258's rework, which keeps touch
motion internal (no theme surface): the generated role table, its script and
the role kinds file are gone, and a table registers only the roles it lists.
The motion doc and its golden take meta#8076's side; the role kinds file stays
deleted.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
meta#8076's head is F1's fold on the maintainer's chain now on main (meta#8058,
meta#8054, meta#8059, meta#8060): touch motion is internal (the tables register their
values as internal motion roles, with no theme surface), and the
`isRtlElement` resolver.

Conflicts, resolved:
- The theme-roles changeset and the motion guide take meta#8076's side; their
  sentence on ChatLayout's Reduce Motion cross-fade goes with the theme
  surface.
- The CLI goldens for the empty icon set take meta#8076's side and regenerate.
- keyboardModel.test.ts reads the internal role registry (`motionRoleIn`)
  now that the generated role-kind table is gone, and no longer names a
  theme.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
…eta#8258 194aab4) into meta#8270

The long press reads its own and the tap's UIKit groups, as main's meta#8059
does, and keeps this PR's lock rule (a hold brings lockSelection; 0 ms and
null bring none); the host keeps her coarse-pointer option without the
held-selection map this PR dropped.
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…n recognizer

# Conflicts:
#	packages/core/src/motion/MEASUREMENTS.md
#	packages/core/src/motion/uikit/gestures.ts
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…hing before 10 pt

The FR14 rule for a touch the browser took is meta#8058's (the arena never claims
it, even for an observer that begins later), so this branch takes it from
there and drops its own variant.

Specs ruling on AST-094 FR17/FR19: a pan starts at 10 pt and nothing allows
an earlier committed claim. The one-way pan's 5 px early claim
(pan.earlyClaimDistance, claimDistance, claimsEarly, commitClaim) is removed:
a 5–9 pt drag leaves the touch to the browser, and a test pins it.
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…) into the pan recognizer

# Conflicts:
#	packages/core/src/motion/MEASUREMENTS.md
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
No conflicts. Brings meta#8059's taps-phase measurement (M-G20) and FR10 test
through meta#8060.
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
No conflicts. Brings meta#8059's M-G20 measurement and FR10 test through meta#8060
and meta#8061.
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…recognizer

# Conflicts:
#	packages/core/src/motion/uikit/gestures.ts
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
No conflicts. Brings main's per-group UIKit table (meta#8468) through meta#8058
and meta#8059, with meta#8060's velocity rows inside UIKIT_VELOCITY.
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
# Conflicts:
#	packages/core/src/gestures/LongPressGestureRecognizer.ts
#	packages/core/src/gestures/TapGestureRecognizer.ts
#	packages/core/src/motion/MEASUREMENTS.md
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
No conflicts. Brings main with meta#8059 landed, through meta#8060 and meta#8061.
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…eta#8258 194aab4) into meta#8270

The long press reads its own and the tap's UIKit groups, as main's meta#8059
does, and keeps this PR's lock rule (a hold brings lockSelection; 0 ms and
null bring none); the host keeps her coarse-pointer option without the
held-selection map this PR dropped.
vjeux pushed a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…listeners only for an enabled consumer) into UIControl press tracking

This branch was successfully deployed

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

2 participants