Repository navigation
feat(core): a checkbox and a radio option press the same way for every pointer (spec:AST-094 FR43) - #8696
feat(core): a checkbox and a radio option press the same way for every pointer (spec:AST-094 FR43)#8696vjeux wants to merge 1896 commits into
Conversation
…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.
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.
The note's reword joined its next sentence onto one long line; it now wraps like the rest of the file. No code change.
…w press release change
…election hold 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>
… 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.
…the press-paint base
…nto the fine-pointer press paint
There was a problem hiding this comment.
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
mainonce 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-motionDR3 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]
…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).
…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).
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.
d96069b to
c574768
Compare
There was a problem hiding this comment.
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
mainonce 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 withmainin 112 files, includingCheckboxInput.tsx,CheckboxListItem.tsxandRadioListItem.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), wheredesign:reduced-motionDR3 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 staysfading. - Three changesets now contradict the change (
checkbox-row-one-press-surface.md,indicator-press-release-compositor.mdandpress-control-profile.mdsay mouse presses are unchanged), and the change adds none of its own. - The coarse-pointer query is spelled twice (
utils/coarsePointer.ts:26besideutils/touchQueries.ts) and checked twice (stylesheetDroppedActiveinutils/pressFeedback.tsbesideisCoarsePointer); use one of each.
[Automated review]
| animationName: 'var(--_indicator-press-release, none)', | ||
| animationDuration: { | ||
| default: CONTROL_RELEASE, | ||
| '@media (prefers-reduced-motion: reduce)': |
There was a problem hiding this comment.
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)': |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
"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 |
There was a problem hiding this comment.
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 | |
There was a problem hiding this comment.
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.
Depends on #8233 and #8682 (and their predecessors). Review only this PR's change: vjeux/astryx@b809fd6...d96069b
User need
spec:AST-094FR43 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:activepaint.Problem and solution fit
:activearms go.:active:not(:has(…))arm and its--_radio-row-presspaint go (fix(RadioList): the pressed circle reads the row's press through a custom property, not a :has() scan #8424's rule, which it replaces). Its source-build test pins the new boundary.Current specification
spec:AST-094FR43, 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
pointerdownand 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:activearms.analyze-pr.js).Design and alternatives
Keeping
:activefor a mouse would leave two press models. Tests that pinned a coarse-only press are conformed, includingscripts/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
pointerdownand 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
Testing
CI=true pnpm lint0 errors;pnpm buildthen rootpnpm typecheckclean; IndicatorPressRelease, FinePointerPress, ButtonPressRelease and ListRowPress in Chromium 48 passed (1 skip); IndicatorPressRelease and FinePointerPress in WebKit 24 passed (3 skips); CheckboxListPress, PressModality and RadioGroup a11y 41/41; units 16,545 underCI=true, with no unhandled error; the load timeouts that test(Item): wait for the swipe layer's module, not a one-second window #8352, test(DropdownMenu): catch an eager sheet import when it is requested, not by a timer #8353, test(Resizable): load the package root while the file is collected, not inside a timed test #8471, test(Selector, MultiSelector): wait for the on-demand sheet module before the first open #8642 and test(DropdownMenu): the SSR test loads the menu and its sheet at collection #8685 fix failed on a loaded machine and each passes alone; a mouse click still toggles every story's checkbox and radio.CI=true pnpm lint0 errors;pnpm buildthen rootpnpm typecheckclean; IndicatorPressRelease, FinePointerPress, ButtonPressRelease and ListRowPress in Chromium 47 passed (1 skip); IndicatorPressRelease and FinePointerPress in WebKit 24 passed (3 skips); CheckboxListPress, PressModality and RadioGroup a11y 41/41; units 16,453 underCI=true, where the load timeouts that test(Item): wait for the swipe layer's module, not a one-second window #8352, test(DropdownMenu): catch an eager sheet import when it is requested, not by a timer #8353, test(Resizable): load the package root while the file is collected, not inside a timed test #8471, test(Selector, MultiSelector): wait for the on-demand sheet module before the first open #8642 and test(DropdownMenu): the SSR test loads the menu and its sheet at collection #8685 fix failed on a loaded machine and each passes alone; a mouse click still toggles every story's checkbox and radio.CI=true pnpm lint0 errors;pnpm buildthen rootpnpm typecheckclean; IndicatorPressRelease, FinePointerPress, ButtonPressRelease and ListRowPress in Chromium 47 passed (1 skip); IndicatorPressRelease and FinePointerPress in WebKit 24 passed (3 skips: pen input and Chromium-only cases); CheckboxListPress, PressModality and RadioGroup a11y 41/41; units 16,448 underCI=true, with load timeouts that test(Item): wait for the swipe layer's module, not a one-second window #8352, test(DropdownMenu): catch an eager sheet import when it is requested, not by a timer #8353, test(Resizable): load the package root while the file is collected, not inside a timed test #8471 and test(Selector, MultiSelector): wait for the on-demand sheet module before the first open #8642 fix; a mouse click still toggles every story's checkbox and radio.scripts/build-css.test.mjs23/23, including the narrowed desktop-rule case; the CLI files that timed out in the full run pass alone (13 files, 442 tests); the other node reds arestory-tree(main's, closed by test(storybook): run the story-tree check where Storybook is built, and bring the tree back to its shape #8509) andcache.test.mjs's npm-lock cases, which fail on main on this machine too.