Skip to content

fix(core): a mouse or Space press on a touch-first device paints the pressed state - #8046

Merged
cixzhang merged 20 commits into
meta:mainfrom
vjeux:uikit/c01-coarse-press
Oct 28, 2026
Merged

cixzhang merged 20 commits into
meta:mainfrom
vjeux:uikit/c01-coarse-press

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review the fix commit; the two merge commits bring in main.

User impact

On a touch-first device with a trackpad or keyboard, a mouse press or Space on a focused button showed no pressed state on any shared-press control. Now those presses paint what a desktop press paints, hold while down, and fade at release.

Chromium, touch-first phone profile Before After
Mouse held, then released
Space held, then released
Mouse on a link, then a drag

Labels: the page's press state and events.

Problem and solution fit

  • Why it occurs: the touch press model drops every :active arm under @media (pointer: coarse) and paints only finger presses. That query reads the primary pointer, so it drops :active for a mouse and Space too.
  • Why it harms: a silent press reads as a missed click; keyboard users lose the sign that Space landed.
  • Why it matters: every pressable regressed at once.

Expected behavior and authority

  • family:buttons: Button owns "focus and press feedback"; active is a shipped visual state.
  • design:user-states DR1: a pointer hold on the listed controls MUST paint --color-overlay-pressed.
  • usePressFeedback docs keep :active for a mouse.

Smallest restoration

  • utils/pressFeedback.ts: a second slot for a primary mouse button or Space, used only where the shared touch resolver's key matches (TOUCH_QUERY, AST-012 FR2). It paints at once, ignores travel (as :active does), fades on the release clock, and drops on pointercancel or dragstart. As on a desktop, Space paints only a focused button that is the surface itself; menu rows keep the menu's highlight (useMenuPress FR6).
  • utils/pressGesture.ts: buttonDown and buttonHeld.
  • No CSS, API or default change. Under reduced motion the release is the existing 200 ms opacity fade (design:reduced-motion DR2).

