Skip to content

fix(core): a presentation resolves with no matchMedia, as in a test DOM - #10275

Merged
cixzhang merged 4 commits into
meta:mainfrom
vjeux:core/c20-presentation-no-matchmedia
Nov 6, 2026
Merged

cixzhang merged 4 commits into
meta:mainfrom
vjeux:core/c20-presentation-no-matchmedia

Conversation

@vjeux

@vjeux vjeux commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

User impact

An app whose own jsdom tests render a menu that resolves its presentation from the pointer crashes with window.matchMedia is not a function. jsdom has no matchMedia unless a test stubs it. Today that happens for a ContextMenu, DropdownMenu, Popover, PowerSearch, Selector or Tokenizer with presentation="adaptive". Once ContextMenu's touch default reads the pointer, it would happen for every ContextMenu, and every Table whose cells render one. After this change they render their default popover instead.

Problem and solution fit

usePresentation, useAdaptivePresentation and Popover's own resolver read the pointer and the compact size through window.matchMedia in their external-store snapshot, with no check that it exists. useMediaQuery already checks: "Where the window has no matchMedia, as in a test DOM, it returns serverDefault". So does readTouchPresentation. All three now read and watch through utils/coarsePointer.ts: its existing watchMediaQuery and a new matchesMediaQuery beside isCoarsePointer. Both answer "no match" where matchMedia is missing, which is the server's answer: the default popover, no touch modes.

Expected behavior and authority

useMediaQuery's documented contract for a window with no matchMedia, and the presentation hooks' own answer on the server and while hydrating (spec:AST-094 FR13).

Smallest restoration

One guarded read in coarsePointer.ts, used by the three resolvers' subscription and snapshot, never during a server render. No API, default or visual change.

Evidence

  • Reproduction before the change: the new cases fail with TypeError: window.matchMedia is not a function: an adaptive ContextMenu, DropdownMenu and Popover, and the hooks.
  • Result after the change: all pass; the hooks answer the default presentation.
  • Representative unchanged path: every existing presentation case, including server and hydrating renders, live pointer changes and the one-listener rule (INV5).
  • Regression test: presentationWithoutMatchMedia.test.tsx, and the new presentation.test.ts and coarsePointer.test.ts cases.

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

On main 66b0952.

  • Red first: at 15fd50c the hook and two component cases fail with TypeError: window.matchMedia is not a function, and at d733bc7 the Popover case does.
  • Local checks at this head: core typecheck; the presentation, coarsePointer, Popover and gestures units (966 passed); eslint and Prettier on the changed files. At b01a7c3: the utils, hooks, ContextMenu, DropdownMenu, PowerSearch, Selector and Tokenizer units (3,593 passed); the CLI template tests (237 passed); check:knowledge; check:changesets.
  • With this guard on feat(ContextMenu): open from a held finger on iOS 26's timeline #8125's head and main, the useTableSortableState template test passes (3) with no matchMedia stub.

vjeux added 2 commits November 6, 2026 04:33
A ContextMenu or a DropdownMenu with `presentation="adaptive"`, and the
presentation hooks themselves, throw where the window has no
matchMedia: `window.matchMedia is not a function`. An app's own jsdom
tests have no matchMedia unless they stub it. These cases fail until
the next commit.
`usePresentation` and `useAdaptivePresentation` read the pointer and
the compact size through matchMedia. Where the window has none, they now
answer no match, so the component renders its default popover, as
`useMediaQuery` already does. The check runs in the subscription and
the snapshot, never during a server render.
@vercel

vercel Bot commented Nov 6, 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 Nov 6, 2026 1:29pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 6, 2026
@astracat-bot

