Skip to content

chore(core): the press controller leaves an action group's tracked actions to its tracker - #8637

Draft
vjeux wants to merge 1631 commits into
meta:mainfrom
vjeux:uikit/c01-press-group-skip
Draft

vjeux wants to merge 1631 commits into
meta:mainfrom
vjeux:uikit/c01-press-group-skip

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Review this PR alone: vjeux/astryx@uikit/f2-click-guard...uikit/c01-press-group-skip

Maintainer impact

Part of the UIKit-parity work: in a group of sibling actions (an alert's buttons, a button group, a toggle group) you can press one and slide to another, as on iOS (family:action-groups, from #8622). The group's own tracker paints the press on the action under the pointer. The page-wide press controller paints the same buttons, so the two erase each other: the controller's release fade wipes the group's highlight, and the button you started on stays lit beside the one you slid to.

Intended invariant

One painter per press. The controller already calls itself the one writer of the press attribute; a group takes that role only for the actions it tracks.

Change

What led to it: building the group tracking for AlertDialog and ButtonGroup showed the double paint.

  • Skip every element inside a tracking group: rejected, it would also silence members the group doesn't track, such as a field or a split button's menu trigger.
  • Chosen: the group marks each action it tracks (data-astryx-press-group-item), and the controller paints nothing on a marked action. The mark is there before the first press, and the group can use the same attribute to find its actions.

The controller still records the touch on a marked action, so a scroll it starts still brakes the next touch. Where the primary pointer is coarse the controller also paints a mouse press itself (#8046); on a marked action it leaves that to the group too, while a Space press, which the group doesn't track, still paints. No component renders the marker yet: nothing changes until each group adopts it (#8083, #8657, #8660).

Evidence

  • packages/core/src/utils/pressFeedback.test.ts, six new cases (below).

Scope

  • No intended public API, product behavior, visual direction, or policy change. utils/pressFeedback.ts is internal: it is not on a published entry point.
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

Nothing to check by hand yet: no component carries the marker.

What was run: six new unit tests. The controller paints nothing on a marked action and leaves the group's paint alone, for a finger and for a mouse on a touch-first device; those fail without this change. It still paints an unmarked member, which fails if the skip checks group membership instead of the mark, and a Space press on a marked action. It still records the touch for the scroll brake, which fails if the skip comes first. Typecheck, lint, the core unit suite and the press accessibility tests pass locally; GitHub Actions isn't running on this repository right now.

Stack: on #8360, which carries #8076. The adoptions (#8083, #8657, #8660) stack on this pull request.

cixzhang and others added 30 commits October 26, 2026 14:00
startCompositorTrace keeps every Display::FrameDisplayed in the browser, and
those events carry no page identity: with a second page animating, a stall
inside the block passed busyWindowProof with presented, crediting the other
page's frames. It now throws when another page is open as it starts or
stops, and says the record is browser-wide (JSDoc and README). Negative
control: another animating page makes it refuse to start, and a page opened
mid-trace makes stop() refuse.
A bare relative ASTRYX_WEBKIT_SHADER_CACHE was mounted as a named volume
instead of a host directory, and an empty one made mkdirSync throw. The
script now resolves a set value with path.resolve and falls back to the
default cache directory on an empty one.
… path) into the recorder stand-down

# Conflicts:
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
…ce; await it before the counted window

The README, waitForRest's JSDoc and traceRendering's JSDoc said a traced
waitForRest reads no style. Its one read of the element's animations, as it
is called, restyles the element when an animation is already running there
(2 Layouts and 2 style recalculations in the anchored fixture, 200 ms into
its animation). They now say so and tell a proof to await it right after
the action that starts the motion, before the window it counts.
…ch-action rules in @layer reset, the watcher split into meta#8589) into meta#8293
Under Reduce Motion, detaching mid-dip landed the waiting move in the model
at the end of the task, but with no element attached the landing wrote
nothing, so a call that owns its opacity left its dip's low opacity inline
on that node: attaching the same node again in a later task (a Suspense or
Activity reveal) showed it at opacity 0. The unmount's landing now writes to
the node just detached, as dispose() does to its element. The test fails
on the previous head.

(Found on meta#8067; fixed here because meta#8056, which carries the planner, is
approved at its exact head.)
… against its span

In WebKit the rAF negative control could read "1/1 moved": one frame from
before the block arrived 45 ms into it, and the moved criterion divided by
the delivered frames. A part that delivered fewer than minJudgedFrames
frames is now judged against its span, as one that delivered none was
(busy.test.ts case; it fails on the previous rule). The README and the
self-test header now say only the busy-main-thread proof carries a negative
control beside its positive check.
…into n8548

# Conflicts:
#	internal/interaction-harness/README.md
The lock moved to utils/selectionLock.ts with the shared input stream, and
stacks based below that move import it from gestures/selectionLock.ts; a
tree carrying both failed to typecheck. The case imports the old path and
expects the utils module's functions. Fails on this head.
…rom utils

A one-line transition re-export at gestures/selectionLock.ts keeps
`import {lockSelection} from '../gestures/selectionLock'` working for the
stacks based below the move to utils, so a tree that carries both
typechecks. Remove it once they have moved up.
A watcher already adds nothing to the derived touch-action, and
manageStyle is per element, so attaching one with manageStyle: false
beside a claiming recognizer switched off that recognizer's touch-action
(spec:AST-094 FR18). The README now says a watcher adds nothing instead.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…l, straight or diagonal, and not before

spec:AST-094 FR20's verification names a finger moved 9 px and 10 px
straight, and (7, 7) and (8, 8) diagonally: a scroll above the touch takes
it only once the finger has travelled 10 px from where it landed. The host
already measures that distance; these cases pin the boundary.
…g running

Landing a waiting Reduce Motion move on the node just detached rebuilt
every channel the landing touched there, so an axis still moving (a coast
on y while x waited in a dip) was baked onto the detached node again and
promoted it, leaving an animation, timers and will-change behind. After the
landing, the unmount now cancels what runs, clears the retire timers and
demotes, before the target goes back to none. The test fails on the
previous head.
vjeux added 4 commits October 27, 2026 07:09
…r along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8356
…r along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8358
…in, as documented; meta#8336: the host compares a finger with a scroller along the flat tree, and a claimed touch finds its pointer finger wherever fingers come from pointer events) into meta#8360

# Conflicts:
#	packages/core/src/hooks/useLongPress.ts
…handlers type

UseLongPressHandlers.start takes a point only again, as released: the
per-press tolerance useMenuPress's trigger chooses as its press starts
lives on the internal useLongPressDetector and LongPressDetectorHandlers,
which the hooks entry does not export. useLongPress returns the same
detector, typed as before.
@cixzhang cixzhang closed this Oct 28, 2026
@cixzhang cixzhang reopened this Oct 28, 2026
@cixzhang

Copy link
Copy Markdown
Contributor

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.

vjeux and others added 14 commits October 28, 2026 10:39
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.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
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.
Both upstream heads are folded onto main 70ea859 (her meta#8045 frame
evidence and meta#8046 coarse press): meta#8446 takes main's harness files, and
meta#8073 carries the press controller onto meta#8046's slots. meta#8076 has no change
of its own in either set of files, so the merge takes theirs with no hand
resolution, and meta#8076 is clean against main again.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
…bute

From the review of meta#8076: the README said paintPress in
utils/pressFeedback.ts is the one writer of data-astryx-press, but the
action-group tracker on main (meta#8346) writes its groups' presses itself.
The sentence now says what holds at this head.
meta#8076's new head carries main 70ea859 with meta#8073's writer of the press
attribute on main's press controller, and its README names both writers.
utils/pressFeedback.ts keeps that controller with this line's own changes
on it: the pointer, drag, scroll and visibility listeners through the
shared input stream (meta#8356) and touchcancel only while the primary pointer
is coarse (meta#8270); the key path stays at the window's bubble phase, so a
Space press the page cancels paints nothing. usePressTracker stays in its
own module, one tracker for a component's life (meta#8315), so meta#8073's
rebuilt-tracker hook and its test are not taken. The harness files are
meta#8076's.
…s, and the action-group tracker writes its own

The header and writePress called it the one writer of the press attribute,
but main's action-group tracker writes its groups' presses itself. They now
say what holds, as meta#8076's gestures README does.
…he press attribute

The release note said every press painter writes data-astryx-press
through one internal writer, but the action-group tracker
(hooks/actionGroupTracker.ts) writes its groups' presses itself, as the
gestures README and utils/pressFeedback.ts already say. The note now says
paintPress writes every press it tracks and the action-group tracker
writes its own.
…arker

Takes meta#8360's press controller as it stands (its finger and button
slots, and the mouse-and-Space press from meta#8046) and re-applies only
this PR's change on it: the group item marker and the controller's
skip of a pointer press on a marked action.
…tion to its tracker

meta#8046 made the controller paint a mouse press itself where the primary
pointer is coarse (a tablet with a trackpad). On an action its group
tracks (family:action-groups), the group's tracker paints that press,
so the controller must paint nothing there, as it already does for a
finger; it paints the press, then turns the tracker's paint to
'fading' at the release. A Space press on the same action still paints:
the group tracks pointers only (guard).
… its tracker

Where the primary pointer is coarse the controller paints a mouse press
itself (meta#8046). It now skips an action a group's tracker paints for a
pointer's press, a mouse's included, as it already did for a finger, so
the two never write the press attribute over each other. A Space press
on the action still paints: the group tracks pointers only.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: the press-group skip itself matches the action-group spec now on main (family:action-groups FR2, landed with #8622), but against main this pull request also carries the whole open #8360/#8076 stack, 26,783 authored lines beside its own 139. Please retarget it onto main once #8360 and #8076 land, so it carries only its three files. Separately, the stack conflicts with the current touch spec in three places that are best fixed in those pull requests:

  • The gesture host attaches only where the primary pointer is coarse, while AST-094 FR19 and FR43 and the runtime record's INV13 now require it to attach for every pointer.
  • A finger held on a DropdownMenu trigger keeps a 10 px drift under a fine primary pointer, while the useMenuPress record's FR9 applies AST-094 FR29's allowance under every primary pointer.
  • PressTracker is a class, while INV13 keeps classes to the recognizer family.

The earlier findings about the fine-pointer finger press and the usePressTracker README are fixed at this head.

[Automated review]

*
* ONLY WHERE THE PRIMARY POINTER IS COARSE
*
* - Recognizers attach, and the host registers listeners, only while the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-094 FR19 now reads "Recognizers attach for every pointer, with FR43's exceptions", and architecture:react-component-runtime INV13 says the host attaches "with no touch-only query". Installing only while the primary pointer is coarse (and the warning at :1326-1338) contradicts both. This belongs in #8076/#8360 rather than here.

* pointer is coarse; where it is fine, the released behavior's tolerance
* (useMenuPress FR9), the long press's own. Read when the press starts.
*/
function triggerTolerance(): LongPressTolerance {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

module:DropdownMenu/useMenuPress FR9 puts the trigger under spec:AST-094 FR29 "under every primary pointer", so a finger on a touchscreen laptop should get the same 13 px drift as on a phone, not the long press's 10 px. Dropping the pointer read also removes the menus' coarsePointer import.

return tap.numberOfTapsRequired === 1 && tap.numberOfTouchesRequired === 1;
}

export class PressTracker implements TouchConsumer {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV13 keeps classes to the recognizer family (its base, the arena and the host), and its verification fails "a class outside the recognizer family". A createPressTracker factory would conform.

@vjeux

vjeux commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. This retargets onto main once #8360 and #8076 land, leaving its three files. Of the stack points, the host attaching for every pointer is #8665's and PressTracker becoming a factory is #8763's; the DropdownMenu trigger's drift is fixed in #8360 (1e1431fe2f).

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:38
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026

This branch was successfully deployed

1 active deployment
Preview — ed3d7509 Deployed Oct 28, 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. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants