Skip to content

feat(press): a mouse's or a pen's press paints at once through the press controller - #8682

Closed
vjeux wants to merge 1938 commits into
meta:mainfrom
vjeux:uikit/c01-press-paint-fine
Closed

vjeux wants to merge 1938 commits into
meta:mainfrom
vjeux:uikit/c01-press-paint-fine

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

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-094 FR43), 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-094 FR11, FR43, DEC-21; component:Button FR15.

Public delta and caller burden

No API change.

Design and alternatives

  • A CSS fade out of :active: CSS can't animate a state that has ended.
  • A press handler per component: the same logic in dozens of places.
  • Chosen: the existing controller for every pointer (DEC-21). One place decides, and every press style already reads its state, so no CSS changes. The cost: a mouse press needs JavaScript, which every Astryx pressable loads on mount.

Testing

To check by hand, in Storybook on a laptop:

  1. Button story: press and hold with the mouse. It darkens at once and stays dark if you drag off; on release it fades over about 0.2 s.
  2. Right-click (ctrl-click on a Mac): nothing lights.
  3. In a list row with a link, click the link: only the link presses.
  4. With a pen, tap a row in a scrolling list: it lights at once.
  5. With reduced motion on, the press ends on release without animating.

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 in utils/pressFeedback.test.ts cover dragging off, pens and the ctrl-click; utils/pressFeedbackMotionRoles.test.ts covers 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

  • Size (gzip): Button JS 46.9 → 47.2 KB (+293 B), CSS +0.

Scope

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

Writing rubric results

component:Button FR15 now covers the mouse and pen. R1–R6 pass: no narration, research, references, rejected alternatives or open questions.

vjeux added 30 commits October 27, 2026 00:02
…px whatever the element's width) into the experimental touch exports
… move keeps that finger until it lifts) into the experimental touch exports
…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.
…he edge band decides at 10 px again, DEC-22) into the experimental touch exports
…ouch decides at 15 px on an element within the band and at 10 px beyond it, DEC-22; meta#8641 bec2283) into meta#8270
…8641's moving-touch claim kept on this line's internals delivery) into meta#8315

# Conflicts:
#	packages/core/src/gestures/GestureArena.ts
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.
…8589's watcher test reads its end; the gestures-table gate retired) into meta#8360
…8589's watcher test reads its end; the gestures-table gate retired) into the every-pointer host
…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.
…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 the every-pointer host

# Conflicts:
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
… (gestures for every pointer) and meta#8118 through the union helper (496efa6)
vjeux added 3 commits October 29, 2026 19:13
…ndant release) into the fine-pointer press paint

The release clock's reduced-motion cap reads the Reduce Motion fade's
own export, as meta#8890 now does, beside this branch's role-read clock.
…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]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  • surfaceAt takes a row's first control as its activator, so a TreeList parent row with onClick or href shows 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 stacked press-touch-down-highlight changeset and the input-marker comments still describe the old desktop press.
  • The diff against main carries 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

vjeux added a commit to vjeux/astryx that referenced this pull request Nov 2, 2026
…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).
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…ess controller

meta#8682's own change. A mouse's main button and a pen go through the touch
press controller on every device (spec:AST-094 FR11, FR43, DEC-21): the
press paints at `pointerdown`, holds while down and fades on release; a
pen never waits or brakes, and a right click, a macOS ctrl-click, a pen's
barrel button and a nested control take no surface press. The flash and
release clocks resolve their motion roles from the controller's own two
values, so no timing table ships (architecture:component-style-authoring
INV6). With FR15's 250 ms left out (previous commit), every press releases
on the shared 200 ms, so no press carries an input marker.

Absorbs meta#8682 (674ceb4).
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…r the circle's FR43 change

The UIKit constants table's barrel (motion/uikit/index.ts) takes both
changes' entries, unioned: the instant-marks change's `selection` and
meta#8682's `button` and `list` export lines all stay, and the header names
`navigation`, `selection`, the button's and the list cells' values
(`button`, `list`) and the gesture thresholds. Nothing else in the merge
needed a hand.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…t) under the selection controls' FR43 change
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…ry pointer (FR43)

A mouse and a pen press through the same press controller as a finger
(spec:AST-094 FR11's fine-pointer profile, FR43, DEC-21), so:

- The owners' press arms (indicatorPressOwnerStyles) declare the press's
  image and release for every pointer, not only under a coarse pointer.
- CheckboxInput's layer over the box reads them for every pointer and
  releases on the same clock as a finger's (UIButton's, or the row's
  deselect when a row owns the press); its mouse `:active` arm is gone.
- A CheckboxList row that delegates its clicks owns its checkbox's press for
  every pointer, so the shared touch-pointer store had no reader left and is
  removed.

