Skip to content

feat(core): one touch presentation resolver - #8062

Merged
cixzhang merged 16 commits into
meta:mainfrom
vjeux:wip/uikit-pr-touch-presentation
Oct 25, 2026
Merged

cixzhang merged 16 commits into
meta:mainfrom
vjeux:wip/uikit-pr-touch-presentation

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Depends on nothing (based on main); review the resolver commits, then the follow-ups on top (from 4d728a7).

Primary intent

Answer "is this a touch presentation, and is it compact?" once. Touch behaviors key on a coarse primary pointer, layout keys on the theme's compact definition, and the adaptive popover-or-sheet presentation keys on both.

User or maintainer impact

Core asked this several ways ((max-width: 768px) and (pointer: coarse), (pointer: coarse), (hover: none)), so presentations disagreed on one device.

This is a behavior change for presentation="adaptive" on DropdownMenu, ContextMenu, MoreMenu, Selector and MultiSelector, and for a DropdownMenu's default sub-menu drill-in. Under a coarse primary pointer, compact is now the theme's spec:AST-012 FR9 definition: narrower than the theme's md point (768 px by default) or shorter than 500 px.

  • A coarse screen exactly 768 px wide keeps the anchored popover, where it used to get a sheet.
  • A phone in landscape gets the sheet.
  • presentation="popover" or "bottom-sheet" pins the presentation. The changeset says so.

Proposed; pending owner review.

Problem and solution fit

  • Why does it happen? Each component wrote its own query.
  • Why does that hurt? Presentations disagree on one device.
  • Why does it matter? With one rule taken from the theme, moving md moves every edge.