astracat-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 6, 2026, 1:35 PM UTC

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · bef26da

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
TableCell 91.7 kB (+29 B) · 17.0 kB CSS (0) — — · — · —
TreeList 41.1 kB (+27 B) · 5.4 kB CSS (0) — — · — · —
lab/ChatEmojiPicker 78.6 kB (+26 B) · 773 B CSS (0) — — · — · —
114 more components
Component Size LoC Visual · A11y · RTL
SideNavItem 61.7 kB (+21 B) · 10.4 kB CSS (0) — — · — · —
lab/TourStep 67.1 kB (+21 B) · 590 B CSS (0) — — · — · —
TimeInput 149.7 kB (+19 B) · 15.5 kB CSS (0) — — · — · —
TabMenu 63.1 kB (+17 B) · 10.4 kB CSS (0) — — · — · —
DateRangeInput 85.7 kB (+16 B) · 12.1 kB CSS (0) — — · — · —
lab/ChatReactionBar 79.8 kB (+16 B) · 1.1 kB CSS (0) — — · — · —
lab/EmojiPicker 81.3 kB (+16 B) · 751 B CSS (0) — — · — · —
lab/TransferList 90.0 kB (+16 B) · 1.7 kB CSS (0) — — · — · —
lab/TransferListSelector 97.0 kB (+16 B) · 1.9 kB CSS (0) — — · — · —
AvatarGroupOverflow 44.5 kB (+15 B) · 5.5 kB CSS (0) — — · — · —
CommandPaletteItem 11.9 kB (+15 B) · 3.6 kB CSS (0) — — · — · —
DropdownMenuItem 20.5 kB (+15 B) · 6.0 kB CSS (0) — — · — · —
Lightbox 103.0 kB (+15 B) · 9.8 kB CSS (0) — — · — · —
SelectableCard 16.9 kB (+15 B) · 7.3 kB CSS (0) — — · — · —
TableSelectionToolbar 61.2 kB (+15 B) · 12.4 kB CSS (0) — — · — · —
Toast 89.3 kB (+15 B) · 9.6 kB CSS (0) — — · — · —
TopNavMegaMenu 61.1 kB (+15 B) · 11.2 kB CSS (0) — — · — · —
richtext/RichTextView 271.1 kB (+15 B) · 2.3 kB CSS (0) — — · — · —
Carousel 59.7 kB (+14 B) · 10.3 kB CSS (0) — — · — · —
DateInput 172.8 kB (+14 B) · 16.4 kB CSS (0) — — · — · —
SelectorOption 19.5 kB (+14 B) · 5.8 kB CSS (0) — — · — · —
TextInput 62.8 kB (+14 B) · 10.8 kB CSS (0) — — · — · —
Token 27.4 kB (+14 B) · 4.7 kB CSS (0) — — · — · —
richtext/RichTextEditor 290.9 kB (+14 B) · 2.6 kB CSS (0) — — · — · —
ChatDictationButton 52.3 kB (+13 B) · 9.4 kB CSS (0) — — · — · —
ChatLayoutScrollButton 52.2 kB (+13 B) · 9.5 kB CSS (0) — — · — · —
DrawerHeader 54.7 kB (+13 B) · 12.2 kB CSS (0) — — · — · —
Link 48.7 kB (+13 B) · 6.9 kB CSS (0) — — · — · —
Slider 101.8 kB (+13 B) · 8.7 kB CSS (0) — — · — · —
Tab 13.3 kB (+13 B) · 4.2 kB CSS (0) — — · — · —
ToggleButton 52.6 kB (+13 B) · 9.4 kB CSS (0) — — · — · —
lab/NavigationScreen 78.4 kB (+13 B) · 1.5 kB CSS (0) — — · — · —
lab/NavigationStack 78.4 kB (+13 B) · 1.5 kB CSS (0) — — · — · —
lab/SelectableCardGroup 59.7 kB (+13 B) · 162 B CSS (0) — — · — · —
AppShell 81.2 kB (+12 B) · 12.8 kB CSS (0) — — · — · —
Calendar 62.5 kB (+12 B) · 9.9 kB CSS (0) — — · — · —
ClickableCard 16.7 kB (+12 B) · 7.1 kB CSS (0) — — · — · —
DialogHeader 54.5 kB (+12 B) · 10.8 kB CSS (0) — — · — · —
DropdownMenuCheckboxItem 21.3 kB (+12 B) · 6.4 kB CSS (0) — — · — · —
FileInput 64.2 kB (+12 B) · 10.6 kB CSS (0) — — · — · —
SideNavCollapseButton 52.5 kB (+12 B) · 9.4 kB CSS (0) — — · — · —
SideNavHeading 66.8 kB (+12 B) · 10.6 kB CSS (0) — — · — · —
ChatLayout 55.0 kB (+11 B) · 9.8 kB CSS (0) — — · — · —
ChatSendButton 52.1 kB (+11 B) · 9.3 kB CSS (0) — — · — · —
IconButton 51.7 kB (+11 B) · 9.3 kB CSS (0) — — · — · —
MobileNavToggle 52.0 kB (+11 B) · 9.3 kB CSS (0) — — · — · —
Button 51.7 kB (+10 B) · 9.3 kB CSS (0) — — · — · —
CheckboxInput 59.9 kB (+10 B) · 9.1 kB CSS (0) — — · — · —
CheckboxListItem 65.9 kB (+10 B) · 10.9 kB CSS (0) — — · — · —
richtext/RichTextEditorToolbar 244.0 kB (+10 B) · 454 B CSS (0) — — · — · —
Banner 53.7 kB (+9 B) · 9.8 kB CSS (0) — — · — · —
BreadcrumbItem 75.9 kB (+9 B) · 11.5 kB CSS (0) — — · — · —
DropdownMenuSubMenu 64.6 kB (+9 B) · 9.4 kB CSS (0) — — · — · —
ListItem 64.9 kB (+9 B) · 10.9 kB CSS (0) — — · — · —
Outline 23.7 kB (+9 B) · 4.1 kB CSS (0) — — · — · —
RadioListItem 62.7 kB (+9 B) · 9.8 kB CSS (0) — — · — · —
Switch 96.3 kB (+9 B) · 9.8 kB CSS (0) — — · — · —
TopNavHeading 66.3 kB (+9 B) · 10.4 kB CSS (0) — — · — · —
TopNavItem 12.6 kB (+9 B) · 4.0 kB CSS (0) — — · — · —
lab/DiffViewer 78.1 kB (+9 B) · 2.1 kB CSS (0) — — · — · —
lab/PageControl 71.7 kB (+9 B) · 709 B CSS (0) — — · — · —
ButtonGroup 40.1 kB (+8 B) · 3.7 kB CSS (0) — — · — · —
ChatComposerInput 66.3 kB (+8 B) · 10.6 kB CSS (0) — — · — · —
Collapsible 24.8 kB (+8 B) · 4.5 kB CSS (0) — — · — · —
ContextMenuItem 26.8 kB (+8 B) · 6.1 kB CSS (0) — — · — · —
DropdownMenuRadioItem 21.3 kB (+8 B) · 6.4 kB CSS (0) — — · — · —
Step 29.8 kB (+8 B) · 5.6 kB CSS (0) — — · — · —
TableHeaderCell 91.3 kB (+8 B) · 16.8 kB CSS (0) — — · — · —
BottomSheet 58.6 kB (+7 B) · 7.5 kB CSS (0) — — · — · —
ChatComposer 68.3 kB (+7 B) · 11.2 kB CSS (0) — — · — · —
ChatComposerTokenElement 66.3 kB (+7 B) · 10.6 kB CSS (0) — — · — · —
CodeBlock 66.3 kB (+7 B) · 10.6 kB CSS (0) — — · — · —
CommandPalette 59.5 kB (+7 B) · 12.4 kB CSS (0) — — · — · —
ComplexSelector 65.8 kB (+7 B) · 11.3 kB CSS (0) — — · — · —
DropdownMenu 86.0 kB (+7 B) · 16.7 kB CSS (0) — — · — · —
Item 16.1 kB (+7 B) · 5.3 kB CSS (0) — — · — · —
SegmentedControlItem 16.2 kB (+7 B) · 4.3 kB CSS (0) — — · — · —
TopNav 63.3 kB (+7 B) · 10.2 kB CSS (0) — — · — · —
TopNavMenu 66.0 kB (+7 B) · 10.5 kB CSS (0) — — · — · —
BaseTypeahead 62.0 kB (+6 B) · 10.3 kB CSS (0) — — · — · —
SegmentedControl 89.5 kB (+6 B) · 6.8 kB CSS (0) — — · — · —
SideNav 81.0 kB (+6 B) · 10.7 kB CSS (0) — — · — · —
Stepper 53.4 kB (+6 B) · 9.5 kB CSS (0) — — · — · —
lab/ListInput 95.4 kB (+6 B) · 1.6 kB CSS (0) — — · — · —
charts/Chart 175.9 kB (+5 B) · 494 B CSS (0) — — · — · —
Thumbnail 56.0 kB (+5 B) · 10.9 kB CSS (0) — — · — · —
TopNavMegaMenuItem 12.7 kB (+5 B) · 4.2 kB CSS (0) — — · — · —
MoreMenu 86.3 kB (+4 B) · 16.7 kB CSS (0) — — · — · —
MultiSelector 88.7 kB (+4 B) · 16.3 kB CSS (0) — — · — · —
Popover · Storybook 61.0 kB (+4 B) · 13.4 kB CSS (0) 1,037 (−2) ✓ · ✓ · ✓
Drawer 44.7 kB (+3 B) · 8.2 kB CSS (0) — — · — · —
NumberInput 66.8 kB (+3 B) · 11.1 kB CSS (0) — — · — · —
Typeahead 74.4 kB (+3 B) · 12.1 kB CSS (0) — — · — · —
lab/Schedule 99.3 kB (+3 B) · 3.9 kB CSS (0) — — · — · —
BottomSheetSwitcher 17.5 kB (+2 B) · 3.4 kB CSS (0) — — · — · —
DateTimeInput 195.1 kB (+2 B) · 18.1 kB CSS (0) — — · — · —
Markdown 162.1 kB (+2 B) · 19.5 kB CSS (0) — — · — · —
MobileNav 61.6 kB (+2 B) · 9.9 kB CSS (0) — — · — · —
AlertDialog 95.7 kB (+1 B) · 13.7 kB CSS (0) — — · — · —
Dialog 22.5 kB (0) · 6.7 kB CSS (0) — — · — · —
Overlay 21.6 kB (0) · 3.9 kB CSS (0) — — · — · —
ProgressBar 11.5 kB (0) · 6.2 kB CSS (0) — — · — · —
lab/CommentComposer 120.0 kB (0) · 1.9 kB CSS (0) — — · — · —
lab/ThreeDChart 5.0 kB (0) · 20 B CSS (0) — — · — · —
ContextMenu 90.5 kB (−1 B) · 16.6 kB CSS (0) — — · — · —
Timestamp 23.2 kB (−1 B) · 9.6 kB CSS (0) — — · — · —
lab/CommentPin 52.7 kB (−1 B) · 1.2 kB CSS (0) — — · — · —
Tokenizer 84.8 kB (−3 B) · 18.1 kB CSS (0) — — · — · —
Selector 93.1 kB (−9 B) · 16.9 kB CSS (0) — — · — · —
Table 99.9 kB (−11 B) · 17.3 kB CSS (0) — — · — · —
Pagination 103.3 kB (−14 B) · 17.3 kB CSS (0) — — · — · —
PowerSearch 270.9 kB (−28 B) · 23.1 kB CSS (0) — — · — · —
lab/Comments 301.7 kB (−31 B) · 2.5 kB CSS (0) — — · — · —
lab/CommentThread 296.1 kB (−35 B) · 2.4 kB CSS (0) — — · — · —

