Skip to content

feat(core): a checkbox and a radio option press the same way for every pointer (spec:AST-094 FR43) - #8696

Draft
vjeux wants to merge 1896 commits into
meta:mainfrom
vjeux:uikit/c06-fr43a
Draft

vjeux wants to merge 1896 commits into
meta:mainfrom
vjeux:uikit/c06-fr43a

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8233 and #8682 (and their predecessors). Review only this PR's change: vjeux/astryx@b809fd6...d96069b

User need

spec:AST-094 FR43 gives a fine pointer a finger's press model. With a mouse, CheckboxInput, CheckboxList and RadioList still pressed through :active: a CheckboxList row pressed on its label left its box blank, a pen on a fine-pointer page pressed the box instead of the row, and a RadioList row kept its own :active paint.

Problem and solution fit

Current specification

  • spec:AST-094 FR43, FR11's fine-pointer profile, DEC-21; FR24.

Public delta and caller burden

No API change. With a mouse or a pen, a checkbox and a radio option paint their press at pointerdown and fade on release, as under a finger; a CheckboxList row pressed anywhere paints the row and its box. A right click and a read-only row paint nothing. Touch targets stay coarse-only. The stylesheet loses the pointer-query wrappers and :active arms.

  • Size: base → head per component in this pull request's CI Component Size report (analyze-pr.js).

Design and alternatives

Keeping :active for a mouse would leave two press models. Tests that pinned a coarse-only press are conformed, including scripts/build-css.test.mjs's desktop-rule case (from #8118's line), narrowed to the Button's host layer: a selection mark's layer is its only press for every pointer.

Evidence

Chromium, a fine primary pointer, a real Storybook build: a mouse marks the press at pointerdown and the layer paints the pressed token, then releases on the host's clock (checkbox, CheckboxList row, RadioList row; each fails on the previous head). A held pen paints the same way. With a mouse or a pen, a CheckboxList row presses from its label and its box.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

cixzhang and others added 30 commits October 26, 2026 19:51
…8072 9800eb5, the FR18-FR22 browser states) as a dependency
…eta#8061 9adbe2d, meta#8072 9800eb5) into the gesture watcher

The README's touch-action table takes meta#8061's edge-pan rows beside this
PR's watcher row.
…es its recognizers, and the group keeps answering afterwards

Under a fine pointer the host is not installed, so usesTouchEvents keeps
its initial true, and a group press's own touch pointermove and pointerup
went down the touch-pairing path and returned: the finger's moves and lift
never reached the group, its contact stayed in the arena, and the group
answered no press after it, a mouse's included. The group press now feeds
its own events through the pointer path for every pointer type.

Test (failing first): under a fine pointer a finger tap on a group
recognizes, and a mouse tap afterwards does too.
… are all passive

spec:AST-094 FR20: a watcher registers only passive listeners, but a
watcher-only page still got the host's cancelable document touchend and
its contextmenu and window click listeners. The document touchend is now
passive until an enabled claimer (a recognizer that claims touches, or a
consumer) is attached, and the contextmenu and click listeners are on only
then, swapped as attachments change. The new test (a lone watcher leaves
no cancelable document or window listener; a claiming pan brings the
three) fails on the previous head.
…r watchers) into the host line

The host keeps this PR's isRtlElement and its fingers held before a
recognizer begins (a kept press, a scroll-edge pan), beside meta#8061's view
size and meta#8309's pointer-events finger lookup. meta#8589's claimer check
counts only participants active on this pointer (isActive,
isConsumerActive), and a group press's click listener now stays while a
claimer keeps it installed. The browser spec keeps the fine-pointer
describe beside the FR18-FR22 real-touch states.
…'s passive-only watcher listeners) into the arena fixes

The host-owner changeset keeps this PR's own listeners for the scroll
lock and finger-scroll watch, and adds meta#8336's scroll-edge hold.
…inter reaches its recognizers) as a dependency
… under a fine pointer) as a dependency
…tion group's finger under a fine pointer) into the gesture watcher
Main's meta#8521 is its head 25b468c squashed, which this pull request already
carries: every file it touches is byte-identical to 25b468c. In the two
files that conflicted (the motion README and animator.ts), the resolution is
this side's version, main's merged meta#8521 with this pull request's later work
on top. Nothing of main is dropped.
…ta#8521) into meta#8270

