Repository navigation
feat(core): one touch presentation resolver - #8062
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
…tal touch exports
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewModified ComponentsContextMenu (@astryxdesign/core) · View in Storybook
MoreMenu (@astryxdesign/core) · View in Storybook
MultiSelector (@astryxdesign/core) · View in Storybook
Component Size
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 SummaryGzip figures measure each package's entry file, not the code an import pulls in.
Accessibility AuditStatus: No accessibility violations detected. Visual Regression48 of 502 shot(s) changed. A change here is a question, not a failure: check whether the after is the
and 28 more. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
There was a problem hiding this comment.
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-094FR7 citesspec:AST-090, which is not onmain; link where it lives or drop the reference until it lands.
[Automated review]
| '@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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
…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.
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.
There was a problem hiding this comment.
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]
| '@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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Done in 4b5e872. readTouchPresentation(view, compact) now requires the compact definition, so its first caller passes the theme's (useCompactDefinition()).
…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.
… into the touch foundation # Conflicts: # packages/core/src/utils/touchPresentation.ts
…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).
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.
There was a problem hiding this comment.
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]
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'sspec:AST-012FR9 definition: narrower than the theme'smdpoint (768 px by default) or shorter than 500 px.presentation="popover"or"bottom-sheet"pins the presentation. The changeset says so.Proposed; pending owner review.
Problem and solution fit
mdmoves every edge.Deltas:
utils/touchPresentation.tsholds the touch and compact queries, the SSR-safeuseTouchPresentation, andreadTouchPresentation. Both answer{isTouch, isCompact}from the active theme.'@supports (-webkit-touch-callout: none)'nested inside'@media (pointer: coarse)'. A test keeps every such key spelled and nested that way.useAdaptivePresentationreads the resolver. Docs, stories and tests follow it.isCompactTouchfield, which nothing imports;md, andreadTouchPresentationrequiring the compact definition it reads (nothing calls it yet).Authority
Compact follows
spec:AST-012FR9 (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
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:
Every frame reports
{isTouch: true}from both the hook and the one-moment read, and-webkit-touch-calloutis 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-030FR11); 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 whosemdis 1000.tsc --project packages/core/tsconfig.json --noEmit: clean.pnpm check:changesetsandpnpm check:sync: clean.