Package size: @astryxdesign/core 6.75 MB packed (+163 B); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: cli 19 rows, core 24 rows; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec b01a7c3

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 6, 2026

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

Changes requested: the guard fixes the crash for ContextMenu, DropdownMenu, PowerSearch, Selector and Tokenizer, but two things need to change first.

  • Popover with presentation="adaptive" still throws window.matchMedia is not a function in a DOM without matchMedia, because its own resolver (usePresentationSurface in Popover.tsx) is unguarded, while the changeset says presentation="adaptive" no longer crashes; guard Popover too and add a Popover case to the new test, or narrow the changeset to the components this fixes.
  • The matchMedia check is copied into the hand-rolled subscriptions in presentation.ts and useAdaptivePresentation.ts instead of subscribing through watchMediaQuery in utils/coarsePointer.ts, which already handles a missing matchMedia; route all three resolvers through it, with one shared guarded read beside isCoarsePointer, so a later fix cannot miss one of them.

The new tests fail before the fix and pass after it, and the restored behavior matches the Tokenizer record's "the resolver answers popover where there is no matchMedia".

[Automated review]

'@astryxdesign/core': patch
---

[fix] `presentation="adaptive"` no longer crashes in environments without `matchMedia` (jsdom tests).

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 reads as every presentation="adaptive", but Popover resolves adaptive through its own usePresentationSurface (Popover.tsx), which still throws window.matchMedia is not a function in a DOM without matchMedia. Guard Popover too and add a Popover case to the new test, or name the components this fixes.

Comment thread packages/core/src/utils/presentation.ts Outdated
const subscribe = useCallback(
(onStoreChange: () => void) => {
if (query == null) {
if (query == null || typeof window.matchMedia !== 'function') {

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.

watchMediaQuery in utils/coarsePointer.ts already returns a no-op where matchMedia is missing, and useMediaQuery subscribes through it. Subscribing through it here (query == null ? noSubscription() : watchMediaQuery(window, query, onStoreChange)) keeps the check in one owner, so a later fix reaches every resolver.

(onStoreChange: () => void) => {
if (!isAdaptive) {
// A window with no `matchMedia`, as in a test DOM, is not compact.
if (!isAdaptive || typeof window.matchMedia !== 'function') {

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.

Same as in presentation.ts: subscribe through watchMediaQuery (!isAdaptive ? noSubscription() : watchMediaQuery(window, query, onStoreChange)) instead of copying its matchMedia check, and read the snapshot through one shared guarded helper beside isCoarsePointer.

vjeux added 2 commits November 6, 2026 05:18
Popover resolves `presentation="adaptive"` through its own resolver,
which still throws `window.matchMedia is not a function` in a DOM
without matchMedia. coarsePointer's `matchesMediaQuery` reads any query
and matches nothing there. The Popover case fails until the next commit.
The two presentation hooks and Popover's resolver subscribe through
coarsePointer's `watchMediaQuery` and read through `matchesMediaQuery`,
which answer nothing where the window has no matchMedia. The check now
lives in one owner, beside `isCoarsePointer`, so a later fix reaches all
three.
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec bef26da

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

Approved pending CI: CI has not run on this head yet because the pull request is a draft, and it runs once the pull request is marked ready for review.

Both earlier points are addressed: an adaptive Popover no longer throws without matchMedia, and all three resolvers now read and watch through utils/coarsePointer.ts. The new cases fail before the fix and pass after it, and the restored behavior matches the Tokenizer record's "the resolver answers popover where there is no matchMedia". One optional wording fix to the coarsePointer.ts header is left inline.

[Automated review]

Comment on lines +11 to +12
* while it holds. useMediaQuery and the presentation resolvers read and
* watch through matchesMediaQuery and watchMediaQuery

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.

Non-blocking: useMediaQuery only watches through watchMediaQuery; it keeps its own read, because it answers serverDefault where matchMedia is missing. Suggested: "useMediaQuery watches through watchMediaQuery; the presentation resolvers read and watch through matchesMediaQuery and watchMediaQuery".

This branch was successfully deployed

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

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) 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