# Conflicts:
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
…g) into meta#8293

# Conflicts:
#	packages/core/src/motion/animator.ts
The note's reword joined its next sentence onto one long line; it now
wraps like the rest of the file. No code change.
cixzhang's meta#8589 keeps the host's touchend passive, and its contextmenu
and click listeners off, until a claimer is attached
(syncClaimingListeners). Ported onto this pull request's input stream with
no change to what it does: install() subscribes the touchend passive, and
syncClaimingListeners swaps it for a cancelling subscription and adds the
contextmenu and click subscriptions while a claimer is attached. A group
press brings its own click subscription only while the host has none, and
the host's takes it over. Her watcher test reads those listeners on the
window, where the input stream registers them; her new group test (a
finger, then a mouse, under a fine pointer) sits before the detach test.
The test file and the bare fixture import both her
ScreenEdgePanGestureRecognizer and this layer's selection lock from utils.
Here the click guard (utils/clickGuard.ts) owns clicks, so cixzhang's
meta#8589 syncClaimingListeners, ported at meta#8356, switches only the touchend's
cancelability and the contextmenu subscription; the group press subscribes
no click of its own. Her meta#8072 5c59af1 (a finger on an action group
under a fine pointer reaches its recognizers) arms the click guard where it
armed the swallower. Her watcher test reads the touchend and the
contextmenu on the window, and no click listener: the guard's comes only
once a gesture acts.
… the lift, under real touch input

spec:AST-094 FR20's "follows the scroll to the lift" had only a jsdom
test with a synthetic takeover; the FR18-FR22 verification row asks for
real touch input. In Chromium with injected touches, a vertical drag on a
list a watcher pan is attached to scrolls the list natively, no touchmove
is cancelled, and the watcher goes from began to ended at the lift.
… at the edge stays the page's

spec:AST-094 FR10: two fingers before a claim are the page's pinch. The
scroll-edge hold (DEC-18) counted any finger, so with two fingers down it
cancelled the pinch's moves before any drag was claimed. It now holds only
while a single finger is down. The new two-finger test fails on the
previous head.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
vjeux added 15 commits October 27, 2026 13:38
… paint

Button.spec.md FR15: main's text (meta#8613: the 200 ms release at tempo 1,
scaled by the theme's tempo, spec:AST-108 FR4) with this pull request's
own wording kept; the measured 250 ms release is likewise stated at
tempo 1.
…busy first frame outran

pressFeedback.ts removes data-astryx-press="fading" on a timer of the
release's duration, started at the write. When the main thread is busy
right after the lift, the release animation starts only at the first frame
after the block, so the timer removes the release partway through it: on
a list row after a 150 ms block, 0.736 -> 0.558 -> gone about 52 ms in.
These fail on that code:

- pressFeedback.test.ts: with a release animation the stylesheet started
  late (or still waiting for its first frame) when the clock runs out,
  the controller's release and another painter's (paintPress) stay
  fading until the animation finishes. Guards: a cancelled animation ends
  it, no animation ends it on the clock (reduced motion's instant
  release, a surface without a press layer), a descendant's, an endless or
  an earlier animation is not waited for, and a new press keeps its paint
  when the old release ends later.
- ButtonPressRelease.interaction.browser.spec.ts, both engines: after
  150 ms of script at the release, the button's layer keeps the release
  until its strength reaches 0.
…y first frame started it

endAfterRelease replaces the clock that removed data-astryx-press="fading"
at the release's duration after the write, for the controller's release
and for every other painter's (paintPress). The clock now only looks: at
the release's duration, a release animation the write started on the
surface (one of its own CSS animations, on it or its ::before or ::after,
finite, started at or after the write or still waiting for its first
frame) is waited for until it finishes or is cancelled, so a busy main
thread that starts the animation late ends it late too, at strength 0.
With no such animation (reduced motion's instant release, a surface
without a press layer, an engine without Web Animations) the release ends
on the clock, as before. Any new write, or a new press, cancels the wait.
A slot's pending clock is now a cancel function, so the release's wait
and every timer clear the same way.
…t) under the selection controls' FR43 change
…nder the selection controls' FR43 change
…release ends when its animation does) into the selection controls' FR43 change

