Repository navigation
feat(core): tap and long-press recognizers with UIKit's measured thresholds - #8059
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
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-101compares 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-836begins 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]
| const first = touches[0]; | ||
| if ( | ||
| this.firstTapLocation != null && | ||
| distance({x: first.startX, y: first.startY}, this.firstTapLocation) > | ||
| this.allowableSeparation | ||
| ) { | ||
| this.setState('failed'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
# Conflicts: # packages/core/src/gestures/LongPressGestureRecognizer.ts # packages/core/src/gestures/TapGestureRecognizer.ts # packages/core/src/motion/MEASUREMENTS.md
…listeners only for an enabled consumer) into UIControl press tracking
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.
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.
…n recognizer # Conflicts: # packages/core/src/motion/MEASUREMENTS.md # packages/core/src/motion/uikit/gestures.ts
…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.
# Conflicts: # packages/core/src/gestures/LongPressGestureRecognizer.ts # packages/core/src/gestures/TapGestureRecognizer.ts # packages/core/src/motion/MEASUREMENTS.md
…listeners only for an enabled consumer) into UIControl press tracking
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
Deltas:
TapGestureRecognizer:LongPressGestureRecognizer:Authority
spec:AST-094(FR15–FR17), proposed; pending owner review. Thresholds come frommotion/uikit/gestures.tswith their sources.Change and evidence
Read from UIKit's recognizers in a reference app on the iOS 26.2 simulator, confirmed with HID touches:
Mobile Safari, which runs UIKit's tap and double-tap recognizers, agrees:
The tests use explicit timestamps only.
Scope
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.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.