Repository navigation
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.
|
|
An offer for one changeset, on the fork branch |
meta#8117 brings meta#8082 b9a18f5: meta#8076 4adaf8b with meta#8073's one press writer, and the list's internal constants (registerListTable is gone). pressFeedback.ts writes through the one writer: the controller's paint lands only where a press may replace it, so a held row keeps its paint, and a press's end clears its input marker as before. motion/uikit/index.ts exports the button's and the list's values; build-css keeps this branch's Reduce Motion cases beside meta#8082's row-layer describe.
meta#8082 brings meta#8076 4adaf8b with meta#8073's one press writer, and meta#8077's internal constants: the row press layer lives in rowPressLayer.ts and registerListTable is gone. pressFeedback.ts keeps this branch's profiles and slots and writes through the one writer (writePress, which keeps a held row, so a press's end clears only its own paint); the release timer plays the profile's clock, capped under reduced motion as meta#8082's is. Item and TreeListItem import the row layer beside this branch's profiles; build-css and motion/uikit/index.ts take meta#8082's.
…egan-elsewhere; a recognizer's own scroll is not the browser taking the finger From CodeBunny on meta#8195, fixed once here where the host enters: - spec:AST-094 FR28: a touch with no recognizer or consumer on its path never reached the arena, so a recognizer tracking a finger was never told about a second finger beside it (a menu press could not end on one). GestureArena.untrackedBegan reports such a touch to recognizers that are tracking touches; it creates no record, claim or lock, and its moves and end stay the page's. - spec:AST-094 FR30: a recognizer that scrolls a scroller itself (a menu's edge autoscroll) was cancelled by its own write. The internal GestureHost.ownScroll(scroller) makes onScroll skip that scroller until two frames pass; a real pan in that window still reports itself through pointercancel or touchcancel. Three tests; two fail on the previous head.
…s a dependency useGestureRecognizer.ts: meta#8073's node attachment (a disabled gesture is not attached, a replaced node re-attaches), its host listener sync and the scroll-brake default, plus meta#8074's own changes from c248459: the isEnabled sync keeps the listener quiet (architecture:react-component-runtime INV2) and a change to the press tracker's four build options rebuilds it from the next touch. The tests keep both sides' cases.
…meta#8067, meta#8056 and meta#8055 fixes) into the presented start Four files both sides changed: the animator's attach flushes waiting presented calls before the element changes and leaves demotion to her later step after the landing's commit; dispose flushes them, then lands pending moves as her version does; the test's style stub takes her pseudo-element stylesheet and keeps this side's live-animation hold; the README and the browser spec's header take her wording, with this side's added proofs.
There was a problem hiding this comment.
Changes are needed before approval: the press tracking itself matches the current spec:AST-094 FR11, but the gesture host this diff carries predates the spec's DEC-8, and the diff still includes the unlanded gesture stack (6,330 authored lines, of which the press-tracking layer is 1,469). Please narrow it to the press-tracking layer once #8061, #8071 and #8072 land (or base the branch on #8072), conform those layers to the points below, and update the description, which still describes the removed delegated press, the old cell delay and a 0.25 s release.
GestureHosttreats any scroll under an unclaimed touch as the browser taking it, so a tap that moved 1 px loses its click, while FR20 counts a native scroll only after 10 px of travel.GestureHostwrites the derivedtouch-actionover the element's own, turning an authornoneorpan-yintomanipulation, while FR18 requires intersecting it with the author's value.- The one-edge
ScreenEdgePanGestureRecognizerdecides at 15 px while leaving the browser every touch behavior, against FR19's 10 px, and the host still needs real-touch browser tests (FR18 to FR22, FR41,spec:AST-013FR10) wherever it lands.
[Automated review]
| ) { | ||
| continue; | ||
| } | ||
| if (touch.target instanceof Node && scroller.contains(touch.target)) { |
There was a problem hiding this comment.
This counts any scroll of an ancestor as the browser taking every unclaimed touch inside it, whatever that finger's travel, so a tap that moved 1 px fails and its click is swallowed when its scroller fires one scroll event. Current spec:AST-094 FR20 counts a native scroll "only once that finger has moved the scroll take distance (FR2)", which is 10 px (DEC-8). Checking each touch's travel from touch-down against 10 px before taking it would conform. The test "reads a scroll above an unclaimed touch as the browser taking it" pins the old rule.
| element.style.getPropertyValue(property), | ||
| ); | ||
| } | ||
| element.style.setProperty(property, value); |
There was a problem hiding this comment.
This replaces the element's own touch-action with the value derived from its recognizers, so an author none or pan-y becomes manipulation once a tap or long press attaches. Current spec:AST-094 FR18 (DEC-8): "The derived value is intersected with the element's own author touch-action and is never wider than it". Reading the element's computed value before the first write and intersecting the tokens, keeping the author's value when it lacks pinch-zoom, would conform.
| constructor(options: ScreenEdgePanGestureRecognizerOptions = {}) { | ||
| super({ | ||
| maximumNumberOfTouches: 1, | ||
| hysteresis: UIKIT_SCREEN_EDGE_PAN.hysteresis.value, |
There was a problem hiding this comment.
With one edge, this pan leaves the browser every touch behavior (lines 63-66) but begins or fails only at 15 px: it is still possible at 10, 12 and 14 px. spec:AST-094 FR19 says "A recognizer that competes with native scrolling MUST decide within 10 px of travel". Deciding by 10 px, a touch-action that does not compete, or an owner's amendment of FR19 would resolve it. This belongs with #8061.
| expect(highlights).toEqual([true, false]); | ||
| }); | ||
|
|
||
| it('shows a quicker tap at the lift straight into the release, with no flash', () => { |
There was a problem hiding this comment.
This runs outside a scroller, where the cell profile lights at touch-down, so the quick-tap path the name describes is never reached. Passing scroll: scrollProbe(() => true, true) to down would cover the cell's quick tap inside a scroll view, which no test exercises today.
| optionsRef.current.onControlEvent?.(event, touch), | ||
| onHighlightChange: isHighlighted => | ||
| optionsRef.current.onHighlightChange?.(isHighlighted), | ||
| trackingExpansion: options.trackingExpansion, |
There was a problem hiding this comment.
trackingExpansion, highlightDelay, cancelTravel and quickTapFlash are read once, when the tracker is created, so a later value is silently ignored while the callbacks and isEnabled stay live. Reading them through optionsRef, as the callbacks are, would keep them current.
…ated consumer The gesture host has no delegated consumers and no scroll handoff, so its header drops both, and one changeset covers the press tracking, the one press writer and the hooks. Both texts are cixzhang's from c34bf73 on meta#8073, taken verbatim so the next meta#8076 merges without a conflict.
meta#8076's head is a fast-forward of the ee34b25 meta#8176 carries: meta#8073's began-elsewhere for untracked fingers and a recognizer's own scroll, and the motion roles' table values in the docs. No conflicts; `timing.ts` is meta#8076's with exactly meta#8484's change. This branch takes it ahead of meta#8176, whose next head brings it too.
meta#8076's head is a fast-forward of the ee34b25 meta#8176 carries: meta#8073's began-elsewhere for untracked fingers and a recognizer's own scroll, and the motion roles' table values in the docs. No conflicts; `timing.ts` is meta#8076's with exactly meta#8484's change. This branch takes it ahead of meta#8176, whose next head brings it too.
|
Posted by Cindy's agent on her behalf. Cindy's decision on context menus: a public, framework-agnostic For this PR, a small change: |
Main merged meta#8045 and meta#8046. The one conflict is utils/pressFeedback.ts: it takes main's merged meta#8046 text (the finger and button slots, the mouse and key press) and keeps this pull request's own additions, the one writer of the press attribute (writePress, paintPress, readPressPaint) and the header line naming them.
Main's button slot, merged with meta#8046, ends a press's release at once when the next press starts, by clearing the press attribute directly. On a row another painter holds, that clear erased the hold, which this pull request's one writer refuses. A mouse press on a held row, released, then a press elsewhere must leave the row held. Fails until the early end goes through the one writer.
finish() writes none through writePress, as this pull request's controller paints, so a held row stays its owner's when the next press ends a release early, under the finger and the button slots alike.
There was a problem hiding this comment.
Changes are needed before approval: the press tracking itself matches spec:AST-094 FR11 under a finger, but the gesture layers this diff carries no longer match the current spec, and the diff is 6,343 authored lines, 4,923 of them from #8061, #8071 and #8072. Please narrow it to the press-tracking layer once those land (or base the branch on #8072), with these fixed where they live:
GestureHostinstalls nothing unless the primary pointer is coarse, so a mouse, a pen or a touchscreen laptop gets no press tracking or gestures, while FR43 andreact-component-runtimeINV13 require the host to attach for every pointer.- A scroll under a finger that moved 1 px cancels a tap and swallows its click (FR20 waits for 10 px), and the host's
touch-actionoverrides a narrower author value (FR18). - The screen-edge pan should claim
pan-xonly in its band, decide at 10 px on a wider element and stay off under a pen (FR18, FR19, FR43). PressTrackershould be a factory with noinstanceofchecks (INV13) and import the gesture tables by group (component-style-authoringINV6).
[Automated review]
| } | ||
|
|
||
| private shouldListen(): boolean { | ||
| if (!this.isPrimaryPointerCoarse()) { |
There was a problem hiding this comment.
Under a fine primary pointer the host registers nothing, so a mouse, a pen or a touchscreen laptop gets no press tracking or gestures. Current spec:AST-094 FR43 and react-component-runtime INV13 say the host attaches for every pointer, with no touch-only query; under a fine pointer it can listen on the attached element and add document listeners only during a press (FR24).
| ) { | ||
| continue; | ||
| } | ||
| if (touch.target instanceof Node && scroller.contains(touch.target)) { |
There was a problem hiding this comment.
Any scroll of a scroller that contains an unclaimed touch counts as the browser taking it, however far the finger moved, so a tap that moved 1 px loses its click. FR20 (DEC-8) counts a native scroll only once that finger has travelled the 10 px scroll take distance.
| element.style.getPropertyValue(property), | ||
| ); | ||
| } | ||
| element.style.setProperty(property, value); |
There was a problem hiding this comment.
This writes the derived value over the element's own touch-action, so an author none or pan-y becomes manipulation. FR18 (DEC-8) intersects the derived value with the author's and keeps the author's value when it leaves out pinch-zoom.
| ); | ||
| } | ||
|
|
||
| export class PressTracker implements TouchConsumer { |
There was a problem hiding this comment.
react-component-runtime INV13 keeps classes to the recognizer family (its base, the arena and the host), and its verification fails a class outside it and an instanceof check on a recognizer (line 148). A createPressTracker(options) factory that returns a plain object, as createActionGroupTracker does, would fit.
| * swallows for a touch a recognizer took. | ||
| */ | ||
|
|
||
| import {UIKIT_GESTURES} from '../motion/uikit/gestures'; |
There was a problem hiding this comment.
UIKIT_GESTURES bundles every group's rows: a bundle of this file carries the screen-edge, rotation, swipe, click and velocity rows. component-style-authoring INV6 ships a component only the rows it reads, so importing UIKIT_PRESS, UIKIT_PAN and UIKIT_CONTROL by name (and press instead of timing) keeps press-tracked controls lean.
| * FR18). Pans from several edges keep the browser the axis along them. | ||
| */ | ||
| override get nativeTouchBehavior(): NativeTouchBehavior { | ||
| if (this.edges.length === 1) { |
There was a problem hiding this comment.
Current FR18 (DEC-13, DEC-22) has a one-edge pan claim pan-x for a touch in its band on an element within the band, and decide at FR19's 10 px on a wider element; FR43 keeps it off under a mouse, a trackpad or a pen. Here it claims nothing, always waits 15 px, and accepts every pointer type.
| optionsRef.current.onControlEvent?.(event, touch), | ||
| onHighlightChange: isHighlighted => | ||
| optionsRef.current.onHighlightChange?.(isHighlighted), | ||
| trackingExpansion: options.trackingExpansion, |
There was a problem hiding this comment.
These four options reach only the first render's tracker, so a later value (another profile, say) is silently ignored while the callbacks stay live. Reading them through optionsRef, as the callbacks are, fixes it.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: UIControl press tracking with control and cell profiles and the gesture hooks, with every press painter writing `data-astryx-press` through one internal writer in `utils/pressFeedback.ts`. Not exported from the package entry point; no component uses them yet. |
There was a problem hiding this comment.
hooks/actionGroupTracker.ts still writes data-astryx-press directly, with its own fade clock, so "every press painter ... through one internal writer" is not true yet. Either route its paint, cut and release through paintPress, or narrow this sentence.
|
Closing: carried by #8360 (this head, 63128d3, is in it by hash), with each open review item handled where it lives:
|
Depends on #8072 (and its predecessors). Review the commits after #8072's in the Commits tab; this push alone: vjeux/astryx@b600a78...63128d3
Why
On a phone, Astryx's rows and buttons paint their press without UIKit's tracking rules. A finger that slides a little off a button un-presses it at once, a flick through a list lights rows it passes, and a touch that stops a scroll counts as a press. Rows and buttons are most of what a finger touches.
What led to it
This is part of the UIKit-parity work that makes Astryx's touch components behave as they do on iOS. The gesture spec gives presses two profiles, a control's and a table cell's (
spec:AST-094FR11), with every clock taken from the measured constants table.Options and tradeoffs
:activeand the generic press controller: no notion of UIControl's events, so the problems above stay.Why this choice
PressTrackerreports UIControl's events, with "inside" the bounds expanded by 70 pt, as UIButton measures. The control profile highlights in the same frame (after the 150 ms content-touch delay inside a scroller) and flashes a quicker tap; the cell profile always waits for the delay and cancels past the pan's slop. A touch braking a fling paints nothing. All painters writedata-astryx-pressthrough one writer, which owns the release clock and never overwrites a row another painter holds.useGestureRecognizerandusePressTrackerattach without a render per move. Everything stays on the internal gesture entry.How to check it
Evidence
touchDragExitat 71,touchDragEnterat 69.pnpm check:repopass.