@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.

Approval waits on one spec decision: the branch rewords the current component:Button FR15 (a 250 ms coarse-pointer release painted on ::before, 200 ms for a Space press and for fine pointers, and a Reduce Motion split) and adds a verification row for it, so an engineering owner needs to approve that record by commenting /approve-spec <full head SHA> on this pull request, or the record should keep main's FR15 text; the author may do so as an engineering owner. The selection controls' own mouse and pen press is covered by spec:AST-094 FR11, FR43 and DEC-21 on main. Whatever the FR15 decision, these still need changes:

  • Rebase onto main once the predecessor pull requests land, so this pull request carries its own 15 files (996 lines) instead of 271 files and 42,815 authored lines.
  • With Reduce Motion and a mouse, the checkbox and radio release still fades for 200 ms (CheckboxInput.tsx, RadioListItem.tsx); design:reduced-motion DR3 asks for an immediate change, which the shared press overlay already makes with its (prefers-reduced-motion: reduce) and (not (pointer: coarse)) arm.
  • A standalone checkbox's layer declares a 250 ms release while the press controller clears a fine pointer's press after about 200 ms, so the fade snaps off at about 15%; align the two clocks and assert how long the press stays fading.

[Automated review]

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:39
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…y pointer

meta#8696's own change (spec:AST-094 FR43). CheckboxInput, CheckboxList and
RadioList take one press model for a finger, a mouse and a pen: their owners
and the layer over the box serve every pointer, a RadioList row drops its
`:active` press arm, and the press tests read that one model.

The pen case follows in its own commit: it needs the pen driver
(`webInputDriver`, `PEN_SKIP`) from meta#8666, which lands first.

Absorbs meta#8696 (d96069b).
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
meta#8696's pen case (spec:AST-094 FR43), on its own because it needs the pen
driver (`webInputDriver`, `PEN_SKIP`) from meta#8666, which lands first.

Absorbs meta#8696 (d96069b).
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…y pointer

meta#8696's own change (spec:AST-094 FR43). CheckboxInput, CheckboxList and
RadioList take one press model for a finger, a mouse and a pen: their owners
and the layer over the box serve every pointer, a RadioList row drops its
`:active` press arm, and the press tests read that one model.

The pen case follows in its own commit: it needs the pen driver
(`webInputDriver`, `PEN_SKIP`) from meta#8666, which lands in section 1.

Absorbs meta#8696 (d96069b).
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
meta#8696's pen case (spec:AST-094 FR43), on its own because it needs the pen
driver (`webInputDriver`, `PEN_SKIP`) from meta#8666, which lands in section 1
before this section.

Absorbs meta#8696 (d96069b).
cixzhang added a commit that referenced this pull request Nov 3, 2026
Under spec:AST-094 FR43 the press controller paints a mouse's or a pen's
press too (data-astryx-press), not only a finger's. Tab painted its
pressed image only inside @media (pointer: coarse), so a mouse press
showed nothing but :active's colour, with no release fade.

- Its image arms paint the controller's press for every pointer.
- Its hover and pressed colours yield while the press is on, so a press
  never paints twice, and paint again over the release, so a hovering
  pointer's release lands on the tab's hover (as the shared overlay does).

Checkbox and Radio are #8696's; Switch is #8681's.

Test: Tab's compiled CSS paints both arms outside the coarse media.
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
meta#8696's pen case (spec:AST-094 FR43), on its own because it needs the pen
driver (`webInputDriver`, `PEN_SKIP`) from meta#8666, which lands first.

Absorbs meta#8696 (d96069b).

@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.

