Repository navigation
Conversation
…seudo-element support
The README's busy-main-thread paragraph and busyWindowProof's JSDoc said an undelivered frame is never a miss; when fewer than minJudgedFrames frames were delivered in the judged part, its span counts every frame. One clause in each says so.
…ose finds it The unmount kept the node of the first attach(null) in the task, so after attach(null), attach(next), attach(null) it landed a waiting Reduce Motion move on the node already replaced, and next stayed at its dip's opacity. A dispose right after attach(null) landed on no element, leaving the detached node at opacity 0. The latest detached node is now kept while its unmount is pending; the unmount and dispose both land there. Two tests fail on the previous head.
…that Effect The isEnabled sync's silence covered recognizer listeners but not usePressTracker: a press-tracked control inside an element whose double tap a rerender turns off heard touchUpInside (and its un-highlight) from that layout Effect (react-component-runtime INV2). Silencing it would leave the control highlighted, so the tracker's callbacks that arrive during a sync run in a microtask just after it, in order: the press still ends and un-highlights, outside the Effect. Tests, failing before (the events arrived inside the Effect): a press waiting on an ancestor double tap, alone and with a single tap waiting on it, reports nothing from the Effect that turns the double tap off, then touchUpInside and the un-highlight just after. Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…ile it ran A page opened after startCompositorTrace and closed before stop() was still credited: its frames are in the browser-wide record, and neither check saw it. The record now watches for new pages from start to stop, in the page's own context and through the browser's target discovery in every context, and stop() throws if one opened. Negative controls: a page opened and closed mid-record, and one in another context, both make stop() refuse; alone, it records.
…rns to joins that dip Under Reduce Motion a call that names an opacity while an earlier dip's move still waits to land took the dip branch. So a call naming the opacity the dip already returns to, with no travel of its own, opened a second dip from where the first had got to: mid fade out the element faded out twice, and in the dip's own task the browser got a second, identical fade. A swipe row's release does exactly that: its move's layer plays the travel as a dip, then its state layer lands opacity 1 on the same element in the same task. Such a call now joins the waiting dip: the planner returns joins, the fade goes on as it was, the call takes the landing over and settles when the dip ends, and the call it replaces lands with it, each value keeping the call whose move it is. A call that names travel, or another opacity, plans as before. Four tests (the planner's, mid dip, in the dip's task, and a release played then landed) fail on the previous code.
@eslint-react/naming-convention-ref-name: the test component added in 07b3383 held its refs as outer and inner, two lint errors that failed CI's ESLint step. Renamed outerRef and innerRef; no other change.
Inside a trace, waitForAnimation and watch.started() read the element through handles, which find no element rather than wait for one, so a wait on an element the page removed rejected with "no animation started on the element" instead of "the element is not in the document", unlike the untraced path. At its deadline, a traced wait with no element now rejects as missing. Traced twin of the missing-element self-test; it fails on the previous code. Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…med or not The planner's comment on the cross-fade's length said it is not a theme role and no theme lengthens it. Under spec:AST-094 DEC-11 no motion role is themeable: the fade's length is no role at all, and nothing lengthens it.
A promotion keeps the stylesheet's will-change reasons beside its own, and read them with getComputedStyle as it started: in a drag's first move, that forces a style recalculation inside the input handler. The stylesheet's value is now read in a task of its own after the element is attached, and again after each demotion, so a promotion uses what was read. One that comes before that read (an element attached in this task that moves at once) still reads it then. A test that counts style reads in the promoting task fails on the previous code; one for the element that moves at once passes on both.
…orts after that Effect react-component-runtime INV2 keeps caller callbacks out of Effects, and the isEnabled sync and a replaced node's detach did it by silencing every listener for the arena pass they start. A single tap waiting on a double tap recognizes in that pass when the double tap is turned off or its node replaced, so its caller never heard the tap. The hook cases expect that caller to hear it once, just after the Effect, reading the recognized state; the arena case expects GestureArena.deferResolution to fail the stopped recognizer at once and resolve the recognition waiting on it in a pass a microtask later. Fail on this head.
…aims an axis only in its edge zone; the promotion's style read in its own task; the planner's DEC-11 line) into the presented start
The isEnabled sync and a replaced node's detach silenced every listener for the arena pass they start (react-component-runtime INV2), so a single tap waiting on a double tap they stopped recognized in that pass and was never reported. Delaying the listener call alone would hand it a recognizer the arena has already reset to possible. GestureArena.deferResolution(body) runs code outside input that stops recognizers without resolving what the stops unblock (the recognitions waiting on them, held touches, resets); a pass a microtask later, at the last event's time, resolves it, or the next input does first. Both hook syncs run inside it, reaching the arena through recognizerInternals: the stopped recognizer's own cancel stays silent, and a recognizer that required it to fail recognizes outside the Effect, where its listener reads the recognized state. The README and the isEnabled option say so.
…rred unmount runs It read will-change right after attach(null), before the unmount's microtask, so it passed while that microtask could still promote the element and leave timers behind. It now awaits the microtask, then checks will-change and that no timer is left. It fails on meta#8521's earlier head (97436b2), before the unmount cleared what the landing rebuilt.
…pseudo-element support
…ith one passive listener and press-time document listeners
family:action-groups FR5 (and the AST-094 FR19/FR24 exception proposed with
it): the behavior applies to every pointer, mouse and pen included, so a
group must hear a press where the primary pointer is fine, where the host
otherwise registers nothing. attach() takes { actionGroup: true }: under a
fine pointer the group element gets one passive pointerdown listener; a
press on it adds document pointermove, pointerup and pointercancel
listeners (and window blur, plus the click listener for the click the lift
owes), and they go at the release. Only the group's own recognizers and
consumer take that press. Under a coarse pointer the host's usual listeners
serve the group, as every other recognizer.
Tests (failing first): idle under a fine pointer, only the group's one
passive pointerdown and nothing for a recognizer that is not a group; a
press adds the document listeners and the release removes them; a
recognizer that is not a group on the press path takes no part; detach
removes the group listener, and a coarse pointer keeps the usual
listeners.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
…tion constant From review on meta#8890: spec:AST-094 FR4 lists the 200 ms Reduce Motion cap among the touch motion roles and says a component never plays a literal. The release capped its clock at a literal 200 in pressFeedback.ts (REDUCED_MOTION_MAX_MS) and at a literal 200ms in the overlay's reduced release duration. Both now read the shared constant: the controller timing.reducedMotionCrossFadeMs, the stylesheet reducedMotionFadeConsts.duration. Both are 200 ms, so nothing on screen changes; the built stylesheet's cap is still 200 ms. Red first: a unit case that stubs timing.reducedMotionCrossFadeMs at 120 ms found the release still fading at 120 ms on the change before this one; it now ends there.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
…Motion constant too The row press layer capped its release under reduced motion at a literal 200ms (CELL_RELEASE_REDUCED), the same cap the review on meta#8890 asked the overlay to read from the shared constant (spec:AST-094 FR4: the 200 ms Reduce Motion cap is a role, never a literal). It now reads reducedMotionFadeConsts.duration, as the overlay does; both are 200 ms, and the built stylesheet's cap is still 200 ms. A unit case holds every reduced-motion cap in the layer and the overlay to the constant. Red first: it found the layer's literal on the change before this one.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
… change [skip ci]
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
meta#8890's own change. The touch press controller ends a release when its animation ends, a descendant's release included when this surface is its nearest press owner, instead of on a clock from the lift, and the release under reduced motion reads the Reduce Motion fade alone. Absorbs meta#8890 (78f233b).
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
meta#8118's own change. A control's touch press paints on a pseudo-element layer whose opacity the release animates, so the release runs on the compositor; the cards and Thumbnail take the same layer. The release end is meta#8890's (a release ends when its animation does). The overlay keeps its own release curve, so only rows ship the row layer (meta#8077); the next commit leaves FR15's 250 ms out until it is decided. Absorbs meta#8118 (a9fd510).
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
…#8890, meta#8360 045f666) into the press release The release's 250 ms for a finger on a control stays where it was, in the control press layer's CSS, and a mouse's or the Space key's press keeps 200 ms through its input marker. The controller's own profile clock goes with the profiles meta#8117 dropped: the release-end code from meta#8890 waits for the running release animation, so a finger's release still ends at 250 ms. This branch's own copy of that code gives way to meta#8890's.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
meta#8638's own change. The tracker paints its groups' presses through paintPress, the one writer, which now takes an `onFadeEnd` for a `fading` paint: the writer calls it once, when that fade stops being the element's paint, on its release's end (meta#8890's endAfterRelease) or when a later write replaces it. The tracker forgets an action there, so `dispose` ends only its own running fades. Absorbs meta#8638.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
meta#8638's own change. The tracker paints its groups' presses through paintPress, the one writer, which now takes an `onFadeEnd` for a `fading` paint: the writer calls it once, when that fade stops being the element's paint, on its release's end (meta#8890's endAfterRelease) or when a later write replaces it. The tracker forgets an action there, so `dispose` ends only its own running fades. Absorbs meta#8638.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
meta#8638's own change. The tracker paints its groups' presses through paintPress, the one writer, which now takes an `onFadeEnd` for a `fading` paint: the writer calls it once, when that fade stops being the element's paint, on its release's end (meta#8890's endAfterRelease) or when a later write replaces it. The tracker forgets an action there, so `dispose` ends only its own running fades. Absorbs meta#8638.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
…n ends meta#8890's test for another painter's release, with paintPress: a `fading` paint stays until the surface's release animation ends, as the controller's own release does. Fails here: paintPress ends its fade on a clock started at the write.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
meta#8890's release end, for paintPress: its `fading` paint waits for the release animation (endAfterRelease), not a clock started at the write, so a busy first frame no longer cuts it short; a write on the element cancels that wait, as it ended the clock.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
meta#8638's own change. The tracker paints its groups' presses through paintPress, the one writer, which now takes an `onFadeEnd` for a `fading` paint: the writer calls it once, when that fade stops being the element's paint, on its release's end (meta#8890's endAfterRelease) or when a later write replaces it. The tracker forgets an action there, so `dispose` ends only its own running fades. Absorbs meta#8638. The gestures README says so: the tracker paints its groups' presses with paintPress.
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
vjeux
added a commit
to vjeux/astryx
that referenced
this pull request
Nov 3, 2026
3 tasks done
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Review only this PR's change: vjeux/astryx@540299d...78f233b (depends on #8360).
User impact
Under a finger, a pressed control's highlight (a button, switch, checkbox, card, menu row or list row) should fade out after the lift. A tap usually starts work, so the page's script often delays the fade's first frame; the release then ended mid-fade, and the highlight cut off.
Problem and solution fit
What led to it: this is part of a program to make Astryx's list components behave like iOS tables on phones. Moving a list row's release onto the compositor (#8077) showed it being cut short whenever script delayed its first frame.
Why: the touch press controller removed
fadingon a clock started at the lift. The release animation's end moves later with its first frame; the clock didn't. Why it matters: the press's last frames are the feedback that the tap registered, and on a busy tap they vanished.Now, when the clock runs out, the controller looks for the press's finite release animation: on the surface (or its
::beforeor::after), or on a part inside it whose nearest press owner is the surface, for a component that draws its release there (#8233's checkbox draws it on its box). A press owner nested inside keeps its own release. While one runs or still waits for its first frame, the release ends when that animation finishes or is cancelled; with none, it ends at the clock. Under reduced motion the clock is capped by the Reduce Motion cross-fade's duration (200 ms, read from its constant), and the overlay's release animation is capped the same way.Size (gzip, base → head, the repository's size report run against #8360): the 86 components that load the touch press controller +334–465 B of import (Button +411 B); every other core component +38–83 B, the shared Reduce Motion constants the overlay now reads; component CSS +22–46 B, the package stylesheet +23 B.
Expected behavior and authority
usePressFeedback("fades the release over 200 ms");spec:AST-094FR5 (the release plays the press model's duration and curve);design:user-statesDR1/DR5;design:reduced-motionDR2 (a touch transition finishes in 200 ms or less).Smallest restoration
animationendlistener per surface: every surface wires it, and a surface that unmounts mid-release never fires it.Evidence
fadingwas removed mid-fade (unit cases below, red on fix(core): one click guard for the menu press model and the gesture host #8360's controller).fadingstays until the animation ends.fadingwent at the 200 ms clock, and that frame relaid and repainted the box in a Chromium trace; with this change it holds to the release's end.releaseAnimationsOfwith the owner-only rule, so it needs the same change when it takes this pull request.Scope
Testing
pnpm vitest run --project ui packages/core/src/utils/pressFeedback.test.ts: the eight cases under "the release ends with its animation, not its clock".pnpm vitest run --project node scripts/build-css.test.mjs: the overlay's release, capped under reduced motion, on the controller's clock.What was run: the unit tests of the press surfaces (utils, Button, Switch, Checkbox, Card, Item, List, RadioList, menus), the stylesheet test,
pnpm check:repoand the core type check. These are local runs; no CI run is expected for this pull request.Vercel's failed deployment on e2c10d2 was on Vercel's side: its build command passes locally on that commit, and #8077's head, which contains it, deployed.