Repository navigation
feat(core): pan gesture recognizer with UIKit's hysteresis and velocity - #8060
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.
Changes requested: the pan and the velocity tracker now follow the current spec:AST-094, spec:AST-100 and architecture:interaction-modality INV10, including their per-group table imports, but two items remain before this can land.
- The compare view is 5,197 lines because it carries older copies of #8058 and #8059; merging
main, which now has #8058, and landing #8059 first leaves this pull request at its 1,467 lines of pan and velocity work. - The description still lists
claimDistance, its tests and an earlier size, none of which this head has.
The FR17 and FR19 exception question in this thread is for an engineering owner to answer for a later push; this head follows the current text.
[Automated review]
# Conflicts: # packages/core/src/gestures/GestureArena.ts # packages/core/src/gestures/GestureRecognizer.ts # packages/core/src/gestures/types.ts
# Conflicts: # packages/core/src/gestures/LongPressGestureRecognizer.ts # packages/core/src/gestures/TapGestureRecognizer.ts # packages/core/src/motion/MEASUREMENTS.md
…R17, FR19) cixzhang's specs ruling on meta#8060: a pan starts at 10 pt and nothing allows an earlier committed claim. The opt-in early claim this PR kept goes with it: the claimDistance option and its 'early' table distance (pan.earlyClaimDistance), the host's cancelled moves before a pan begins and the committed claim that kept a reversal or a second finger from failing it. The README's early-claim section becomes that rule, the migration guide no longer points a banner at it, and the changeset drops it (and a stale 'no component uses it yet'). Tests: a one-way pan is possible with its touch unclaimed at 9 pt and begins and claims at 10 pt (from her meta#8060); the early-claim tests go, and the browser fixture's overlay card claims when it begins, keeping its direction from the page in Chromium as before.
There was a problem hiding this comment.
Recommended for approval: the pan recognizer and velocity tracker follow the current spec:AST-094 (FR10, FR14, FR17–FR19), spec:AST-100 FR2 and architecture:interaction-modality INV10, the compare view now holds only this PR's 1,467 lines, and the description matches the code. For the components that adopt it, a one-finger drag needs maximumNumberOfTouches: 1, because at the unbounded default a second finger moves a claimed drag and cancels it if the browser takes that finger; the FR17/FR19 exception question for Mobile Safari stays with the engineering owners. Optional cleanup: the unused scrollProbe import in PanGestureRecognizer.test.ts and the stale "handoff" case listed in the arenaContinuous.test.ts header.
[Automated review]
| super(options); | ||
| this.minimumNumberOfTouches = options.minimumNumberOfTouches ?? 1; | ||
| this.maximumNumberOfTouches = | ||
| options.maximumNumberOfTouches ?? Number.POSITIVE_INFINITY; |
There was a problem hiding this comment.
FR10 note for adopters: at this unbounded default (UIKit's), a second finger that lands on a begun pan joins its centroid, so moving it 40 px moves translation() 20 px, and if the browser takes that unclaimed finger the arena cancels the pan. A one-finger presentation needs maximumNumberOfTouches: 1, as the FR10 tests set; a default of 1 would keep spec:AST-094 FR10 ("A second finger MUST NOT cancel or redirect a drag a recognizer has claimed") without each adopter remembering it.
| import {describe, expect, it} from 'vitest'; | ||
| import {UIKIT_GESTURES} from '../../motion/uikit/gestures'; | ||
| import {PanGestureRecognizer} from '../PanGestureRecognizer'; | ||
| import {createHarness, scrollProbe} from './harness'; |
There was a problem hiding this comment.
scrollProbe is imported but unused in this file (an ESLint @typescript-eslint/no-unused-vars warning).
| * @input GestureArena with pans, long presses, taps, consumers and scroll | ||
| * probes, driven by synthetic touches with explicit timestamps | ||
| * @output The pan's conflicts: native scrolling (failing, scroll edge, | ||
| * handoff), long press vs pan, nested pans, delegate hooks, two fingers on |
There was a problem hiding this comment.
No handoff case remains in this file since scrollHandoff was removed, so the header can drop it.
meta#8060's final head (99b47ce) is already in this branch through meta#8072, and main's VelocityTracker.ts, PanGestureRecognizer.test.ts and types.ts each equal that head's copy, so their add/adds resolve to this branch's side (that head plus the chain's later changes). # Conflicts: # packages/core/src/gestures/VelocityTracker.ts # packages/core/src/gestures/__tests__/PanGestureRecognizer.test.ts # packages/core/src/gestures/types.ts
meta#8060's final head is already in this branch, and main's copies of its files equal that head's, so the pan changeset, VelocityTracker, the pan recognizer test and the gesture types keep this branch's copies.
…meta#8060) into the disabled-tracker fix
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.
meta#8060 has landed (squashed). Three add/add conflicts with this branch's copy of it resolve to this branch's version, which is main's plus its own deltas: VelocityTracker's isMotion, the pan test without an unused import, and NativeScrollProbe without the handoff-only distanceToEdge. This branch's diff is now its own change over main.
No conflicts. Brings main with meta#8060 landed.
Depends on #8059 (#8053 and #8058 are merged); review the commits after #8059 that are not merges.
Primary intent
Add the pan recognizer, reading translation and velocity as UIKit's
UIPanGestureRecognizerdoes, with the velocity estimator a release hands to a spring.User or maintainer impact
Drags differ most from iOS: release speeds, slops, jumps. The pan gives every component UIKit's numbers: it begins past 10 pt on either axis, its translation leaves that threshold out, and
velocity()is UIKit's estimate.Problem and solution fit
Each component measured its own drag: a flick that dismisses one surface snaps another back, and springs seeded from another velocity cannot match UIKit's.
Deltas:
PanGestureRecognizer:axisRatiosets how far off its axis a drag still counts, decided at the 10 pt begin;scrollEdge(begin only where the content under the touch cannot scroll that way): who gets a drag is decided once, when it begins (spec:AST-094FR14);translation(),totalTranslation(),velocity(), setters.VelocityTracker: UIKit's blend,scrollVelocity()for scrolling surfaces, zero after a 40 ms rest.Authority
spec:AST-100FR1, FR2 andarchitecture:interaction-modalityINV10 for velocity;spec:AST-094FR10, FR14, FR17, FR19 for claims. Thresholds come frommotion/uikit/gestures.ts, each measured source dated (FR2).Change and evidence
Read from UIKit's recognizers (iOS 26.2 simulator, HID touches, state log):
translation(in:)reads 0 when a slow drag begins; an overshooting step keeps the overshoot;velocity(in:)is 0.2 × the newest event's instantaneous velocity + 0.8 × the previous one's, reproducing all 69 values UIKit reported on six recorded streams. UIKit's never decays; ours reads 0 after a 40 ms rest (FR2). The tests replay two streams against UIKit's numbers.scrollViewWillEndDragging(INV10).Not modelled: some fast first moves read 0 at
.beganin UIKit.Scope
Testing
At 7f712c4:
pnpm exec vitest run --project ui packages/core/src/gestures packages/core/src/motion: 245 tests passed. Failing first: the four picker cases with the weight at 0.2 instead of 0.25. A case pins FR14: a drag begun as a scroll never reaches the sheet; another pins the 10 pt decision with anaxisRatioabove 1.tsc --project packages/core/tsconfig.json --noEmit: clean.ASTRYX_STRICT_LINT=1 eslinton the changed files: 0 errors. The CI duration guard passes (23/23).pnpm check:changesets: clean.Stack (merge in this order): #8059 tap and long press → #8060 pan → #8061 other recognizers → #8055 animator → #8056 animator curves; the gesture DOM host, press tracking and browser proof follow the animator.