Deltas:

  • utils/touchPresentation.ts holds the touch and compact queries, the SSR-safe useTouchPresentation, and readTouchPresentation. Both answer {isTouch, isCompact} from the active theme.
  • There is one touch model on every device, with no platform idiom. The only iOS gate left is a style WebKit requires (core's 16 px input floor): '@supports (-webkit-touch-callout: none)' nested inside '@media (pointer: coarse)'. A test keeps every such key spelled and nested that way.
  • useAdaptivePresentation reads the resolver. Docs, stories and tests follow it.
  • Follow-ups on top of the branch's latest commits:
    • 4d728a7 drops the unused isCompactTouch field, which nothing imports;
    • a sub-menu drill-in test under a theme that moves md, and readTouchPresentation requiring the compact definition it reads (nothing calls it yet).

Authority

Compact follows spec:AST-012 FR9 (current). The adaptive presentation follows the Selector record. AST-094 is proposed separately in #8316. The behavior change above is proposed; pending owner review.

Scope

  • This PR has one primary intent.
  • Independently removable deltas were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  • Mobile Safari, iOS 26.2 simulator on an iPhone 17 Pro (402 × 874 pt) and an iPad (A16) (820 × 1180 pt; iPadOS sends its desktop-class user agent and still matches coarse). The page has four iframes running the resolver and the adaptive hook. Both devices gave the same table:

    frame inner size coarse compact adaptive menu
    w767 767 × 900 true true bottom-sheet
    w768 768 × 900 true false popover
    h499 1024 × 499 true true bottom-sheet
    h500 1024 × 500 true false popover

    Every frame reports {isTouch: true} from both the hook and the one-moment read, and -webkit-touch-callout is supported.

  • The same four frames in Chromium (4/4) and WebKit (4/4, mcr.microsoft.com/playwright:v1.61.1-noble). The browser specs that ran them leave this pull request, since no CI job runs their config (spec:AST-030 FR11); they are re-homed in test(core): the touch presentation's browser proofs run on the interaction harness #8376 on the CI-run interaction harness (test(interaction): frame evidence #8045), and the unit suites keep the resolver's answers.

  • pnpm exec vitest run --project ui packages/core/src/utils/touchPresentation packages/core/src/hooks packages/core/src/ContextMenu packages/core/src/DropdownMenu packages/core/src/MultiSelector packages/core/src/Selector packages/core/src/MoreMenu: 60 files, 1,398 tests passed, including a sub-menu drill-in under a theme whose md is 1000.

  • tsc --project packages/core/tsconfig.json --noEmit: clean. pnpm check:changesets and pnpm check:sync: clean.

vjeux added 2 commits October 23, 2026 20:53
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.
@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 5:31am UTC

Request Review

@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, 8:06 AM 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

Modified Components

ContextMenu (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 1.1KB 1.1KB 0B
Lines of Code 639 639 -
Complexity Very High Very High (84) -
MoreMenu (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 219B 219B 0B
Lines of Code 87 87 -
Complexity Low Low (4) -
MultiSelector (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 273B 273B 0B
Lines of Code 2626 2626 -
Complexity Very High Very High (338) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/ContextMenu 73.5KB 73.8KB +356B 14.2KB 14.2KB 0B
core/ContextMenuItem 16.4KB 16.4KB 0B 5.0KB 5.0KB 0B
core/DropdownMenu 68.5KB 68.9KB +346B 14.4KB 14.4KB 0B
core/DropdownMenuCheckboxItem 16.0KB 16.0KB 0B 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 15.3KB 0B 4.9KB 4.9KB 0B
core/DropdownMenuRadioGroup 5.9KB 5.9KB 0B 3.1KB 3.1KB 0B
core/DropdownMenuRadioItem 16.0KB 16.0KB 0B 5.4KB 5.4KB 0B
core/DropdownMenuSubMenu 54.0KB 54.0KB 0B 9.3KB 9.3KB 0B
core/Markdown 132.7KB 133.1KB +402B 16.9KB 16.9KB 0B
core/MoreMenu 68.8KB 69.1KB +306B 14.4KB 14.4KB 0B
core/MultiSelector 71.6KB 72.0KB +395B 14.1KB 14.1KB 0B
core/Pagination 83.1KB 83.4KB +346B 14.8KB 14.8KB 0B
core/PowerSearch 143.4KB 143.7KB +341B 18.1KB 18.1KB 0B
core/Selector 74.5KB 74.9KB +386B 14.5KB 14.5KB 0B
core/SelectorOption 14.4KB 14.4KB 0B 4.8KB 4.8KB 0B
core/Table 79.6KB 79.9KB +359B 14.6KB 14.6KB 0B
core/TableCell 74.5KB 74.9KB +376B 14.4KB 14.4KB 0B
core/TableHeaderCell 74.3KB 74.7KB +357B 14.3KB 14.3KB 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.7KB 33.7KB 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

@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 change is on hold for an engineering owner's decision: no current spec states the new compact-touch rule (a coarse pointer with a viewport under 744px wide or 500px tall) that presentation="adaptive" and the default DropdownMenuSubMenu drill-in now follow, and the cited spec:AST-094 FR13 is draft and names no thresholds. An engineering owner needs to approve the rule and identify the record that states it, for example an amended component:Selector DEC-1 or FR13 with the numbers, by commenting /approve-spec <full head SHA> on this pull request; the author may record it if they are an engineering owner.

  • Add a changeset sentence for the default sub-menu change: on a phone in landscape sub-menus now drill in, and on an iPad mini in portrait they fly out.
  • spec:AST-094 FR7 cites spec:AST-090, which is not on main; link where it lives or drop the reference until it lands.

[Automated review]

Comment thread .changeset/touch-presentation.md Outdated
'@astryxdesign/core': patch
---

[feat] `presentation="adaptive"` on DropdownMenu, ContextMenu, MoreMenu, Selector and MultiSelector chooses the bottom sheet on a compact touch screen: a coarse primary pointer and a viewport narrower than 744px or shorter than 500px (it was 768px wide and below). An iPad mini in portrait keeps the anchored popover; a phone in landscape now gets the sheet.

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 note covers the root presentation="adaptive" only. DropdownMenuSubMenu defaults to presentation="adaptive" and drills in from the same COMPACT_TOUCH_PRESENTATION_QUERY, so every default sub-menu changes too: on a phone in landscape it now drills in, and on an iPad mini in portrait it flies out. Please add a sentence for that.

follows.
- **FR7 — Exits animate as UIKit's do.** A touch presentation MUST play the
exit its UIKit counterpart plays. **Proposed; pending owner review:** this
departs from the draft `spec:AST-090` FR1 (exits off by default) and FR10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-090 does not exist on main or at this head, so a reader cannot check the FR1 and FR10 this departs from; link where it lives or hold this note until it lands.

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Update: in #8331 (commit 3ff7e84), core's keys stay literal, pinned to the resolver's constants by test, because the StyleX build gives an imported at-rule key a different priority than the literal (the repro goes upstream). touchKeys is exported as the spelling to match.

vjeux added 2 commits October 24, 2026 17:58
…ation

Nothing reads it: an adaptive overlay decides through
useAdaptivePresentation and the compact definition's coarse-pointer query.
useTouchPresentation and readTouchPresentation answer isTouch and
isCompact.
Four frames sized at the compact edges (767 and 768 px wide, 499 and 500 px
tall) report what useAdaptivePresentation('adaptive') picks: the bottom
sheet at 767 and 499 under a finger, the popover at 768 and 500 and
everywhere under a mouse, in Chromium and WebKit.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
The maintainers ruled one touch model on every touch device, iOS and Android
alike, with no platform idiom (the touch resolver in meta#8062, now in meta#8076; AST-094
in meta#8316): touch behaviors key on a coarse primary pointer, and the resolver no
longer reports an idiom. The slider's touch presentation (the thumbs' touch
targets, UISlider's tracking and coast, the track's tap, the lift) now applies
under any finger; a mouse or a trackpad keeps the pointer handlers. The docs,
story and changeset say "under a finger" instead of "on iOS".

Tests: the Android cases that kept today's slider are flipped. The unit case
gives a coarse pointer with no iOS engine (no -webkit-touch-callout) the touch
targets and leaves a finger on the track to the touch presentation; it fails on
the previous head. The interaction cases run with no iOS-engine emulation, and
the Android case now checks the same model there: the lift, the travel past the
threshold and a track tap that sets the value on its lift (the Android Reduce
Motion case was the iOS one's duplicate and goes). The fine-pointer cases stay
as they were: no touch targets, and the desktop geometry case's pointer
styles.

@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 adaptive presentation and sub-menu drill-in now follow the theme's compact definition, which matches the current spec:AST-012 FR9 and Selector record, and the iOS simulator results cover the Safari edge check. One change is needed before approval: the new browser proofs under packages/core/src/utils/__tests__/browser/ run under their own Playwright config that no CI job picks up, and spec:AST-030 FR11 asks new coverage to join the existing owner, so please add them to the root Playwright config with a coarse-pointer project, or drop them and keep the results in the description. Optional: add a sub-menu test under a theme that moves md, make readTouchPresentation's compact argument required, and mention ContextMenu sub-menus in the changeset.

[Automated review]

Comment thread .changeset/touch-presentation.md Outdated
'@astryxdesign/core': patch
---

[feat] `presentation="adaptive"` on DropdownMenu, ContextMenu, MoreMenu, Selector and MultiSelector, and a DropdownMenu's default sub-menu drill-in, now use the theme's compact definition with a coarse primary pointer: a viewport narrower than the theme's `md` point (768px by default) or shorter than 500px. This is a behavior change. The old default was a bottom sheet at 768px wide and below on a coarse pointer. Now a coarse-pointer screen exactly 768px wide keeps the anchored popover, and a phone in landscape (wider than 768px but shorter than 500px) gets the bottom sheet and the drilled-in sub-menu. A theme that moves `md` moves the edge with it. To pin a component's presentation regardless of the rule, pass `presentation="popover"` or `presentation="bottom-sheet"`; to keep a sub-menu's flyout, pass `presentation="flyout"` on `DropdownMenuSubMenu`. Mouse and keyboard users on desktop see no change, because every case requires a coarse pointer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ContextMenu's sub-menu rows decide the drill-in through the same useMenuDrillIn (packages/core/src/ContextMenu/ContextMenu.tsx:668), so they change too without opting into adaptive. Could this name them, for example "a DropdownMenu's or ContextMenu's default sub-menu drill-in"? The presentation="flyout" advice already covers both.

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.

Done in 4b5e872. The changeset now names "a DropdownMenu's or ContextMenu's default sub-menu drill-in".

* opens and the stack empties when it closes.
*/
export function useMenuDrillIn(isOpen: boolean): UseMenuDrillInReturn {
const compactTouchQuery = useCompactDefinition().coarsePointerQuery;

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 test pins this theme read: replacing it with DEFAULT_COMPACT_DEFINITION.coarsePointerQuery leaves the touch-presentation, DropdownMenu, DropdownMenuSubMenu and ContextMenu suites green, while the same swap in useAdaptivePresentation fails the md: 1000 case. One sub-menu case under that theme would keep the drill-in and the root sheet from drifting apart.

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.

Pinned in f809dae. On a 900 px coarse-pointer screen, the sub-menu flies out under the default md (768) and drills in under a theme whose md is 1000. Replacing the theme read with DEFAULT_COMPACT_DEFINITION.coarsePointerQuery makes the test fail.

*/
export function readTouchPresentation(
view: Window | null = typeof window === 'undefined' ? null : window,
compact: CompactDefinition = DEFAULT_COMPACT_DEFINITION,

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 default ignores a theme that moves md, while spec:AST-012 FR9 wants the effective point. Nothing calls readTouchPresentation yet, so making compact required now keeps its first caller from inheriting the default.

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.

Done in 4b5e872. readTouchPresentation(view, compact) now requires the compact definition, so its first caller passes the theme's (useCompactDefinition()).

vjeux added 3 commits October 24, 2026 22:23
…cription

They ran under their own Playwright config, which no CI job picks up;
spec:AST-030 FR11 asks new coverage to join its existing owner. Their
results (Chromium and WebKit, and Mobile Safari on the iOS 26.2 simulator)
stay in the pull request description, and the unit suites keep the
resolver's answers.
Nothing pinned useMenuDrillIn's theme read: a fixed default query left the
suites green. A 900 px coarse screen now flies the sub-menu out under the
default md (768) and drills in under a theme whose md is 1000, as the root
sheet does (spec:AST-012 FR9).
… reads

Its default ignored a theme that moves md; nothing calls it yet, so the
compact definition is now required and its first caller passes the
theme's (useCompactDefinition()). The changeset names ContextMenu's
sub-menus, whose drill-in changes too.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
… into the touch foundation

# Conflicts:
#	packages/core/src/utils/touchPresentation.ts
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…a#8062 settled on

meta#8076 now carries meta#8062's resolver: TouchPresentation is isTouch and
isCompact (the unused isCompactTouch is gone), and readTouchPresentation
takes the compact definition it reads. resolvePresentation asks for a
compact touch screen as isTouch && isCompact, and readPresentation takes
the theme's compact definition (useCompactDefinition(), read while
rendering), so a theme that moves md moves the one-moment read too
(spec:AST-012 FR9).
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
The maintainers ruled one touch model on every touch device, iOS and Android
alike, with no platform idiom (the touch resolver in meta#8062, now in meta#8076; AST-094
in meta#8316): touch behaviors key on a coarse primary pointer, and the resolver no
longer reports an idiom. The slider's touch presentation (the thumbs' touch
targets, UISlider's tracking and coast, the track's tap, the lift) now applies
under any finger; a mouse or a trackpad keeps the pointer handlers. The docs,
story and changeset say "under a finger" instead of "on iOS".

Tests: the Android cases that kept today's slider are flipped. The unit case
gives a coarse pointer with no iOS engine (no -webkit-touch-callout) the touch
targets and leaves a finger on the track to the touch presentation; it fails on
the previous head. The interaction cases run with no iOS-engine emulation, and
the Android case now checks the same model there: the lift, the travel past the
threshold and a track tap that sets the value on its lift (the Android Reduce
Motion case was the iOS one's duplicate and goes). The fine-pointer cases stay
as they were: no touch targets, and the desktop geometry case's pointer
styles.

@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 approve. presentation="adaptive" and the default sub-menu drill-in now follow the theme's compact definition with a coarse pointer, matching spec:AST-012 FR9, architecture:public-component-api INV16 and INV17 and the Selector record, and the earlier follow-ups are in: the standalone browser specs are gone, the sub-menu md test and the required compact argument were added, and the Changeset names ContextMenu sub-menus. Browser checks show the surface switching only at the new edges (under the md point wide or under 500px tall, on a coarse pointer), while mouse users see no change. No further changes are needed from the contributor.

[Automated review]

@cixzhang
cixzhang merged commit cd3787b into meta:main Oct 25, 2026
74 checks passed
@vjeux
vjeux deleted the wip/uikit-pr-touch-presentation branch October 25, 2026 20:39
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
Brings the harness as resolved against main (meta#8043 merged), so this stack
carries one copy of it. The only conflict, touchPresentation.ts, keeps
this pull request's TOUCH_QUERY move to touchQueries.ts over main's
resolver (meta#8062).

This branch was successfully deployed

1 active deployment
Preview — 4b5e8723 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants