Repository navigation
feat(core): one tracker for a held finger across any group of targets - #8346
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
4931e65 to
c576918
Compare
c576918 to
45a954c
Compare
`createActionGroupTracker` (hooks/actionGroupTracker.ts), exported from `@astryxdesign/core/hooks` as experimental with `ActionGroupTracker`, `ActionGroupTrackerOptions` and `ActionGroupTarget`: UIKit's tracking of a held finger across a group of targets, as a plain object any gesture recognizer can drive, so an alert's or an action sheet's actions, a menu's rows, a segmented control's segments and a page control's dots share one implementation instead of each resolving, painting and activating the target under the finger itself. - `begin` reads the targets' rects once; `move` resolves the finger against them (the hit test grown by `highlightExpansion`, or the owner's `resolveTarget(x, y, targets)`: the nearest segment along an axis, the end one past the ends) and never reads layout; `offsetBy` and `remeasure` follow content that moves under a still finger. - The highlighted target is painted with the press attribute (or not: `paintsHighlight: false`) and reported with the one it replaces (`onHighlightChange(next, previous)`); `beganOn` is the target the press began on; `onMove(x, y)` sees every position first. - `end` fades the paint and runs the target through its own `click()` inside the lift's event (or `onActivate`); `onActivated(item, cancelled)` learns whether a handler's `preventDefault()` vetoed it, and `swallowsNativeClick` takes the browser's own click for an unclaimed touch once, with listeners that exist only until that click, the next press or a key. - An inert press is taken but tracks nothing; disabled targets are skipped; `dispose` finishes pending fades.
45a954c to
37ee506
Compare
…acker The scrub was PageControl's own tracking of a held finger across its dots. It now runs on core's one tracker for a held finger across a group of targets (createActionGroupTracker, meta#8346), the piece an alert's actions, a menu's rows and a segmented control's segments share: - the tracker begins once the finger has travelled scrubStartDistance (until then the press is a tap) and reads the dots once; the page under the finger is resolveTarget: scrubTarget on the control's geometry, read once at that begin, so nothing reads layout while the finger moves; - past the content's edge of a windowed control it resolves no dot, and the jog steps the page as before; - onHighlightChange asks for the dot's page (paintsHighlight: false: the current dot shows it), and the lift commits nothing more (onActivate). The scrub browser test now also counts layout reads: none after the scrub begins.
No new public surface without its own spec decision (spec:AST-002 FR18): createActionGroupTracker and its types are no longer exported from @astryxdesign/core/hooks. Core's consumers import the module directly.
… it is core's one track-across-items loop An export-lock test proves neither the hooks entry point nor the package exports reach the tracker. The header records that it is core's one track-across-items loop, that useMenuPress's copy moves into it next, and that nothing drives it until its adopters land. Co-authored-by: Christopher Chedeau <vjeux@users.noreply.github.com>
|
/approve-spec 0b92ae3 |
There was a problem hiding this comment.
This is ready to merge as an internal building block: no package path reaches the tracker (a new test enforces that), no component uses it yet, and its click swallower never blocks a detail === 0 activation, as spec:AST-094 FR21 asks. Before #8110 and #8127 start depending on its defaults, four of them deserve a fix or a documented precondition:
- When
highlightExpansionexceeds the gap between neighbours, the hit test returns whichever target comes first in the DOM, so a finger just inside the second target can fire the first. - Targets marked
data-astryx-pressablealso getdata-astryx-pressfrom the document press controller, which wipes the tracker's highlight after a finger leaves its starting target and returns. onActivatedreportscancelled: falsefor a vetoed click on a target inside a shadow root, because the check comparesevent.target, which outside the shadow tree is the host.- With
swallowsNativeClick, a lift that fires nothing (an inert press, a slide off every target, a target disabled mid-press) arms no swallower, so the browser's own click for that touch still goes through.
[Automated review]
| const view = element.ownerDocument.defaultView; | ||
| let clicked: Event | null = null; | ||
| const grab = (event: Event) => { | ||
| if (clicked == null && event.target === element) { |
There was a problem hiding this comment.
For a target inside a shadow root, this window listener sees the host as event.target, so clicked stays null and onActivated gets cancelled: false even when a handler called preventDefault(), while the same target in the light DOM reports true. Menus already render inside shadow roots and the header says their loop is moving here, so identifying the element through event.composedPath() would keep the report right for them.
| const lifted = highlighted; | ||
| const wasTracking = isTracking; | ||
| reset(); | ||
| if (!wasTracking || lifted == null) { |
There was a problem hiding this comment.
With swallowsNativeClick, this early return (and the disabled one just below) arms no swallower, so after an inert press, a slide off every target or a target disabled mid-press, the browser's own click for an unclaimed touch can still reach the target the touch began on, against the header's "nothing when there is none". Arming the swallower on every lift of a tracked touch, or documenting that the owner must block that click, would close the gap.
|
Posted by Cindy's agent on her behalf: closed and reopened only to restart this PR's CI on the current workflow after tonight's queue-wide cancel; nothing in the PR changed. |
Depends on nothing (based on main).
Maintainer impact
A held finger that slides across targets and lifts on one is tracked in several places. Menus (
useMenuPress), alerts and action sheets (useActionGroupTracking, #8083), the segmented control (#8127) and the page control's scrub (#8174) each resolve the target under the finger, paint or report it, and activate it. Review asked for one implementation ("a shared tracker"). This adds that tracker as a standalone building block, internal to core. #8110 moves the alert's tracking onto it, and the others stack on it without pulling in menus or alerts.Intended invariant
remeasure), never per move.highlightExpansion.click()inside the lift's event, keeping the user activation and every click handler.Change
hooks/actionGroupTracker.tsaddscreateActionGroupTracker(getOptions), which returnsbegin,move,end,cancel,offsetBy,remeasure,dispose,highlighted,beganOnandisTracking. Options:roots,itemSelector,highlightExpansion.resolveTarget(x, y, targets): the owner picks from the measured rects (a segmented control: the nearest segment along x, and past the ends the end one, so a lift anywhere commits).onMove(x, y): every finger position, before it is resolved.paintsHighlight: false, plusonHighlightChange(next, previous): for an owner that shows the highlight another way (focus, a dimmed title).onActivate(replaces the click), andonActivated(item, cancelled): whether a handler'spreventDefault()vetoed the click.swallowsNativeClick: the browser's own click for an unclaimed touch is taken once after the lift. Its listeners exist only until that click, the next press or a key. An activation withdetail === 0(a key, assistive technology) always passes (spec:AST-094FR21).Evidence
detail === 0passing it, and the swallower's listener lifetime.Public delta
None: the tracker is internal to core, with no export from a package entry point (no new public surface without its own spec decision). No product behavior, visual direction or policy changes.
Testing
pnpm vitest run packages/core/src/hooks/actionGroupTracker.test.ts: 22 pass; thedetail === 0case fails without its change.check:changesets, changeset coverage,sync:exports(unchanged), the CLI's experimental-isolation goldens (unchanged).