Repository navigation
Conversation
…than the band (spec:AST-094 FR18, DEC-22) FR18 as amended in meta#8631 (DEC-22): the band's pan-x claim and 15 px start hold only where the screen-edge pan is attached to an element within the band; on an element that reaches beyond it, whose touch-action cannot claim per touch, a touch in the band decides at FR19's 10 px like every other claim. This restores the 10 px decision 3d85c30 removed and keeps 15 px on an element within the band. The cases pin DEC-22 both ways (a wide element with a touch at x=4 begins at 10, an in-band strip at 15, and from several edges at 10 on a wide element); the 10 px cases fail on 3d85c30.
…ads its end before the arena's reset) into meta#8336
The case walked ContextMenu, DropdownMenu, useLongPress and useMenuPress's imports and failed on the UIKit gestures table: it kept that table out of the menus' bundles, the lazy design the no-lazy-load ruling and direct imports retire. The long-press numbers keep one source, still pinned by the table, recognizer and hooks case and by the hooks reaching longPress.ts.
…xposes no ontouchstart is taken once Chromium fixes ontouchstart when the page is created, so turning touch emulation on later sends Touch Events without it: the host reads Pointer Events for the finger and must not read its Touch Events too. A one-finger long press fails here, the finger tracked twice.
…from their pointer events install() reads a finger from its pointer events when the window has no ontouchstart, but the four touch handlers still took the Touch Events a browser sends anyway (Chromium after touch emulation turns on), tracking the finger twice, so a one-finger long press never recognized. Each touch handler now returns while usesTouchEvents is false, after noting the fingers down, as install()'s comment promised.
…ter events) into meta#8320
…ter events) into meta#8293
…ter events) into meta#8356
…ter events) into meta#8358
…d tracking listeners) onto F2's fZ (meta#8360 23ade34: the controller on the shared input stream, FR24's touch-only gate) Conflicts in utils/pressFeedback.ts and its test, resolved as one controller: meta#8118's structure (a finger slot and a button slot; two arming listeners at rest, the rest attached while a press is tracked) with fZ's listener plumbing. Pointer, touch, drag, scroll, scrollend and visibility input come through subscribeInput on the window's capture phase; the keyboard stays on the window's bubble phase, outside the stream, so a Space press sees the target's preventDefault. The finger's touch-only listeners (touchend, touchcancel) are on only while a press is tracked and the primary pointer is coarse, following it (FR24), and a finger counts as down only where its lift is heard. Tests: meta#8118's brake and listener cases run on a coarse primary pointer and read the stream's window listeners; fZ's touchcancel case dispatches an event with an empty touch list, as a browser's has.
…n before the click guard internal/motion.ts (meta#8562) re-exports UIKIT_CLICK from the barrel, so a tree carrying both fails typecheck and the Storybook build since this pull request dropped it. The barrel keeps it, with the click guard's window, and the table of every gesture group still leaves it out. Fails until the barrel exports it.
…'s window This pull request dropped UIKIT_CLICK when the click guard took the stray-click window over, but importers written before it read the barrel's UIKIT_CLICK (internal/motion.ts in meta#8562 re-exports it), so a tree carrying both failed typecheck and the Storybook build. UIKIT_CLICK stays in the barrel, deprecated, with the guard's 400 ms (CLICK_GUARD_WINDOW_MS), and UIKIT_GESTURES still leaves it out.
…ter events) into meta#8360
…ter events) into the every-pointer host # Conflicts: # packages/core/src/gestures/__tests__/GestureHost.test.ts
…ame window Both 'a dip held before its first frame lands once the hold is …' proofs waited for no running animation, then one frame and a timeout, assuming the next step starts within a frame. The dip lands on a timer due on the call's own clock, though, and the proofs passed only because the evaluate round trips covered the gap: in WebKit on a loaded box the window closed first and the proof read the element mid-dip (opacity 0, translate none; reproduced 1 in 4 with the round trips removed). They now wait on the call's own finished, then on the browser's own animations still running or waiting for their first frame, and read nothing from the animator.
…ment at once and settles then A Reduce Motion dip whose landing runs more than its whole fade back in late (a hold released long after the dip was due) played a 1 ms opacity animation, which WebKit keeps pending until its next frame, and settled the call on a 1 ms timer: the call's finished could resolve while the element still showed opacity 0. The animator's notes say the element appears at once there. Fails now: the landing plays one opacity animation and the call has not settled.
…or the Space key's 200 ms, a list row's 200 ms Measures each release on a touch-first device (component:Button FR15): the release animation's own duration, read when `fading` is written; `fading` lasting at least that clock; and `fading` going within a few frames of the animation's end. Frames, not ms, bound the end, so a loaded machine cannot fail it. Passes in Chromium and WebKit on this head, before the fold onto meta#8117's new head, and is kept through it as the proof that the fold changes no timing.
…#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.
…element at once and settles A Reduce Motion landing that runs at least the whole fade back in late had nothing left of the cross-fade, but still played a 1 ms opacity animation and settled the call 1 ms later. WebKit keeps that animation pending until its next frame, so the call's finished could resolve with the element still at opacity 0. Such a landing now writes the end opacity at once (handing a borrowed one back to the stylesheet), plays nothing, and settles the call then, as the animator's notes say.
…ot the timing table From review (an inline comment on meta#8077 about this line): the press controller, which every pressable component loads, imported the whole UIKit timing table for one value, its reduced-motion cap, while architecture:component-style-authoring INV6 ships a component only the rows of a shared table it uses. It now reads the fade's own export, reducedMotionCrossFade.durationMs, as the animator does; the value is the same 200 ms. Red first: a bundle of pressFeedback.ts carried the table's other rows (stateChangeMs, buttonReleaseMs) on the change before this one; it now carries the fade alone.
…th dividers drawn From review on meta#8077: component:List FR11's verification asks for a pressed row between two divided rows, and no case used a list with dividers. A touch case opens the Interactive story with hasDividers, holds the middle row and releases it, and checks the divider above it and its own divider below, by computed border and by the pixels of each divider's strip, while held, halfway through the release fade and after it; each strip must differ from the row's surface just inside it at rest, so a match is a drawn divider. It passes in Chromium and WebKit. A page style that hides a pressed row's divider fails it while the row is held.
…duce Motion constant meta#8077's FR4 check (interactionOverlay.cell.test.ts) asks every release cap in the overlay to read the Reduce Motion constant through a min(). This branch's reduced-motion arms used the constant directly; they now take min(<the release's clock>, the constant), so a control's 250 ms and a mouse's or the Space key's 200 ms both cap at 200 ms, as before. The build's reduced-motion check reads the cap.
… or press profile The selection hold and the press profiles are gone from the base, so the Item case and the shared checks' notes no longer mention a held arm, a cell marker or a profile clock; the checks themselves are unchanged.
… judges its first frame on that frame's time 'a spring started right before a block plays in full after it' waited a fixed 300 ms of wall clock after the block, then read the screencast. In the full interaction run on a loaded box no moving frame had reached the screencast by then, and the proof failed on an empty read. It now waits until the page shows the box a quarter of the way along and three more frames reach the screencast. Its bound changes form, not strength: the curve's own start (the handle's started) must come after the block, and the first moving frame may be no further along than a curve started a frame before the block ended, at that frame's own time, which is the old 30 px for a frame right after the block and still holds when a loaded machine sends frames sparsely. A curve dated at the call is 107 px in.
…into the fine-pointer press paint Carried regions take the base's text. This branch's own controller changes are re-applied on the new controller: a mouse's or a pen's press paints at once through it on every device, a pen never waits or brakes, a pen's barrel button and a nested control take no surface press, and only a Space press carries the input marker, so a mouse's release on a touch-first device is a finger's (spec:AST-094 FR11, FR43). The press profiles are gone from the base, so the clocks read one role each: the release's the deselect's (timing.press.highlightOutMs), a longer release animation being waited for, and the flash its own. The selection-hold spec this branch edited is gone with the hold.
…e surface releaseAnimationsOf counted only the press owner's own animations (on it, or its ::before or ::after). A component that draws its release on a descendant, such as CheckboxInput's release on its box wrapper's ::after, then lost the end of it: fading went at the 200 ms clock while the 250 ms release still ran, and the frame it went relaid and repainted the box. It now also counts a CSS release animation on a descendant whose nearest press owner (closest [data-astryx-pressable]) is this element, the same nearest-owner rule the layers read by, so a press owner nested inside plays its own press's release and never extends this one. Red first: a unit case whose release runs on a box inside the row found fading gone at the clock on the change before this one; it now holds to the release's end. A second case keeps a nested press owner's release, on it or inside it, out of the row's.
…ts own timeline One run measured a 250 ms release whose `fading` lasted 241.6 ms on the page's clock: an animation's start is a frame's time, which can come before the write, so a bound on the page's clock read a whole release as short. The check now takes the animation's start from its own timeline (when it is ready) and the timeline's time when `fading` goes, and asks that it ran to its end; WebKit drops the animation at the removal, so its progress cannot be read there.
…ming table The flash and release clocks resolved their roles from the timing table's press group, which bundled the whole group into the controller: meta#8890's check that the controller ships the Reduce Motion fade and none of the table's other rows failed. They now resolve the same keys from a local table holding the two values the timing table does (100 ms, and the stylesheet's 200 ms release), so the clocks read their roles and only those values ship (architecture:component-style-authoring INV6). [skip ci]
There was a problem hiding this comment.
This is on hold for an engineering owner's decision on two spec points: the rewrite of component:Button FR15 to a 250 ms release for every pointer under a coarse primary pointer (with a new verification row), where FR15 on main gives every press 200 ms, and the new rule that a press on a nested link or button leaves its row unpainted, which no current spec states. An engineering owner records each decision by commenting /approve-spec <full head SHA> on this pull request, or by naming the governing spec; the author may do so as an engineering owner. Independently of that decision, these need changes before merging:
surfaceAttakes a row's first control as its activator, so a TreeList parent row withonClickorhrefshows no press when its label is pressed (packages/core/src/utils/pressFeedback.ts:632-640).- After a desktop click the hover overlay stays hidden until the release clears about 200 ms later, and under Reduce Motion the hovered control shows its resting fill for that time (
packages/core/src/utils/interactionOverlay.stylex.ts:324-325,:346-347). usePressFeedback.doc.mjs(lines 23, 28 and 61), the buttons family's press row, the stackedpress-touch-down-highlightchangeset and the input-marker comments still describe the old desktop press.- The diff against
maincarries the unmerged stack under #8665 and #8118; please land those and rebase until this pull request is only its own 15 files.
[Automated review]
| control != null && | ||
| control !== found && | ||
| found.contains(control) && | ||
| control !== found.querySelector(ACTIVATOR) |
There was a problem hiding this comment.
The first control in tree order is not always the surface's activator. A TreeList parent row renders its expand toggle before its own button or link, and an Item renders startContent before its invisible button, so a press on those rows' labels returns null here and the row shows no press, while a press on the toggle paints the whole row. Could the surface mark its activator explicitly? A test with a control before the activator would hold it.
| ':hover': hoverImage, | ||
| ':active': pressedImage, | ||
| }, | ||
| ...pointerImageArms, | ||
| [PRESSED_ON]: pressedOverlayImage, | ||
| [PRESSED_FADING]: pressedOverlayImage, |
There was a problem hiding this comment.
With a hovering mouse this fading arm replaces the hover image with a press paint that fades to nothing, so after a desktop click a hovered control drops below its hover overlay within about 40 ms and gets hover back only when the attribute clears at about 200 ms; under Reduce Motion it shows its resting fill for those 200 ms. Keeping the hover layer under the fading press for a hover-capable pointer would avoid it.
| }, | ||
| ], | ||
| usage: { | ||
| description: | ||
| 'The touch press model every Astryx pressable uses, for a local component that paints its own press. Under a finger, CSS `:active` paints on the touch itself and can outlive the start of a scroll; the press model instead waits 150 ms before believing a press, cancels it the moment the finger travels 10 px or a scroll claims the gesture, never brings it back inside that gesture, answers a quick tap at the lift, and fades the release over 200 ms — the clocks a native list uses. One document-level controller does this for every marked element (installed by the first pressable to mount, removed by the last to unmount), so calling the hook adds no listener or state to your element. On a desktop a mouse press is still your `:active` rule: keep it, and drop it under `@media (pointer: coarse)` the way the built-in components do. That query reads the primary pointer, so on a phone or tablet it also drops `:active` for a connected mouse, trackpad or keyboard; the controller paints those presses there (at once, faded at the release), so nothing goes unanswered.', | ||
| 'The touch press model every Astryx pressable uses, for a local component that paints its own press. Under a finger, CSS `:active` paints on the touch itself and can outlive the start of a scroll; the press model instead tracks the touch the way UIKit tracks a control: it highlights as the finger lands (after the content-touch delay inside a scroller, where a flick paints nothing), a quick tap answers at the lift, and a scroll that takes the touch cancels it with no fade. The release fades over the 250 ms of a UIButton (ease-in-out) under a coarse primary pointer and over 200 ms with any other; under reduced motion it is an opacity cross-fade of 200 ms or less on a touch screen and immediate otherwise. One document-level controller does this for every marked element (installed by the first pressable to mount, removed by the last to unmount), so calling the hook adds no listener or state to your element. On a desktop a mouse press is still your `:active` rule: keep it, and drop it under `@media (pointer: coarse)` the way the built-in components do. That query reads the primary pointer, so on a phone or tablet it also drops `:active` for a connected mouse, trackpad or keyboard; the controller paints those presses there (at once, faded at the release), so nothing goes unanswered.', |
There was a problem hiding this comment.
This still tells builders "On a desktop a mouse press is still your :active rule: keep it", and lines 23 and 61 say the same, but the controller now paints a desktop mouse press itself, as line 38 says. Could these three strings describe the controller's mouse and pen press?
| | 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 gives a mouse's and a pen's release under a coarse primary pointer UIButton's 250 ms and a coarse-pointer cross-fade under reduced motion; FR15 on main gives every press 200 ms and an instant reduced-motion transition, so the rewrite needs an engineering owner's decision on this head.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [fix] Under a finger, a pressable (a button, card, link, list row or any other surface) now highlights in the frame the finger lands, as UIKit's controls do, instead of after a 150 ms delay. Inside a scroller it still waits that delay, so a flick paints nothing, and a scroll that takes a held press cancels it with no fade. A mouse on a desktop keeps `:active`. |
There was a problem hiding this comment.
"A mouse on a desktop keeps :active." stops being true once press-fine-pointer.md ships in the same release.
| const CONTROL_RELEASE_EASE = controlPressConsts.controlReleaseEase; | ||
| /** | ||
| * A mouse or Space press the controller paints on a touch-first device | ||
| * (pressFeedback.ts `PRESS_INPUT_ATTRIBUTE`): it keeps the fine pointer's |
There was a problem hiding this comment.
pressButton now writes this marker only for a key, so a mouse press on a touch-first device no longer keeps 200 ms here; the same note at pressFeedback.ts:28-31 and :113-119 and interactionOverlay.stylex.ts:255-256 is stale too.
…n prelude StyleX 0.19.0's dev runtime keys a conditional rule whose prelude has more than one parenthesized condition by the prelude alone, so jsdom got only the first rule written under, say, @media (prefers-reduced-motion: reduce) and (pointer: coarse), and a test that read a later one read nothing. vitest.config.ts now names internal/test-utils/src/stylexInject.ts as the babel plugin's runtimeInjection: StyleX's own inject, plus each rule it drops, inserted once into a sheet of its own. Test-only: no StyleX code is patched, and the build keeps every rule as before. It comes out at the StyleX bump to a release that carries meta/stylex#1991, which fixes the key upstream. Red first: the new stylexMultiConditionMedia test (two styles, and one style with two declarations, under one two-condition query) failed with StyleX's own injection; on meta#8226 + meta#8682 + main, Indicator/selectionMotion.test.ts failed 2 of 6. Both pass with it, and the full ui suite on main has the same results (15,951 passing, plus the new test).
Stack: on #8665 and #8118. Review this PR alone: vjeux/astryx@uikit/c01-press-paint-base...uikit/c01-press-paint-fine
User need
Part of the UIKit-parity work, which makes Astryx's presses behave the same on every device. With a mouse, a button's pressed color vanishes the instant you let go; under a finger it fades out over 200 ms. A pen waits 150 ms before a list row shows its press, like a finger.
Problem and solution fit
What led to it: #8646 asks for one press for every pointer (
spec:AST-094FR43), but the press controller handled only fingers, so a mouse fell back to CSS:active, which has no release to fade.Now the mouse's main button and the pen go through it: the press shows at once, holds while down and fades on release. On a phone or tablet a mouse's release is a finger's (a control's 250 ms); a Space press keeps 200 ms. A right click, a Mac ctrl-click and a pen's barrel button show nothing; a link in a pressable row presses itself, not the row.
Current specification
spec:AST-094FR11, FR43, DEC-21;component:ButtonFR15.Public delta and caller burden
No API change.
Design and alternatives
:active: CSS can't animate a state that has ended.Testing
To check by hand, in Storybook on a laptop:
What was run: a new browser test (
FinePointerPress.interaction.browser.spec.ts) does 1, 2, 3 and 5 with a real mouse, in Chromium and WebKit. Before this change 6 of its 8 checks fail; after, all pass. Unit tests inutils/pressFeedback.test.tscover dragging off, pens and the ctrl-click;utils/pressFeedbackMotionRoles.test.tscovers the flash and release clocks reading their roles. The existing press tests pass: the targeted interaction specs (Chromium 49 pass, 1 skip; WebKit 36 pass, 14 skip, twice each), the press a11y specs (37),pnpm test:ui(16,853 of 16,855; the two are MultiSelector's and Selector's lazy bottom-sheet cases, a load red #8642 fixes) and the node project, whose only failures fail the same way on main locally (story-tree.test.ts,release/cache.test.mjs's lock cases). The local gates are the evidence: pushes carry[skip ci], so no CI run is expected.Evidence
Scope
Writing rubric results
component:ButtonFR15 now covers the mouse and pen. R1–R6 pass: no narration, research, references, rejected alternatives or open questions.