RadioListItem follows in its own commit, once the press controller treats a
native interactive descendant as a press boundary (meta#8682); until then the
shared owner change leaves its layer's coarse gate in place, so a radio row
paints as before.

Tests: the owner and layer rules sit behind no pointer query, no `:active`
arm paints, and a CheckboxList row is the press surface on a fine-pointer page
too (each fails on the previous head); forty rows hold no more pointer-query
listeners than one.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…rough the press model at once (FR43)

With a mouse, a checkbox and a CheckboxList row press through the same model
as a finger (spec:AST-094 FR11's fine-pointer profile, FR43): the owner is
marked at pointerdown with no delay, the layer paints the pressed token, and
the release fades on the host's clock. A right click and a read-only row
paint nothing. A RadioList row keeps its :active case until the press
controller treats a nested control as a press boundary. These cases fail
until the controller marks a mouse's press (meta#8682).
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…FR43)

The press controller now treats a link or a button inside a pressable row
as a press boundary for every pointer (meta#8682), the job the row's mouse
`:active:not(:has(a:active, button:active, …))` arm did for a mouse alone
(meta#8424). So a RadioList row drops that arm and its `--_radio-row-press`
paint, and the layer over its circle reads the press the controller marks,
for a finger, a mouse and a pen alike, releasing on the row's clock
(spec:AST-094 FR11's fine-pointer profile, FR43).

Tests: the row declares no `:active` paint and no press rule sits behind a
pointer query (both fail on the previous head); the source-build test now
pins that the row ships no `:active` press and keeps the no-ancestor-scan
case; the mouse press cases take a radio row too.
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…e harness helper 60fa6ea; main 6a751f7) into the selection controls' FR43 change
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…release ends when its animation does) into the selection controls' FR43 change
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…nger's (FR43)

A mouse and a pen now press through the same press controller as a finger
(spec:AST-094 FR11's fine-pointer profile, FR43), so the circle reads its
press image for every pointer and no longer reads the mouse `:active` fill
(`--_indicator-press-fill`); the image is its only press. This reverses
cc91c8a's coarse gate, which kept a held pen from drawing two presses
while a mouse still pressed through `:active`.

Tests: a unit case holds the image on every pointer and no `:active` fill
(it fails on the previous head); the browser case reads a mouse press marked
by the controller at `pointerdown` and painted by the round image, with no
fill beside it (it needs the controller's mouse paint, meta#8682, to pass).
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 3, 2026
…ts animation does) into the circle's FR43 change
cixzhang added a commit that referenced this pull request Nov 4, 2026
…y an at-rule prelude (#8695)

* test: the test runtime keeps every StyleX rule under a multi-condition 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 #8226 + #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).

* test: the StyleX test injection lets an insertRule error throw

The setup caught and dropped any error from inserting a rule StyleX's dev runtime had dropped, so a rule jsdom refused would have gone missing without a trace. It never fired: the full ui suite passes with the catch removed (15,952 tests). A refused rule now throws with the rule's text.

* test: the StyleX test injection restores every rule its dev runtime keys by a prelude alone

Review: StyleX 0.19.0's dev runtime keys a rule by an at-rule's prelude
alone not only under a top-level multi-condition query, but also under one
nested in a one-condition query (`@supports (…) { @media (…) and (…) {…} }`),
under a named container query, and under `@starting-style`; the injection
restored only the first case, so a test that read a later rule under the
others still read nothing.

The injection now keys rules with a copy of StyleX's own getSeenRuleKey,
which StyleX does not export, and restores every later rule whose key is an
at-rule's prelude rather than a selector, once each.

It also skips the group last-member rule, which the test setup refuses on
purpose (its marker now lives in jsdomRefusedRules.ts, shared with
setup.ts): a refusal of that rule threw out of the component's import. Other
refusals still throw. And it records the first rule under a key as present,
so the same rule compiled in two modules is not copied into the test sheet.

* Merge remote-tracking branch 'origin/main' into vjeux/stylex-multi-condition

* test: the StyleX test injection keeps its record across vi.resetModules()

CodeBunny on #8695: the record of restored rules lived in module state,
which vi.resetModules() resets while StyleX's injector keeps its keys, so
restored rules were inserted again and a later rule imported first after a
reset was lost. The record lives on globalThis; a guard case resets between
fixtures under one prelude.

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

vjeux commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by the shared press-token work in #9762/#9783 (thanks to everyone who reviewed this)

@vjeux vjeux closed this Nov 4, 2026
@vjeux
vjeux deleted the uikit/c01-press-paint-fine branch November 4, 2026 05:47

This branch was successfully deployed

1 active deployment
Preview — 674ceb43 Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants