Skip to content

fix(core): a card's press overlay paints a finger's press - #8057

Merged
cixzhang merged 3 commits into
meta:mainfrom
vjeux:uikit/c01-card-press
Oct 24, 2026
Merged

cixzhang merged 3 commits into
meta:mainfrom
vjeux:uikit/c01-card-press

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

User impact

In apps compiling Astryx from source with cascade layers (the Vite and PostCSS integrations, Storybook), a finger held on a ClickableCard, SelectableCard or interactive Thumbnail showed no pressed state; the precompiled stylesheet painted it. Now every build paints it on the first frame and fades it at the lift; mouse states are unchanged.

Finger held, then lifted (iOS 26.2) Before After
ClickableCard

Problem and solution fit

  • Why it occurs: the cards wrote the touch press as an attribute arm of a custom property ('--_press-overlay': {default: 'transparent', '[data-astryx-press="on"]': …}). StyleX's layered output leaves its lowest priority group (every custom property's own declarations) unlayered and puts attribute arms in a priority layer. Unlayered wins whatever the specificity, so the default always won.
  • Why it harms: the finger gets no confirmation; a selection card toggles on release with no feedback during the hold.
  • Why it matters: one source painted differently by build, and jsdom (no cascade layers) passed.

Plain ::after properties keyed on the attribute now paint it, outranking the defaults in layered and flat stylesheets alike.

Expected behavior and authority

  • The cards' docs: the overlay paints the pressed token under a finger (data-astryx-press="on").
  • usePressFeedback docs: a marked surface paints the pressed token on the first frame of a believed press and fades it over the release.
  • History: the touch press model added these arms, asserted only in jsdom.

Smallest restoration

  • ClickableCard, SelectableCard, Thumbnail: the custom-property press arms and --_press-overlay-transition go; on the press arms the ::after gets background-image: var(--_press-paint-image), background-color: transparent (so a fine pointer's :active cannot paint twice) and transition-property: none.
  • No API, token or mouse-path change.
  • Touches neighboring press code and ci.yml with fix(core): a mouse or Space press on a touch-first device paints the pressed state #8046; whichever lands second rebases.

Evidence

  • Before: in Storybook a held touch leaves each card at rest (255, pressed is 229); on the iOS simulator the controller writes on and the card stays white.
  • After: each card paints the desktop press's pixels, fades at the lift, and paints once beside :active (229, not 206).
  • A source guard (no custom property pairs its default with an attribute arm) and per-card jsdom assertions fail on the base; the browser contract fails 3 of 6 there.

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

  • pnpm vitest run packages/core/src/ClickableCard packages/core/src/SelectableCard packages/core/src/Thumbnail packages/core/src/utils/customPropertyArmLayering.test.ts packages/core/src/theme/derivedVarRegistry.test.ts, and the full core unit suite.
  • pnpm exec playwright test packages/core/src/__tests__/CardPress.a11y.chromium.spec.ts --project chromium: 9 passed; 3 fail on the base build, and a build without the colour yield fails the :active case with a double paint. Wired into ci.yml. "a desktop mouse is unchanged: hover, :active, release, leave" (per card) passes on the base build and here.
  • iOS simulator, Mobile Safari 26.2 (iPhone 17 Pro; the clips: iPhone 11), a real touch hold on ClickableCard: on the base data-astryx-press="on" is written and the ::after paints nothing; after, it paints the pressed token and clears 200 ms after the lift. Text selection is off on the story root to keep iOS's long-press selection out of the frame. This is the WebKit leg: the contract's touch input is CDP, which only Chromium has.
  • pnpm build:css: the flat stylesheet orders the new arms above their defaults.
  • pnpm -F @astryxdesign/core typecheck, pnpm check:changesets, CI routing tests, lint and prettier on touched files.

@vercel

vercel Bot commented Oct 24, 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 Oct 24, 2026 6:16pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 24, 2026, 6:40 PM UTC

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

No changes needed. A held finger on ClickableCard, SelectableCard and an interactive Thumbnail now paints the pressed overlay in layered builds (Storybook and the Vite and PostCSS integrations), with the mouse press's pixels, while mouse states and the prebuilt stylesheet behave as before. The new guard and per-card assertions fail on the base and pass on this branch, and the Chromium contract passes in CI; one optional nit is left inline.

[Automated review]

* press is painted once, not twice
* @position Run with `pnpm test:a11y-contract`. Browser layer of the cards'
* press overlay; the jsdom layer is each card's own test and
* utils/pressArmLayering.test.ts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: utils/pressArmLayering.test.ts doesn't exist; the jsdom guard this PR adds is utils/customPropertyArmLayering.test.ts.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

ClickableCard (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 229 -
Complexity N/A High (19) -
SelectableCard (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 305 -
Complexity N/A Very High (32) -
Thumbnail (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 339 -
Complexity N/A Very High (38) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.9KB 1.2KB

Accessibility Audit

Status: 5 accessibility violation(s) found — 5 serious.

Button pattern - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/3 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
Checkbox pattern - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/3 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
ClickableCard - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/5 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
SelectableCard - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/4 stories · Learn more
    • WCAG: 1.4.3 (Level AA)
Thumbnail - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/12 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Visual Regression

46 of 494 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
RichTextEditor Markdown parity: side by side probe dark 94,866
RichTextEditor Markdown parity: side by side probe light 89,762
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232
ProgressBar With Target Mark probe dark 7,895
ProgressBar With Target Mark probe light 7,895
Markdown Default neutral dark 7,573
Markdown Default neutral light 7,566

and 26 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

ClickableCard, SelectableCard and Thumbnail painted the touch press as an
attribute arm of the --_press-overlay custom property. In a layered
stylesheet (Storybook, the Vite and PostCSS integrations) StyleX leaves a
custom property's own declarations unlayered and puts the attribute arm in a
priority layer, so the default always won and a finger's press painted
nothing. The press is now the overlay's own image, keyed on
data-astryx-press as plain ::after properties, with the hover and mouse
colour yielding while it paints. Mouse presses and the precompiled
stylesheet are unchanged.

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

No changes are needed to the fix. A held finger on ClickableCard, SelectableCard and an interactive Thumbnail now paints the pressed overlay in layered builds with the mouse press's pixels, while mouse states and the prebuilt stylesheet are unchanged.

pr-a11y currently fails before it reaches the new card press contract: the job runs main's workflow, whose Lab pinch-zoom step names a spec this branch predates, so rebasing onto main should let it run. One optional wording nit is inline.

[Automated review]

// The `::after` layer paints the hover and mouse states as its colour,
// `--_press-overlay`, which their arms set on the element itself (same
// enabled guard as the shared overlay utility). The press the controller
// paints (a finger's, and on a touch-first device a mouse's or a key's)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit (optional): on this branch the press controller ignores mouse pointers (pressFeedback.ts returns early for pointerType === 'mouse') and handles no keys. "on a touch-first device a mouse's or a key's" therefore describes #8046 rather than this change. The same sentence is in SelectableCard.tsx and Thumbnail.tsx. Consider dropping the parenthetical until #8046 lands, or keep it if #8046 lands first.

Brings the branch up to main so pr-a11y can run its card press contract;
main's workflow runs a Lab spec this branch predated.

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

Changes requested: the card press fix itself works (a held finger now paints the pressed overlay on ClickableCard, SelectableCard and an interactive Thumbnail in layered builds, and the new contract passes), but the latest merge of main into this branch reverts #8042. On this head Item.tsx is back to touch-action: pan-y, the second-finger hand-off in useSwipeAction.ts is gone, and #8042's changeset, pinch unit tests, ItemSwipePinchZoom.a11y.chromium.spec.ts and its pr-a11y step are deleted, so a pinch that starts on a swipeable row no longer zooms the page; #8042's own Chromium contract fails all four pinch cases against this head. Please redo the merge keeping main's versions of those six paths, which a plain merge of the same two parents does without conflicts.

[Automated review]

Comment thread packages/core/src/Item/Item.tsx Outdated
swipeRoot: {
isolation: 'isolate',
touchAction: 'pan-y pinch-zoom',
touchAction: 'pan-y',

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 line comes from the merge of main, which kept the branch's pre-#8042 Item.tsx: main declares touchAction: 'pan-y pinch-zoom' here. The same merge drops #8042's second-finger hand-off in useSwipeAction.ts, its changeset, its five pinch unit tests, ItemSwipePinchZoom.a11y.chromium.spec.ts and its pr-a11y step; that contract fails all four pinch cases against this head's Storybook. Redoing the merge with main's versions of those paths fixes it, and a plain merge of the same two parents keeps them without conflicts.

Comment thread .github/workflows/ci.yml
# (WCAG 1.4.4) and leave the row where it is, whichever finger lands
# first, while a one-finger sideways drag still swipes. Pinch-zoom and
# touch-action only exist in a real engine with two-finger touch input.
- name: Run Item swipe pinch-zoom contract

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 is #8042's pinch-zoom contract step; it should stay beside the new card step, as a plain merge of main keeps both.

…e dropped

The previous merge commit's tree came from an older main than its second
parent, so it reverted Item's swipe pinch-zoom fix (touch-action
'pan-y pinch-zoom', the second-finger hand-off, its tests, changeset and
pr-a11y step). This merge's tree is the clean merge of the branch's own
commit with main, so only this pull request's files differ from main.
Also points the card press contract's header at the jsdom guard that
exists, utils/customPropertyArmLayering.test.ts.

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

No changes needed. A held finger on ClickableCard, SelectableCard and an interactive Thumbnail now paints the pressed overlay in layered source builds with the mouse press's pixels, while mouse states are unchanged and the prebuilt stylesheet still paints the press. The new Chromium contract fails its three finger cases against a base build and passes against this head, and the earlier revert of #8042 is gone from this branch.

[Automated review]

@cixzhang
cixzhang merged commit a88f185 into meta:main Oct 24, 2026
63 checks passed
@vjeux
vjeux deleted the uikit/c01-card-press branch October 24, 2026 20:06
cixzhang pushed a commit that referenced this pull request Oct 25, 2026
…8063)

## User impact

On a phone narrower than the drawer, `MobileNav` covered the whole screen: the default 320 px drawer on a 320 px phone left no backdrop to tap, although the `width` prop promises "Capped at 85vw to prevent overflow on small screens". The page the drawer navigates was hidden entirely.

## Problem and solution fit

The drawer's width was `width: 100vw; max-width: <width>px`, which is min(100vw, width): the only cap was the viewport itself. A drawer that covers the screen hides its context and takes away the usual way out, a tap beside it; UIKit's overlay sidebar never covers the whole screen. Small phones, zoomed-in text and split-screen windows reach these widths.

The fix swaps the two: `width: <width>px; max-width: 85vw`, which is min(width, 85vw). On a 320 px phone the drawer is 272 px wide, and a 48 px strip of the dimmed page stays beside it as a target for the tap that closes it.

## Expected behavior and authority

- `MobileNav.doc.mjs`, `width`: "Drawer width in pixels. Capped at 85vw to prevent overflow on small screens."

## Smallest restoration

One style function in `MobileNav.tsx`. No API, token or default changes. From 377 px up, 85vw is at least 320 px, so the default drawer is unchanged there.

Siblings: #8042, #8046 and #8057 also add `ci.yml` steps, and #7358, #7356 and the close-path fix #8064 touch `MobileNav.tsx`; whichever lands second rebases.

## Evidence

Before (`main`) and after, 320 × 568, drawer open:

![320 px before](https://raw.githubusercontent.com/vjeux/astryx/ac3890212337192ec30b259a1b507ad3144c4194/320-before.png)
![320 px after](https://raw.githubusercontent.com/vjeux/astryx/ac3890212337192ec30b259a1b507ad3144c4194/320-after.png)

iOS 26.2 simulator, real taps (402 px: the drawer keeps 320 px): a tap on the strip closes it:

![tap on the strip closes the drawer](https://raw.githubusercontent.com/vjeux/astryx/e56f122761d3eb0ccf94a2a94c461e263bf1dc59/8063-strip-tap-closes.gif)

- Reproduction before the change: on a 320 px phone the drawer is 320 px wide. The new Chromium contract fails against a `main` Storybook (expected 272, received 320).
- Result after the change: a 272 px drawer and a 48 px strip; the strip is backdrop (`elementFromPoint` is the dialog), and a real tap on it closes the drawer.
- Representative unchanged path: on a 402 px phone the drawer keeps its 320 px width and the 82 px strip.
- Regression test or other mutation-sensitive proof: `MobileNavWidth.a11y.chromium.spec.ts` (real Chromium, touch), plus a jsdom test that the drawer's own class declares `max-width: 85vw` and takes its width from the prop.

## Scope

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

## Testing

- `pnpm exec vitest run packages/core/src/MobileNav packages/core/src/AppShell`: 136 passed.
- `pnpm -F @astryxdesign/core typecheck`; eslint and prettier on touched files; `pnpm check:repo` (pre-commit).
- `pnpm exec playwright test packages/core/src/MobileNav/__tests__/MobileNavWidth.a11y.chromium.spec.ts --project chromium` against a fresh Storybook build: 2 passed. Against `main`: the 320 px test fails and the 402 px test passes.
- CI: a new "Run MobileNav width contract" step in the a11y contract job, after the Token remove target contract.

This branch was successfully deployed

1 active deployment
Preview — b42b6560 Deployed Oct 24, 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