Approval waits on an engineering owner's decision: the branch rewords the current component:Button FR15 (a 250 ms touch release on a layer under the label, 200 ms for Space and fine pointers, and a Reduce Motion split), so an engineering owner needs to approve that rewording by commenting /approve-spec <full head SHA> on this pull request, or the record should keep main's FR15; the author may do so as an engineering owner. Whatever that decision, these need changes:

  • Rebase onto main once the predecessor pull requests land, so this pull request carries its own 15 files (996 lines) instead of 271 files and 42,815 authored lines; it currently conflicts with main in 112 files, including CheckboxInput.tsx, CheckboxListItem.tsx and RadioListItem.tsx.
  • Desktop behavior change needs an owner decision: the mouse and pen press on the checkbox box, CheckboxList rows and RadioList rows (CheckboxInput.tsx, CheckboxListItem.tsx:278, RadioListItem.tsx) changes what a desktop mouse press shows, so either gate it to touch input or cite that decision.
  • With Reduce Motion and a mouse, the checkbox and radio release still fades for 200 ms (CheckboxInput.tsx:123, :142, RadioListItem.tsx:80), where design:reduced-motion DR3 asks for an immediate change, which the shared press layers already make with their (prefers-reduced-motion: reduce) and (not (pointer: coarse)) arm.
  • A standalone checkbox's layer plays a 250 ms release (CheckboxInput.tsx:122) while the press controller ends a fine pointer's press after about 200 ms, so the shade snaps off from about 15%; align the two clocks and assert how long the press stays fading.
  • Three changesets now contradict the change (checkbox-row-one-press-surface.md, indicator-press-release-compositor.md and press-control-profile.md say mouse presses are unchanged), and the change adds none of its own.
  • The coarse-pointer query is spelled twice (utils/coarsePointer.ts:26 beside utils/touchQueries.ts) and checked twice (stylesheetDroppedActive in utils/pressFeedback.ts beside isCoarsePointer); use one of each.

[Automated review]

animationName: 'var(--_indicator-press-release, none)',
animationDuration: {
default: CONTROL_RELEASE,
'@media (prefers-reduced-motion: reduce)':

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With Reduce Motion and a mouse this arm still runs a 200 ms release: on the head the box's layer fades from 0.986 to 0.014 over about 190 ms after a mouse release, while main clears at once. design:reduced-motion DR3 asks a fine pointer for an immediate change. utils/controlPressLayer.ts:86 and utils/rowPressLayer.ts:131 already drop the release under '@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))'; the same arm here (and on :142) would keep a finger's cross-fade and give a mouse an immediate change.

animationName: 'var(--_indicator-press-release, none)',
animationDuration: {
default: CELL_RELEASE,
'@media (prefers-reduced-motion: reduce)':

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The same Reduce Motion gap as the checkbox layer: with a mouse the circle's release still fades for 200 ms (measured on the head), where design:reduced-motion DR3 asks for an immediate change on a fine pointer. The (prefers-reduced-motion: reduce) and (not (pointer: coarse)) arm in utils/rowPressLayer.ts:131 would fix it.

backgroundImage: 'var(--_indicator-press-image, none)',
animationName: 'var(--_indicator-press-release, none)',
animationDuration: {
default: CONTROL_RELEASE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This 250 ms control release now applies to every pointer, but with a fine primary pointer releaseOf (utils/pressFeedback.ts:792-800) clears the press on the 200 ms cell clock, and the wait for a running release animation skips this layer because it runs on a child of the owner (utils/pressFeedback.ts:476). On the head fading ended about 190 ms after release with the layer still at opacity 0.148. Keeping the 250 ms clock behind a coarse primary pointer, as utils/controlPressLayer.ts does, would make the two agree.

// a mouse or a pen on the label, the description or the box itself presses
// the row, and the box paints the row's press, the way a radio row's
// circle does. Nothing is read per pointer, so a row adds no observer.
const rowOwnsPress = hasRowInteraction && !effectiveDisabled;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This makes the row the press surface for a mouse and a pen too, so a desktop mouse press on a CheckboxList row now paints at pointerdown and fades after release instead of :active. That is a desktop behavior change that needs an owner decision: either gate it to touch input or cite the decision.

'@astryxdesign/core': patch
---

[fix] Under a finger, the press over a checkbox box or a radio circle now fades out on the compositor, so the release keeps moving while JavaScript runs. A standalone CheckboxInput releases like a button (250 ms ease-in-out; under Reduce Motion, 200 ms, the cross-fade cap of the reduced-motion design record). A box in a CheckboxList row and a circle in a RadioList row release with their row (200 ms). Inside rows that paint their press on a compositor layer, the box and the circle paint their own press again, and a RadioList row stays bare, as under a mouse. Mouse presses, hover and pressed colors are unchanged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"Mouse presses, hover and pressed colors are unchanged" no longer holds with this change: a mouse press on a box or a circle now paints from the controller and fades at release. checkbox-row-one-press-surface.md:5 and press-control-profile.md:5 say the same; please correct them and add a changeset for the mouse and pen press.

// interactionOverlay.stylex.ts), read once per press.
return (
typeof window.matchMedia === 'function' &&
window.matchMedia(TOUCH_QUERY).matches

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

stylesheetDroppedActive re-implements isCoarsePointer, which this file imports at :97 and calls at :951 and :1254. Calling isCoarsePointer(window) here keeps one check, and utils/coarsePointer.ts:26 can take TOUCH_QUERY from utils/touchQueries.ts (React-free) instead of restating it.

| FR12 | With `tooltip`, the tooltip attaches to the Button element itself (no wrapper element), opens above it, and its content is added to `aria-describedby` after any caller value. | Current source; real-browser accessibility test |
| FR13 | A standalone Button paints the shadow token for its `elevation`. Inside a ButtonGroup it paints no shadow and reflects `data-elevation="none"`, whatever `elevation` says. | `family:buttons` FR8–FR10; current source and tests |
| FR14 | The root carries `astryx-button` and reflects the effective `data-variant`, `data-size`, and `data-elevation`. | `family:buttons` FR10; `architecture:component-theming-surface`; tests |
| FR15 | Hover paints the shared interaction overlay on hover-capable pointers, and a press paints the shared pressed overlay through the press controller: at once under a mouse or a pen (`spec:AST-094` FR11's fine-pointer profile) and on FR11's control profile under a finger, fading out over 200 ms on release at `tempo` 1, scaled by the theme's `tempo` (`spec:AST-108` FR4). Under a coarse primary pointer the overlay paints on Button's own `::before`, present while pressed and through its fade-out, and a finger's, a mouse's or a pen's press fades over UIButton's measured release instead (250 ms at `tempo` 1, `timing.press.buttonReleaseMs`); a Space press keeps the 200 ms fade, as every press does with a fine primary pointer (`spec:AST-017` FR59). Without a reduced-motion preference, a standalone Button also scales to 0.98 on a mouse press, or while the touch press reads on; under reduced motion it MUST NOT scale (`design:reduced-motion` DR1). All of this paint follows the element's exposed disabled state: a Button that is natively disabled or exposes `aria-disabled="true"` (busy-only, disabled with a tooltip, or a caller's own `aria-disabled`) shows no overlay and no press scale, even when it stays operable. Inside a ButtonGroup the press scale does not apply. Button's hover and press transitions are fast. Under reduced motion they are instant with a fine primary pointer, and a release under a coarse primary pointer cross-fades within 200 ms instead (`design:reduced-motion` DR2, DR3). Implementation gap: the 0.98 press scale also applies under reduced motion; only its transition becomes instant. | Current source and tests; `spec:AST-094` FR11 and DEC-21 for the mouse and pen press |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This rewords a normative row of the current component:Button record (the 250 ms touch release on ::before, the 200 ms fine-pointer and Space fade, and the Reduce Motion split), so it needs an engineering owner's /approve-spec on this exact head. The record has also moved to docs/specs/components/Button/Button.spec.md on main, and this edit conflicts with that copy.

This branch was successfully deployed

1 active deployment
Preview — c5747687 Deployed Nov 3, 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 needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants