Repository navigation
fix(core): a presentation resolves with no matchMedia, as in a test DOM - #10275
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
| 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
|
/approve-spec b01a7c3 |
There was a problem hiding this comment.
Changes requested: the guard fixes the crash for ContextMenu, DropdownMenu, PowerSearch, Selector and Tokenizer, but two things need to change first.
Popoverwithpresentation="adaptive"still throwswindow.matchMedia is not a functionin a DOM withoutmatchMedia, because its own resolver (usePresentationSurfaceinPopover.tsx) is unguarded, while the changeset sayspresentation="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
matchMediacheck is copied into the hand-rolled subscriptions inpresentation.tsanduseAdaptivePresentation.tsinstead of subscribing throughwatchMediaQueryinutils/coarsePointer.ts, which already handles a missingmatchMedia; route all three resolvers through it, with one shared guarded read besideisCoarsePointer, 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). |
There was a problem hiding this comment.
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.
| const subscribe = useCallback( | ||
| (onStoreChange: () => void) => { | ||
| if (query == null) { | ||
| if (query == null || typeof window.matchMedia !== 'function') { |
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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.
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.
|
/approve-spec bef26da |
There was a problem hiding this comment.
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]
| * while it holds. useMediaQuery and the presentation resolvers read and | ||
| * watch through matchesMediaQuery and watchMediaQuery |
There was a problem hiding this comment.
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".
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 nomatchMediaunless a test stubs it. Today that happens for aContextMenu,DropdownMenu,Popover,PowerSearch,SelectororTokenizerwithpresentation="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,useAdaptivePresentationand Popover's own resolver read the pointer and the compact size throughwindow.matchMediain their external-store snapshot, with no check that it exists.useMediaQueryalready checks: "Where the window has nomatchMedia, as in a test DOM, it returnsserverDefault". So doesreadTouchPresentation. All three now read and watch throughutils/coarsePointer.ts: its existingwatchMediaQueryand a newmatchesMediaQuerybesideisCoarsePointer. Both answer "no match" wherematchMediais 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 nomatchMedia, and the presentation hooks' own answer on the server and while hydrating (spec:AST-094FR13).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
TypeError: window.matchMedia is not a function: an adaptive ContextMenu, DropdownMenu and Popover, and the hooks.presentationWithoutMatchMedia.test.tsx, and the newpresentation.test.tsandcoarsePointer.test.tscases.Scope
Testing
On main 66b0952.
TypeError: window.matchMedia is not a function, and at d733bc7 the Popover case does.useTableSortableStatetemplate test passes (3) with nomatchMediastub.