Evidence

  • Before: a held mouse press or Space leaves the Button at rest. After: it paints a desktop :active press's pixels and scale, then fades; a drag start drops it at once.
  • Unchanged: desktop and finger presses.
  • 14 new unit tests fail on the base, pass after.

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/utils/pressFeedback.test.ts packages/core/src/utils/pressGesture.test.ts (49 + 29), and the full core unit suite.
  • pnpm exec playwright test packages/core/src/__tests__/PressModality.a11y.chromium.spec.ts --project chromium (6 passed; 2 fail on the base build; the menu-row and checkbox-row cases pass on main and failed on this PR's previous head), routed by its own @pr-a11y scope line (Button and the press machinery), which the PR accessibility runner reads: pnpm test:a11y-contract:pr origin/main --dry-run lists it. The same spec ran locally in WebKit inside the Playwright container: the mouse, Space and desktop cases pass (the finger case needs CDP and skips there).
  • iOS simulator, Mobile Safari 26.2: (pointer: coarse) matches, the condition this fix keys on. A hardware Space key sends no key events to a page whose focused element is a button (re-checked on iOS 26.2 while recording), and the simulator turns host clicks into touches, so those paths are proven in Chromium and WebKit; the clips above are Chromium recordings (Playwright video, real mouse and keyboard input) of the base build and this one.
  • pnpm typecheck, pnpm check:repo, pnpm check:changesets, CI routing tests, lint and prettier on touched files.

Stack (merge in this order): this PR → #8117 → #8118.

@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 25, 2026 8:57pm 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
@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 25, 2026, 9:25 PM UTC

@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

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 54.1KB 54.9KB +843B 12.2KB 12.2KB 0B
core/AppShell 55.2KB 55.8KB +623B 11.4KB 11.4KB 0B
core/AspectRatio 3.6KB 3.6KB 0B 3.3KB 3.3KB 0B
core/Avatar 37.2KB 37.2KB 0B 6.2KB 6.2KB 0B
core/AvatarGroup 39.5KB 39.5KB 0B 6.3KB 6.3KB 0B
core/AvatarGroupOverflow 39.6KB 40.4KB +822B 6.2KB 6.2KB 0B
core/AvatarStatusDot 6.9KB 6.9KB 0B 3.3KB 3.3KB 0B
core/Badge 3.7KB 3.7KB 0B 3.6KB 3.6KB 0B
core/Banner 47.2KB 48.0KB +822B 9.5KB 9.5KB 0B
core/BaseTypeahead 53.4KB 54.2KB +822B 9.7KB 9.7KB 0B
core/Blockquote 3.4KB 3.4KB 0B 3.3KB 3.3KB 0B
core/BottomSheet 25.9KB 25.9KB 0B 7.5KB 7.5KB 0B
core/BottomSheetSwitcher 9.1KB 9.1KB 0B 3.2KB 3.2KB 0B
core/BreadcrumbItem 65.4KB 66.2KB +886B 10.8KB 10.8KB 0B
core/Breadcrumbs 22.3KB 22.3KB 0B 3.1KB 3.1KB 0B
core/Button 42.8KB 43.6KB +849B 9.0KB 9.0KB 0B
core/ButtonGroup 8.3KB 8.3KB 0B 3.2KB 3.2KB 0B
core/Calendar 54.4KB 55.2KB +820B 9.8KB 9.8KB 0B
core/Card 6.3KB 6.3KB 0B 6.2KB 6.2KB 0B
core/Carousel 48.4KB 49.2KB +798B 10.0KB 10.0KB 0B
core/Center 4.0KB 4.0KB 0B 4.4KB 4.4KB 0B
core/ChatComposer 59.9KB 60.7KB +844B 10.5KB 10.5KB 0B
core/ChatComposerDrawer 23.7KB 23.7KB 0B 4.3KB 4.3KB 0B
core/ChatComposerInput 55.0KB 55.8KB +815B 9.8KB 9.8KB 0B
core/ChatComposerTokenElement 55.0KB 55.8KB +816B 9.8KB 9.8KB 0B
core/ChatDictationButton 46.0KB 46.8KB +811B 9.2KB 9.2KB 0B
core/ChatLayout 48.4KB 49.2KB +844B 9.7KB 9.7KB 0B
core/ChatLayoutScrollButton 45.9KB 46.7KB +823B 9.3KB 9.3KB 0B
core/ChatMessage 22.9KB 22.9KB 0B 3.4KB 3.4KB 0B
core/ChatMessageBubble 6.9KB 6.9KB 0B 3.5KB 3.5KB 0B
core/ChatMessageList 28.3KB 28.3KB 0B 7.6KB 7.6KB 0B
core/ChatMessageMetadata 25.7KB 25.7KB 0B 3.7KB 3.7KB 0B
core/ChatSendButton 45.4KB 46.1KB +778B 9.0KB 9.0KB 0B
core/ChatSystemMessage 6.6KB 6.6KB 0B 3.5KB 3.5KB 0B
core/ChatTokenizedText 6.8KB 6.8KB 0B 3.7KB 3.7KB 0B
core/ChatToolCalls 31.7KB 31.7KB 0B 8.7KB 8.7KB 0B
core/CheckIndicator 8.9KB 8.9KB 0B 3.4KB 3.4KB 0B
core/CheckboxIndicator 4.0KB 4.0KB 0B 3.7KB 3.7KB 0B
core/CheckboxInput 49.1KB 49.9KB +843B 9.1KB 9.1KB 0B
core/CheckboxList 45.6KB 45.6KB 0B 7.9KB 7.9KB 0B
core/CheckboxListItem 53.2KB 54.0KB +841B 10.2KB 10.2KB 0B
core/Citation 25.8KB 25.8KB 0B 4.0KB 4.0KB 0B
core/ClickableCard 12.7KB 13.5KB +812B 6.8KB 6.8KB 0B
core/Code 3.4KB 3.4KB 0B 3.2KB 3.2KB 0B
core/CodeBlock 58.3KB 59.1KB +822B 10.4KB 10.4KB 0B
core/Collapsible 11.6KB 12.4KB +834B 4.1KB 4.1KB 0B
core/CollapsibleGroup 6.3KB 6.3KB 0B 3.1KB 3.1KB 0B
core/CommandPalette 49.1KB 49.8KB +779B 11.7KB 11.7KB 0B
core/CommandPaletteEmpty 6.0KB 6.0KB 0B 3.2KB 3.2KB 0B
core/CommandPaletteFooter 23.2KB 23.2KB 0B 3.4KB 3.4KB 0B
core/CommandPaletteGroup 3.3KB 3.3KB 0B 3.2KB 3.2KB 0B
core/CommandPaletteInput 31.7KB 31.7KB 0B 8.0KB 8.0KB 0B
core/CommandPaletteItem 8.3KB 9.1KB +831B 3.3KB 3.3KB 0B
core/CommandPaletteList 22.3KB 22.3KB 0B 3.1KB 3.1KB 0B
core/ComplexSelector 54.8KB 55.6KB +890B 10.3KB 10.3KB 0B
core/ContextMenu 77.0KB 77.6KB +527B 14.2KB 14.2KB 0B
core/ContextMenuItem 16.4KB 17.3KB +852B 5.0KB 5.0KB 0B
core/DateInput 92.3KB 93.1KB +793B 14.3KB 14.3KB 0B
core/DateRangeInput 63.9KB 64.8KB +862B 10.9KB 10.9KB 0B
core/DateTimeInput 101.6KB 102.4KB +816B 14.7KB 14.7KB 0B
core/Dialog 13.6KB 13.6KB 0B 6.9KB 6.9KB 0B
core/DialogHeader 48.3KB 49.1KB +819B 9.6KB 9.6KB 0B
core/Divider 6.3KB 6.3KB 0B 3.4KB 3.4KB 0B
core/Drawer 25.4KB 25.4KB 0B 7.9KB 7.9KB 0B
core/DrawerHeader 48.4KB 49.2KB +824B 11.0KB 11.0KB 0B
core/DropdownMenu 72.1KB 72.6KB +546B 14.3KB 14.3KB 0B
core/DropdownMenuCheckboxItem 16.0KB 16.8KB +864B 5.4KB 5.4KB 0B
core/DropdownMenuDivider 6.5KB 6.5KB 0B 3.4KB 3.4KB 0B
core/DropdownMenuGroup 5.9KB 5.9KB 0B 3.1KB 3.1KB 0B
core/DropdownMenuItem 15.3KB 16.1KB +855B 4.9KB 4.9KB 0B
core/DropdownMenuRadioGroup 5.9KB 5.9KB 0B 3.1KB 3.1KB 0B
core/DropdownMenuRadioItem 16.0KB 16.9KB +861B 5.4KB 5.4KB 0B
core/DropdownMenuSubMenu 57.2KB 58.0KB +823B 9.3KB 9.3KB 0B
core/EmptyState 7.5KB 7.5KB 0B 3.4KB 3.4KB 0B
core/Field 43.8KB 43.8KB 0B 7.7KB 7.7KB 0B
core/FieldLabel 39.5KB 39.5KB 0B 6.4KB 6.4KB 0B
core/FieldStatus 10.2KB 10.2KB 0B 4.0KB 4.0KB 0B
core/FileInput 53.2KB 54.0KB +837B 10.1KB 10.1KB 0B
core/FormLayout 6.2KB 6.2KB 0B 3.2KB 3.2KB 0B
core/Grid 4.2KB 4.2KB 0B 3.5KB 3.5KB 0B
core/GridSpan 3.3KB 3.3KB 0B 3.1KB 3.1KB 0B
core/HStack 7.2KB 7.2KB 0B 4.8KB 4.8KB 0B
core/Heading 9.0KB 9.0KB 0B 6.7KB 6.7KB 0B
core/Icon 8.7KB 8.7KB 0B 3.4KB 3.4KB 0B
core/IconButton 42.9KB 43.7KB +830B 9.0KB 9.0KB 0B
core/InputGroup 44.4KB 44.4KB 0B 7.8KB 7.8KB 0B
core/InputGroupText 6.1KB 6.1KB 0B 3.4KB 3.4KB 0B
core/Item 11.2KB 12.1KB +935B 4.5KB 4.5KB 0B
core/Kbd 6.6KB 6.6KB 0B 3.3KB 3.3KB 0B
core/LayerProvider 57.1KB 57.9KB +817B 9.9KB 9.9KB 0B
core/Layout 8.0KB 8.0KB 0B 5.4KB 5.4KB 0B
core/LayoutContent 7.3KB 7.3KB 0B 4.8KB 4.8KB 0B
core/LayoutFooter 7.2KB 7.2KB 0B 4.7KB 4.7KB 0B
core/LayoutHeader 7.3KB 7.3KB 0B 4.7KB 4.7KB 0B
core/LayoutPanel 7.4KB 7.4KB 0B 4.7KB 4.7KB 0B
core/Lightbox 49.1KB 49.9KB +853B 9.6KB 9.6KB 0B
core/Link 43.2KB 44.0KB +829B 7.4KB 7.4KB 0B
core/List 6.6KB 6.6KB 0B 3.3KB 3.3KB 0B
core/ListItem 52.7KB 53.5KB +839B 10.2KB 10.2KB 0B
core/Markdown 136.4KB 136.9KB +530B 16.9KB 16.9KB 0B
core/MetadataList 23.0KB 23.0KB 0B 3.5KB 3.5KB 0B
core/MetadataListItem 6.3KB 6.3KB 0B 3.3KB 3.3KB 0B
core/MobileNav 48.4KB 49.2KB +823B 9.8KB 9.8KB 0B
core/MobileNavToggle 45.8KB 46.6KB +817B 9.2KB 9.2KB 0B
core/MoreMenu 72.3KB 72.8KB +545B 14.3KB 14.3KB 0B
core/MultiSelector 75.1KB 75.5KB +430B 14.1KB 14.1KB 0B
core/NavHeadingMenu 8.8KB 8.8KB 0B 3.1KB 3.1KB 0B
core/NavIcon 3.3KB 3.3KB 0B 3.2KB 3.2KB 0B
core/NumberInput 55.1KB 55.9KB +810B 10.4KB 10.4KB 0B
core/Outline 28.5KB 29.3KB +829B 3.9KB 3.9KB 0B
core/OverflowList 8.4KB 8.4KB 0B 3.4KB 3.4KB 0B
core/Overlay 14.2KB 14.2KB 0B 3.7KB 3.7KB 0B
core/Pagination 86.8KB 87.3KB +550B 14.8KB 14.8KB 0B
core/Popover 46.9KB 47.7KB +816B 9.2KB 9.2KB 0B
core/PowerSearch 147.2KB 147.7KB +464B 18.1KB 18.1KB 0B
core/ProgressBar 7.6KB 7.6KB 0B 6.4KB 6.4KB 0B
core/RadioIndicator 3.7KB 3.7KB 0B 3.6KB 3.6KB 0B
core/RadioList 45.2KB 45.2KB 0B 7.8KB 7.8KB 0B
core/RadioListItem 51.8KB 52.6KB +831B 9.5KB 9.5KB 0B
core/ResizeHandle 25.0KB 25.0KB 0B 3.7KB 3.7KB 0B
core/Section 7.4KB 7.4KB 0B 6.0KB 6.0KB 0B
core/SegmentedControl 38.5KB 38.5KB 0B 6.2KB 6.2KB 0B
core/SegmentedControlItem 8.5KB 9.3KB +821B 3.7KB 3.7KB 0B
core/SelectableCard 12.6KB 13.4KB +817B 7.0KB 7.0KB 0B
core/Selector 78.2KB 78.7KB +454B 14.5KB 14.5KB 0B
core/SelectorOption 14.4KB 15.2KB +859B 4.8KB 4.8KB 0B
core/SideNav 55.4KB 56.2KB +841B 10.3KB 10.3KB 0B
core/SideNavCollapseButton 46.2KB 47.0KB +837B 9.2KB 9.2KB 0B
core/SideNavHeading 54.3KB 55.0KB +781B 9.9KB 9.9KB 0B
core/SideNavItem 53.9KB 54.7KB +812B 9.9KB 9.9KB 0B
core/SideNavSection 6.4KB 6.4KB 0B 3.5KB 3.5KB 0B
core/Skeleton 3.7KB 3.7KB 0B 3.5KB 3.5KB 0B
core/Slider 48.1KB 48.1KB 0B 8.3KB 8.3KB 0B
core/Spinner 26.2KB 26.2KB 0B 7.4KB 7.4KB 0B
core/Stack 6.9KB 6.9KB 0B 4.8KB 4.8KB 0B
core/StackItem 5.9KB 5.9KB 0B 3.2KB 3.2KB 0B
core/StatusDot 18.7KB 18.7KB 0B 6.0KB 6.0KB 0B
core/Step 32.2KB 33.1KB +842B 5.1KB 5.1KB 0B
core/Stepper 47.0KB 47.8KB +827B 9.3KB 9.3KB 0B
core/Switch 48.0KB 48.8KB +841B 9.1KB 9.1KB 0B
core/Tab 9.6KB 10.4KB +838B 4.0KB 4.0KB 0B
core/TabList 39.9KB 39.9KB 0B 7.0KB 7.0KB 0B
core/TabMenu 51.2KB 52.0KB +843B 9.8KB 9.8KB 0B
core/Table 83.2KB 83.7KB +503B 14.6KB 14.6KB 0B
core/TableBody 3.2KB 3.2KB 0B 3.0KB 3.0KB 0B
core/TableCell 78.1KB 78.6KB +483B 14.3KB 14.3KB 0B
core/TableFooter 3.2KB 3.2KB 0B 3.0KB 3.0KB 0B
core/TableHeader 3.2KB 3.2KB 0B 3.0KB 3.0KB 0B
core/TableHeaderCell 77.9KB 78.3KB +481B 14.3KB 14.3KB 0B
core/TableRow 6.1KB 6.1KB 0B 3.4KB 3.4KB 0B
core/TableSelectionToolbar 51.9KB 52.7KB +856B 11.0KB 11.0KB 0B
core/Text 8.5KB 8.5KB 0B 6.7KB 6.7KB 0B
core/TextArea 48.6KB 48.6KB 0B 9.1KB 9.1KB 0B
core/TextInput 52.6KB 53.4KB +836B 10.2KB 10.2KB 0B
core/Thumbnail 49.6KB 50.5KB +851B 10.8KB 10.8KB 0B
core/TimeInput 78.8KB 79.7KB +878B 13.2KB 13.2KB 0B
core/Timer 9.1KB 9.1KB 0B 6.7KB 6.7KB 0B
core/Timestamp 27.4KB 27.4KB +1B 9.3KB 9.3KB 0B
core/Toast 53.8KB 54.7KB +857B 9.5KB 9.5KB 0B
core/ToggleButton 43.7KB 44.5KB +831B 9.1KB 9.1KB 0B
core/ToggleButtonGroup 6.2KB 6.2KB 0B 3.1KB 3.1KB 0B
core/Token 29.5KB 30.3KB +843B 4.3KB 4.3KB 0B
core/Tokenizer 63.8KB 64.6KB +830B 11.4KB 11.4KB 0B
core/Toolbar 39.5KB 39.5KB 0B 7.6KB 7.6KB 0B
core/TopNav 49.2KB 50.0KB +811B 10.1KB 10.1KB 0B
core/TopNavHeading 53.7KB 54.4KB +787B 9.7KB 9.7KB 0B
core/TopNavItem 8.9KB 9.7KB +845B 3.7KB 3.7KB 0B
core/TopNavMegaMenu 53.7KB 54.5KB +812B 10.5KB 10.5KB 0B
core/TopNavMegaMenuFeaturedCard 6.6KB 6.6KB 0B 3.3KB 3.3KB 0B
core/TopNavMegaMenuItem 9.0KB 9.8KB +843B 3.9KB 3.9KB 0B
core/TopNavMenu 53.1KB 53.9KB +791B 9.9KB 9.9KB 0B
core/TreeList 32.4KB 33.2KB +836B 4.5KB 4.5KB 0B
core/Typeahead 60.5KB 61.3KB +817B 11.0KB 11.0KB 0B
core/TypeaheadItem 6.1KB 6.1KB 0B 3.3KB 3.3KB 0B
core/VStack 7.2KB 7.2KB 0B 4.8KB 4.8KB 0B

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 33.8KB 33.8KB 0B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

48 of 502 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
RichTextEditor Markdown parity: side by side probe dark 556,900
RichTextEditor Markdown parity: side by side probe light 538,816
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
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

and 28 more.


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

Touch behaviours key on a coarse primary pointer, layout behaviours on a
compact viewport, and presentations that need both (a sheet in place of a
popover) ask for both. utils/touchPresentation.ts holds the three queries,
SSR-safe hooks (useTouchPresentation; useMotionPresentation for components
with no interaction to infer from: UIKit motion under a finger, theme
tokens under a mouse) and a one-moment read. Styles write the same queries
as StyleX media keys, and a test keeps core's compact-touch keys spelled as
the shared query.

A compact viewport is narrower than 744 px or shorter than 500 px, the
closest a size gets to UIKit's compact size class: an iPad mini in portrait
keeps the anchored popover and a phone in landscape gets the sheet; a
Max-size iPhone in landscape is the known miss. The adaptive popover-or-
sheet presentation reads the shared query; its docs, stories and tests
follow.

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

The mouse-press restoration works and leaves desktop unchanged, but approval waits on one owner decision and one fix.

  • No current spec covers a held Space painting the CheckboxInput, Switch or RadioList indicator (desktop engines disagree), so an engineering owner settles it by commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner, or asks for the key path to paint only a focused element that is itself the pressable surface.
  • A mouse press dragged across DropdownMenu rows now keeps the pressed row painted beside the focused row, which useMenuPress FR6 and DEC-6 rule out, so leave surfaces inside a data-astryx-menu-press root to the menu's own highlight.

[Automated review]

Comment on lines +101 to +108
const SPACE_ACTIVATED_INPUT_TYPES: ReadonlySet<string> = new Set([
'button',
'checkbox',
'image',
'radio',
'reset',
'submit',
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

checkbox and radio here make a held Space paint the pressable row around the focused input (CheckboxInput, Switch, RadioList). Chromium and WebKit desktop leave that row at rest for the same key press, because only the input matches :active. Firefox paints it, and no current spec settles which is intended, so this part waits on an owner decision.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 798d04e: Space now paints only a focused button that is itself the pressable surface, so the row around a checkbox or radio stays at rest, as in desktop Chromium and WebKit. Tests: the unit case "leaves the row around a focused checkbox or radio at rest" and the real-browser case "Space on a focused checkbox leaves its row at rest, as on a desktop". The browser case fails on the previous head and passes on main and here.

// is fine; the touch treatment's CSS is inside `@media (pointer: coarse)`.
// Not tracking it there keeps desktop identical and costs the mouse path
// nothing. Only the primary button: that is the press `:active` paints.
if (event.button === 0 && stylesheetDroppedActive()) {

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 a coarse primary pointer this also paints DropdownMenu rows, and buttonHeld keeps that paint through the drag, so a press dragged from one row to another leaves two rows lit. That conflicts with useMenuPress FR6 ("At most one row of a menu is lit"), and DEC-6 dropped the coarse-pointer row press paint for that reason. Skipping surfaces inside a [data-astryx-menu-press] root keeps menu rows on the focus highlight alone.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 798d04e: the mouse and Space path skips surfaces inside [data-astryx-menu-press], so the menu's highlight stays its only paint (FR6, DEC-6). Tests: the unit case "leaves a menu row to the menu" and the real-browser case "a mouse press dragged across menu rows lights only the row under the pointer". The browser case fails on the previous head and passes on main and here.

vjeux and others added 2 commits October 24, 2026 08:57
A touch presentation is now also an idiom: ios for WebKit on iOS or iPadOS
under a coarse pointer (CSS.supports('-webkit-touch-callout: none'), which
no other engine implements), default everywhere else, Android included,
which keeps today's behaviour until a Material variant exists.
useTouchPresentation and readTouchPresentation report it, useTouchIdiom
reads it alone, and useMotionPresentation now chooses UIKit motion only in
the iOS idiom. Styles gate the same way with two nested StyleX keys, the
media query outside and the supports query inside (IOS_TOUCH_STYLE_KEYS),
as core's 16px input floor already does; a test keeps every such key in
core spelled and nested that way. A real-engine spec reads Chromium as an
Android phone and WebKit as default.
…documented behavior change

- The resolver answers the same on every engine: the iOS idiom
  (useTouchIdiom, useMotionPresentation, the idiom field) is dropped, so
  nothing chooses a platform; touch motion reads the theme's motion roles.
  The iOS style-gate constants stay, internal, for a style iOS WebKit itself
  requires (core's 16px input floor), and their spelling test stays.
- The changeset names the old and new default (sub-menu drill-in included)
  and the presentation prop that pins a component's presentation.
cixzhang pushed a commit that referenced this pull request Oct 24, 2026
## 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 | ![](https://raw.githubusercontent.com/vjeux/astryx/265ef72da23bf301de667f09c707a43035a745ec/t8057-before.gif) | ![](https://raw.githubusercontent.com/vjeux/astryx/265ef72da23bf301de667f09c707a43035a745ec/t8057-after.gif) |

## 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 #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

- [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 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.
@vjeux
vjeux force-pushed the uikit/c01-coarse-press branch from 1e1aeec to 798d04e Compare October 24, 2026 21:59
Brings in the theme compact definition (spec:AST-012 FR9) this branch's
resolver reads.
COMPACT_QUERY and COMPACT_TOUCH_QUERY are the theme's compact definition
(spec:AST-012 FR9: width below the md point, 768 by default, or height
below 500, the same on every platform), so useAdaptivePresentation and the
sub-menu drill-in keep no size numbers of their own. Tests, docs, stories
and the changeset follow: a coarse-pointer screen exactly 768 wide keeps the
popover, and a phone in landscape gets the sheet.
…t width

The previous commit's formatter pass rewrapped the whole file; this keeps
the file as it was and changes only 744px to 768px in both languages.

@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 looks ready to merge: on touch-first devices a held mouse, trackpad or Space press now shows the same pressed state as on desktop, desktop presses are unchanged, and both issues from the previous revision (Space painting checkbox, radio and switch rows, and a second lit row while dragging across a menu) are fixed. Two optional touch-ups: the usePressFeedback docs say the controller paints "every press" and "a Space-key press", while the key path only paints a focused native button that carries the marker and nothing inside a menu, so naming that condition as the changeset does would keep them exact; and the description's test counts predate the last commit (13 new unit tests fail on the base, and pressFeedback.test.ts now has 51 cases).

[Automated review]

guidance: true,
description:
'Let the composed style own the press on every pointer: it keeps `:active` for a mouse and drops it under `@media (pointer: coarse)`, so a finger sees only the press model. A rule of your own that paints `:active` would paint under a finger too, on the touch and through a scroll.',
'Let the composed style own the press on every pointer: it keeps `:active` for a desktop mouse and drops it under `@media (pointer: coarse)`, where the controller paints every press (a finger through the press model, a mouse or Space key at once). A rule of your own that paints `:active` would paint under a finger too, on the touch and through a scroll.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional wording: the controller doesn't paint every press here. The key path paints only a focused native <button> (or button-like <input>) that itself carries the marker, and neither the mouse nor the key path paints inside a [data-astryx-menu-press] root (pressFeedback.ts:342-345). Naming that condition, as the changeset already does, keeps the docs exact for a local <div role="button"> pressable.

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Both are already in this head (103b1f4; fix commit 0933a68):

  • A held Space paints only a focused element that is itself the pressable surface and a button (<button>, or an input of type button, image, reset or submit). A checkbox, radio or switch, its indicator and the row around it stay at rest. Tests: the unit case "leaves the row around a focused checkbox or radio at rest, as desktop browsers do", and the browser case "Space on a focused checkbox leaves its row at rest, as on a desktop", which compares the checkbox's pixels at rest and while Space is held, and checks that no press attribute is written, in both a phone and a desktop context.
  • Two lit rows: the mouse and Space path skips every surface inside [data-astryx-menu-press], so a menu's highlight is its only paint (useMenuPress FR6). Tests: the unit case "leaves a menu row to the menu: its highlight is the only paint", and the browser case "a mouse press dragged across menu rows lights only the row under the pointer".

If either still reproduces, which surface and input does it show on?

@cixzhang
cixzhang merged commit 0913354 into meta:main Oct 28, 2026
66 checks passed
@vjeux
vjeux deleted the uikit/c01-coarse-press branch October 28, 2026 17:28
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
Main merged meta#8045 and meta#8046. The one conflict is utils/pressFeedback.ts:
it takes main's merged meta#8046 text (the finger and button slots, the mouse
and key press) and keeps this pull request's own additions, the one
writer of the press attribute (writePress, paintPress, readPressPaint)
and the header line naming them.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
Main's button slot, merged with meta#8046, ends a press's release at once
when the next press starts, by clearing the press attribute directly. On
a row another painter holds, that clear erased the hold, which this pull
request's one writer refuses. A mouse press on a held row, released, then
a press elsewhere must leave the row held. Fails until the early end goes
through the one writer.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
meta#8073 merged main 70ea859, where meta#8045 and meta#8046 landed, and keeps its
one writer of the press attribute on main's press controller. Taking that
head resolves this pull request's conflict with main in
utils/pressFeedback.ts with no change of its own.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
Main brings meta#8046, which gives the press controller a slot per press so a
mouse or Space press on a touch-first device paints too. The conflict is
the release timer: main's slot-based timer keeps meta#8661's tempo factor,
so every press's release fade follows the theme's tempo, a mouse or
Space press's included. A new case pins that: it fails with main's side
of the timer.

The no-icon-set search goldens conflict too; they are regenerated with
experimental-isolation.test.mjs's documented --update (main's 369 matches
plus this pull request's motion docs), and the diff names no icon set and
nothing experimental.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
Both upstream heads are folded onto main 70ea859 (her meta#8045 frame
evidence and meta#8046 coarse press): meta#8446 takes main's harness files, and
meta#8073 carries the press controller onto meta#8046's slots. meta#8076 has no change
of its own in either set of files, so the merge takes theirs with no hand
resolution, and meta#8076 is clean against main again.
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/cli/assets/docs/author-a-theme.doc.mjs
#	packages/cli/test/__golden__/no-icon-set/docs-topics.json
#	packages/core/src/utils/pressFeedback.ts
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/src/busy.test.ts
#	internal/interaction-harness/src/busy.ts
#	internal/interaction-harness/src/frames.ts
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	packages/core/src/utils/pressFeedback.ts
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
With main's coarse press for buttons (meta#8046) and the row press release
(meta#8077) in this line, tapping the story's Insert button plays the
button's own release on the next frame. The insert spec's one-frame
check read every animation on the page, so it failed on that release
(one start 16.7 ms after the others) while every animation of the
update itself started on the same frame. It now reads the animations
whose target is inside the list, whatever drives them: the new row's
entrance and the glides of the four rows after it.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
…into the headless context menu

packages/core/src/utils/pressFeedback.ts: meta#8670's text, with this
stack's own press-paint subscription (c338e93: subscribePressPaint,
which ContextMenu's press feedback follows) re-applied on it; meta#8670's
fold kept only its own line's edits to this file.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
…arker

Takes meta#8360's press controller as it stands (its finger and button
slots, and the mouse-and-Space press from meta#8046) and re-applies only
this PR's change on it: the group item marker and the controller's
skip of a pointer press on a marked action.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
…tion to its tracker

meta#8046 made the controller paint a mouse press itself where the primary
pointer is coarse (a tablet with a trackpad). On an action its group
tracks (family:action-groups), the group's tracker paints that press,
so the controller must paint nothing there, as it already does for a
finger; it paints the press, then turns the tracker's paint to
'fading' at the release. A Space press on the same action still paints:
the group tracks pointers only (guard).
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
… its tracker

Where the primary pointer is coarse the controller paints a mouse press
itself (meta#8046). It now skips an action a group's tracker paints for a
pointer's press, a mouse's included, as it already did for a finger, so
the two never write the press attribute over each other. A Space press
on the action still paints: the group tracks pointers only.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
…) into the touch-down highlight

Takes meta#8077's press controller as it now stands (main's finger and
button slots, meta#8046's mouse-and-Space press, and the release that ends
with its animation) and keeps only this PR's behavior on it: under a
finger a surface highlights in the frame the finger lands unless a
scroller sits above it, where it waits the content-touch delay
(spec:AST-094 FR11). That is pressGesture's down.delayed and the
controller's isInsideScroller, with their tests.

The two press profiles go: they had become identical, meta#8077 moved the
row's release to its stylesheet, so Item and TreeList keep meta#8077's
pressable props. This PR's listener arming and brake rewrite are not
carried either; meta#8077's own listeners and brake stand.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
…head

Main's merges of meta#8045 and meta#8046 (the frame-evidence harness and the coarse
press) conflict with the older foundation heads this branch carries, in the
interaction harness files. meta#8106's current head carries that main and the
foundation it now rests on, so taking it leaves this branch clean against
main with nothing resolved by hand. A clean merge.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
This pull request sat on meta#8076 and carried its old copies of the harness
meta#8045 and meta#8046 landed on main. meta#8360 carries meta#8076 with main's harness,
so after the merge this pull request's own diff is the presentation
resolver's commits. The one conflict, the gestures README, keeps both
sides: this pull request's paragraph on the presentation prop ends the
touch presentation section, and meta#8360's migration section follows it.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
This pull request sat on meta#8073's old head and carried old copies of the
harness meta#8045 and meta#8046 landed on main. meta#8360 carries that foundation with
main's harness, so after the merge this pull request's own diff is its
three commits (the touch style keys, internal, and the press paint's
priority over the coarse-pointer :active reset). The merge is clean.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
meta#8665 now carries meta#8360's new head (75392b2), which carries the
gesture foundation with the harness meta#8045 and meta#8046 landed on main, so
this pull request no longer carries their old copies. The merge is
clean.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
meta#8665 now carries meta#8360's new head (75392b2), which carries the
gesture foundation with the harness meta#8045 and meta#8046 landed on main, so
this pull request no longer carries their old copies. The merge is
clean.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 29, 2026
meta#8665 now carries meta#8360's new head (75392b2), which carries the
gesture foundation with the harness meta#8045 and meta#8046 landed on main, so
this pull request no longer carries their old copies. The merge is
clean.

This branch was successfully deployed

1 active deployment
Preview — 103b1f45 Deployed Oct 25, 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