Skip to content

fix(core): the cards' touch press release is the shared one - #9919

Draft
vjeux wants to merge 18 commits into
meta:mainfrom
vjeux:uikit/c01-cards-shared-press-release
Draft

vjeux wants to merge 18 commits into
meta:mainfrom
vjeux:uikit/c01-cards-shared-press-release

Conversation

@vjeux

@vjeux vjeux commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #9890 and #8326 → this pull request. Review only the top commit, "fix(core): the cards' touch press release is the shared one". The commit below it merges #8326 into #9890.

User impact

This affects people who press ClickableCard, SelectableCard or Thumbnail with a finger.

  • With fix(core): no press scale or card color fade under reduced motion #8326 and fix(core): a press's release fades on the theme's exit motion tokens #9890 both merged, before this change: under the touch presentation the cards' release fades on the theme's exit tokens, capped at a copy of the press controller's old clock. With a fine primary pointer, a finger's release on a card keeps that old clock and curve, while other touch press releases fade on the exit tokens (spec:AST-094 FR5).
  • After this change: a touch press's release on the three cards fades on --duration-exit and --ease-exit, like other touch press releases, also for a finger with a fine primary pointer. The press controller's clock reads the same token, so there is nothing left to cap.
  • Unchanged: the press shows at once. A mouse's hover and press on the cards keep their desktop press styling (compared with main below). Under Reduce Motion a finger's release fades for 200 ms or less, and only opacity fades on a touch screen.

Problem and solution fit

Expected behavior and authority

Smallest restoration

  • ClickableCard.tsx, SelectableCard.tsx and Thumbnail.tsx: the SHARED_RELEASE* constants, styles.touchRelease and its composition are removed, and the import is {interactionOverlayStyles} again. fix(core): no press scale or card color fade under reduced motion #8326's Reduce Motion arms on the overlay, the border and the selected root stay.
  • Tests:
    • utils/sharedPressRelease.test.ts (the sync test for the copies) is deleted;
    • hasCardReleaseFade leaves __tests__/pressState.ts, which is then fix(core): a press's release fades on the theme's exit motion tokens #9890's file;
    • the three card unit tests use hasReleaseFade again, as on main;
    • in ReducedMotionPress.a11y.chromium.spec.ts, the fine-pointer case expects a finger's release on the exit tokens.
  • Records: each card's FR clause takes the spec owner's wording above, without the cap. The Compatibility bullet and readiness row cite the card's FR and spec:AST-094 FR5, and the bullet says a mouse press keeps the card's desktop press styling. Governing rules add spec:AST-094/FR5.
  • Changeset: [fix] for a finger's release with a fine primary pointer.

Rebase

When #8326 and #9890 merge, this branch rebases onto main and keeps only the top commit.

Overlaps with open pull requests

Writing rubric results

Run over the three changed records: component:ClickableCard, component:SelectableCard and component:Thumbnail.

  • R1 narration: none. The scan for proposes, we decided, previously, a port of and now finds nothing. The changed FR clauses, Compatibility bullets and readiness rows are present tense.
  • R2 research: none in the records.
  • R3 evidence: no verification or basis cell names a pull request, issue or commit.
  • R4 rejected alternatives: none in these records.
  • R5 change references: none (no # followed by digits, /pull/ or /issues/).
  • R6 open questions: component:ClickableCard OQ1 is unanswered and keeps its two options. The other two records have none.

Evidence

  • Reproduction before the change: on the merge commit (fix(core): no press scale or card color fade under reduced motion #8326 and fix(core): a press's release fades on the theme's exit motion tokens #9890 together, without this commit), this commit's tests fail. Units: 4 of 4 (each card's touch release case, and sharedPressRelease.test.ts's check that the copies match the shared module). Browser (ReducedMotionPress.a11y.chromium.spec.ts on a Storybook built at the merge commit): the fine-pointer case fails on a finger's release (old clock and curve); the other 5 pass.
  • Result after the change: at this head, ReducedMotionPress.a11y.chromium.spec.ts 6/6, CardPress.a11y.chromium.spec.ts 9/9 and Button's accessibility contract 20/20, on a Storybook built at this head.
  • Representative unchanged path: desktop hover and mouse press on the three cards. Compared with main's own Storybook build (main f10bc91, whose card code is fix(core): no press scale or card color fade under reduced motion #8326's base), at 1024×768 with a fine pointer: at rest, on hover, pressed, on the first frame after the release and settled after it, each card's screenshot differs from main's by 0 pixels, and the surface's and overlay's computed colors, transitions and animations are equal. That holds with animations frozen (rate 0) and with them settled. A positive control on main's build shows the comparison sees state: rest to hover changes 4,016–20,223 pixels per card, and hover to pressed 4,018–19,523. Local probe, not part of the diff.
  • Regression test or other mutation-sensitive proof: the reproduction above. The card unit tests are main's again, and hasReleaseFade holds them to the shared release.

Touch frames against #8326 and #9890

Local probe, not part of the diff. Real Chromium with phone emulation (402×714, touch, coarse primary pointer), one real tap on each card through touch input. When the press controller writes the release, the probe pauses the release animation and seeks it to each time below (the controller holds the release until that animation finishes, so nothing ends early). At each time it reads the surface's --astryx-press-alpha, the overlay's computed paint, and a screenshot of the card. The comparison build is the merge of #8326's and #9890's heads. Measured at this pull request's earlier head 94b1c01, against that merge (05ccb64); this head rebuilds the same fold on #9890's amended head, whose change to the press code keeps the same release clock.

Card Reduce Motion Release clock, both builds Strength at 0 / 25 / 50 / 100 / 150 ms, both builds Pixels that differ, this pull request against #8326 + #9890
ClickableCard off 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
SelectableCard off 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
Thumbnail off 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
ClickableCard on 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
SelectableCard on 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0 in most rounds; see the note
Thumbnail on 125 ms 1 / 0.921 / 0.725 / 0.222 / 0 0 / 0 / 0 / 0 / 0
  • The overlay's computed paint is equal at every time, on every card, with Reduce Motion on and off.
  • With Reduce Motion on, the release is also 125 ms on both builds: the theme's --duration-exit is 125 ms, under the 200 ms bound.
  • SelectableCard with Reduce Motion on: in some rounds, 24 pixels (at 0 ms) and 10 pixels (at 50 and 150 ms) differ inside the card. The same pattern appears when a build is compared with itself: in 8 repeated rounds it shows in fix(core): no press scale or card color fade under reduced motion #8326 + fix(core): a press's release fades on the theme's exit motion tokens #9890 against itself 3 times, this pull request against itself 4 times, and the two builds against each other 4 times. So it comes from the story's own run-to-run rendering, not from the release. The strength values are equal in every round.
  • Positive control: on the comparison build, each card's screenshot at 0 ms differs from its screenshot at 150 ms by 4,026–20,665 pixels, so the probe sees the release.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

vjeux and others added 16 commits November 3, 2026 06:48
Under a finger, the touch press controller ended a press's release on a
clock that starts at the lift. When script delays the first frame of the
release animation, the animation ends later, but the clock does not, so
the highlight was cut off mid-fade. When the clock runs out, the
controller now waits for a release animation that still runs or still
waits for its first frame: on the surface, its ::before or ::after, or on
a part inside it whose nearest press owner is the surface. With none, the
release ends at the clock, as before.

Rebased onto main after meta#8360 landed: main's theme tempo already caps the
release under Reduce Motion, so only the release-ends-with-its-animation
part remains.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…animation

CodeBunny's notes on the first round: pressGesture.ts still described
PRESS_FADE_MS as the timer that removes the attribute, and two new
comments in pressFeedback.ts had sentences over 25 words.
Under a finger, a pressed ListItem, actionable Item or TreeList row now
paints its press on a ::before layer under the row's content and fades
that layer's opacity, which the browser runs off the main thread. The
row keeps fading while the tapped page runs script, instead of staying
pressed and then snapping off. A mouse or trackpad row is unchanged.

Rebased onto main and meta#9758 (which supersedes meta#8890):
- The row layer's styles read the private press attribute, so they live
  in its owner module, privateAttributes/press.stylex.ts, as INV23 and the
  no-raw-private-attribute rule require.
- The layer's release clock follows the theme's tempo and its Reduce
  Motion cap, the same clock as the strength's release on main.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
…aits only inside a scroller

UIKit delays a touch only inside a scroll view. Under a finger, a
pressable now highlights in the frame the finger lands unless a
scroller sits above it, where it still waits the 150 ms content-touch
delay. Rows follow the same rule. The controller walks up the flat tree
from the touched surface for a box that can scroll on an axis
touch-action leaves to the browser, and a fixed box ends the walk.

Rebased onto main and meta#9762 (which supersedes meta#8077):
- Main's shortened file headers are kept, with the scroller rule added in
  short sentences.
- The controller writes the press attribute only when the paint changes,
  as the original base did, so a held press is one change.
- The busy-main-thread row test reads the row at rest from its last frame:
  a row outside a scroller now paints in the first frame.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
… and WebKit skips the first-frame case

CodeBunny's first round on this pull request:
- RadioListItem set the press layer's ::before content again on Item's
  root. The owner module now exports cellWithoutLayer, which also leaves
  a pressed radio row out of a stacking context.
- WebKit starts the release at the time busy script began, so the
  first-frame case skips WebKit and records the value it measured.
…obe share one scroll-view test

CodeBunny's first round: the controller's walk copied the gesture
core's native-scroll probe. isInsideScrollView now lives in
utils/scrollView.ts, with parseTouchAction and the root-overflow test,
and both the controller and the probe's isInsideScroller call it. The
changeset says a page that scrolls keeps the delay.
Specs' text under the owner's 2026-10-25 decision: under the touch
presentation an interactive row uses its ::before as the press layer, a
pressed row's divider keeps its resting colour, and a caller's own
::before merges into the layer only while the row is pressed. DEC-6
records the decision and answers OQ1, and the compatibility row says the
compositor release is implemented.
…le, and the list table entry goes

CodeBunny's second round on this pull request:
- INV23: cellWithoutLayer now carries the radio row's empty image and its
  strength release, so RadioListItem no longer sets those beside cell, and
  swipeRoot drops isolation (its transform already isolates the row).
- motion/uikit/list.ts only re-exported timing.press, so it goes with its
  index, README and MEASUREMENTS rows; cellPressConsts is held to
  timing.press directly. This brings the change under 2,000 lines.
- WebKit: the busy-first-frame end case asserts only the end and the order
  of frames there, since WebKit's first frame can already be near rest.
Main's meta#8638 renamed the release clock helper fadeMs to an exported
pressFadeMs. The release call here now uses pressFadeMs; nothing else
in this pull request changes.
Under reduced motion a press never scales a Button, and ClickableCard,
SelectableCard and Thumbnail change their hover and selection colors
at once on every pointer.

Under a coarse primary pointer the three cards' press release fades on
the theme's exit duration and easing, capped at the press controller's
release clock (component:ClickableCard FR5, component:SelectableCard
FR4, component:Thumbnail FR4). The press still shows at once. Under
reduced motion that fade takes at most 200 ms, opacity only. A fine
primary pointer keeps the timing it had for hover and a mouse press.
Every press release that fades now plays `--duration-exit` and
`--ease-exit` instead of 200 ms, scaled by the theme's tempo, on a curve
of its own: the shared overlay's touch release, the controller's release
clock (which action groups read too), Switch's press layers and the list
row layer. A desktop pen press, which the controller fades, moves to the
tokens too. Under Reduce Motion a finger's release is no longer than
200 ms, as before. A mouse press on a desktop is unchanged.
@vercel

vercel Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 4, 2026 10:02am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 4, 2026
@astracat-bot

astracat-bot Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 4, 2026, 10:21 AM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Nov 4, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
vjeux added 2 commits November 4, 2026 01:46
scripts/build-css.test.mjs keeps meta#9890's version. meta#8326's checks of the
cards' exit-token arms leave with those arms in the next commit.
ClickableCard, SelectableCard and Thumbnail drop their copies of the
press release and compose the shared pressedAlpha alone. Under the
touch presentation it fades on the exit tokens, and the press
controller's clock reads the same token, so the cap goes too.

With a fine primary pointer, a finger's release on these cards now
fades on the exit tokens, as other touch press releases do. A mouse
press keeps the cards' desktop press styling.
@vjeux
vjeux force-pushed the uikit/c01-cards-shared-press-release branch from 94b1c01 to 772101a Compare November 4, 2026 09:53

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

This pull request needs an engineering owner's spec decision before it can be approved, and it needs changes either way.

Spec decision. As filed against main, the branch rewrites component:Button FR15 (a 200 ms × tempo release) and design:reduced-motion DR2 (colors cross-fade on a touch screen), and it adds requirements main does not state: spec:AST-094 FR5's exit-token sentence, the press-release clauses in component:ClickableCard FR5, component:SelectableCard FR4 and component:Thumbnail FR4, component:List FR11 with DEC-6 (closing OQ1), and design:reduced-motion DEC-4. An engineering owner records the decision by commenting /approve-spec 772101aed922045524391d467b3e1c4d32982b30 on this pull request (the author may do so as an engineering owner), or the records can be approved in their own pull requests first.

Required changes:

  • Rebase onto main, because the branch conflicts with it and the auto-merged packages/core/src/utils/pressFeedback.ts declares releaseAnimationsOf and endAfterRelease twice, since main already holds #9758's change.
  • .changeset/cards-touch-release-exit-tokens.md promises a release "capped at the press's release time" under a coarse pointer, but the head plays the shared release on every pointer with no cap.
  • .changeset/cards-touch-release-fine-pointer.md announces a fix for a fine-pointer finger release that main never shipped, because the cards there already use the shared release.
  • .changeset/list-row-press-compositor.md says the row release keeps "the same color, clock and curve as before", while the row now plays --duration-exit and --ease-exit.
  • Miss: ButtonPress.interaction.browser.spec.ts, ListRowPress.interaction.browser.spec.ts and ListRowPressPaths.interaction.browser.spec.ts import test from @playwright/test, which the lint config forbids in browser specs, so pnpm lint fails until they import from @astryxdesign/a11y-spec/playwright.
  • At 4,421 authored lines from six separable increments, the pull request should carry only its top commit (rebase after #8326 and #9890 land) or become a declared stack in which every pull request targets main.
  • component:Button FR15 still lists "the 0.98 press scale also applies under reduced motion" as an implementation gap, though this change fixes it.
  • .changeset/press-release-exit-tokens.md credits no contributor, so pnpm check:changesets fails.

The private duration parser and the unused test import noted earlier are fixed.

[Automated review]

'@astryxdesign/core': patch
---

[feat] Under a coarse primary pointer, the press release of ClickableCard, SelectableCard and Thumbnail now fades on the theme's exit duration and easing, capped at the press's release time.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At this head the cards play the shared pressedAlpha release on every pointer, with no cap, so this sentence describes a release that no longer ships. press-release-exit-tokens.md already names the cards; this changeset can go.

'@astryxdesign/core': patch
---

[fix] With a fine primary pointer, a finger's press release on ClickableCard, SelectableCard and Thumbnail now fades on the theme's exit duration and easing. A mouse press is 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.

On main the cards already compose pressedAlpha alone, so there is no fine-pointer card release to fix in a released version; press-release-exit-tokens.md covers the change.

'@astryxdesign/core': patch
---

[fix] A list row's touch press release keeps fading while the page runs script. `ListItem`, `Item` and `TreeList` rows used to stay fully pressed after the finger lifted until the main thread was free, and every frame of the fade repainted the row; the release now runs on the compositor, with the same color, clock and curve as before. Under a finger, while a row is pressed and through its release, the press paints on the row's `::before` and the row is its own stacking context; at rest nothing of it applies, so a row's own `::before` decoration and its children's stacking are as they were. During a press the layer is the row's `::before`, and any `::before` property a caller sets, from `xstyle` (which `Item` composes after the layer) or from a stylesheet, wins over the layer's: a decoration with its own size and background jumps to the row's corner, and the row shows no pressed fill until the release ends. Decoration that should stay through a press belongs on `::after` or a child element. With a mouse or trackpad the row is unchanged: no layer and no new stacking context.

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 row layer plays --duration-exit and --ease-exit (CELL_RELEASE, RELEASE_EASE in privateAttributes/press.stylex.ts), not the same clock and curve as before. Suggested: "on the theme's exit motion tokens".

'@astryxdesign/core': patch
---

[fix] A press's release now fades on the theme's exit motion tokens, `--duration-exit` and `--ease-exit`, instead of 200 ms, scaled by the theme's `tempo`, on a curve of its own. This covers every release that fades: a finger, mouse, pen or key press on a phone or tablet, and a pen press on a desktop, on buttons, cards, switches, checkboxes, list and tree rows, tabs, menu items and action groups. With the default tokens the release is 175 ms and holds its colour a little longer before it goes. A theme that changes `--duration-exit` or `--ease-exit` now changes the release too; a theme's `tempo` still does, through `--duration-exit`. Under Reduce Motion a finger's release still fades for no more than 200 ms. A mouse press on a desktop is 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.

This body credits no contributor, so pnpm check:changesets fails: add an @handle line.

| 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). 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, and instant under reduced motion. 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 |
| 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 on release on the theme's exit motion tokens (`--duration-exit`, `--ease-exit`; `spec:AST-094` FR5). 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, and instant under reduced motion. 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.

The row still ends with "Implementation gap: the 0.98 press scale also applies under reduced motion", but Button.tsx now adds the reduced-motion scale(1) arms and this record drops the matching Current-state bullet. Delete the sentence.

* (utils/pressFeedback.ts, as installed by hooks/usePressFeedback.ts)
*/

import {expect, test, type Locator, type Page} from '@playwright/test';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

eslint.config.js forbids @playwright/test in browser specs (no-restricted-imports); import test and expect from @astryxdesign/a11y-spec/playwright, as Switch.interaction.browser.spec.ts does.

*/

import {readFile} from 'node:fs/promises';
import {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same lint error as the Button spec: import test and expect from @astryxdesign/a11y-spec/playwright.

* ListRowPress.interaction.browser.spec.ts
*/

import {expect, test, type Locator, type Page} from '@playwright/test';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same lint error: import test and expect from @astryxdesign/a11y-spec/playwright.

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 772101a

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026

This branch was successfully deployed

1 active deployment
Preview — 772101ae Deployed Nov 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants