Skip to content

test(DropdownMenu): a footer stays reachable by Tab when the React root is the document - #9815

Merged
cixzhang merged 3 commits into
mainfrom
fix/focus-trap-handled-tab
Nov 4, 2026
Merged

cixzhang merged 3 commits into
mainfrom
fix/focus-trap-handled-tab

Conversation

@imdreamrunner

@imdreamrunner imdreamrunner commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

User impact

In an app whose React root is the document (every Next.js App Router app), a keyboard user could not reach the controls in a DropdownMenu footer: the popover's focus trap undid the menu's own Tab move. The fix shipped in #8883, which makes the focus trap leave a Tab whose default is already prevented to the content that handled it. This pull request adds the regression tests for that case, so the defect cannot come back unnoticed. It changes tests only; nothing ships to consumers.

What the tests prove

  • DropdownMenuDocumentRoot.test.tsx (new file; React attaches its root listeners only for the first root in a test environment, so the document root needs its own file): with a footer holding two controls, Tab from the last row goes through both footer controls; Shift+Tab from the last footer control goes back through the footer to the menu; Tab past the last footer control wraps to the menu, and Shift+Tab wraps back.
  • useFocusTrap.test.tsx › "a Tab that content inside has already handled": content moves focus to the last control (Tab) or the first (Shift+Tab) and the trap keeps it there; after a handled Tab, a later move outside is not pulled back in; an unhandled Tab still wraps at either end, which guards against a guard that is too broad.

Authority

  • component:DropdownMenu FR15 / AR5 / DEC-10 (current): with a header or footer, Tab and Shift+Tab move through the header's controls, the menu, then the footer's controls, and wrap at either end.
  • component:Popover AR3: Tab inside the open popup wraps, except where a child role's own record states its own Tab model, as DropdownMenu FR15 does.

Testing

  • On current main: npx vitest run packages/core/src/DropdownMenu/DropdownMenuDocumentRoot.test.tsx packages/core/src/hooks/useFocusTrap.test.tsx: 38 passed.
  • With the event.defaultPrevented early return from fix(Dialog, AlertDialog, CommandPalette, BottomSheet, Lightbox): Tab wraps inside the open modal through the shared focus-region module #8883 removed from useFocusTrap: 5 of the new tests fail (3 in useFocusTrap.test.tsx, 2 in DropdownMenuDocumentRoot.test.tsx). In jsdom the forward footer case passes either way, because jsdom returns the trap's focusable-selector matches grouped by selector rather than in document order.
  • Prettier and eslint on the changed files; pnpm check:changesets; changeset coverage: nothing here ships to consumers, so no Changeset.

Scope

…andled, so a DropdownMenu footer is reachable in a Next.js app
@vercel

vercel Bot commented Nov 3, 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 4, 2026 9:46pm UTC

Request Review

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

astracat-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 4, 2026, 9:53 PM UTC

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

This looks ready to merge. The early return in useFocusTrap restores the DropdownMenu header and footer Tab walk that FR15 describes for apps whose React root is the document, and the new tests fail without it and pass with it.

One optional, non-blocking suggestion: the comment at DropdownMenu.tsx:1192-1194 packs three ideas into one sentence, and the inline note offers a shorter version.

[Automated review]

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

This looks ready to merge. The early return in useFocusTrap restores the DropdownMenu header and footer Tab walk that FR15 describes for apps whose React root is the document: the new tests fail without it and pass with it, and on a document-root page in Chromium every Tab and Shift+Tab lands where FR15 says, while an unhandled Tab still wraps.

[Automated review]

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 3951b2b

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
SideNavItem 55.3 kB (+12 B) · 10.5 kB CSS (0) — — · — · —
DropdownMenu 74.1 kB (+11 B) · 15.5 kB CSS (0) — ✓ · ✓ · ✓
TabMenu 56.3 kB (+11 B) · 10.5 kB CSS (0) — — · — · —
45 more components
Component Size LoC Visual · A11y · RTL
TopNavHeading 59.1 kB (+11 B) · 10.4 kB CSS (0) — — · — · —
ChatComposerInput 59.9 kB (+10 B) · 10.7 kB CSS (0) — — · — · —
ChatComposerTokenElement 59.9 kB (+10 B) · 10.7 kB CSS (0) — — · — · —
SideNavHeading 59.7 kB (+10 B) · 10.6 kB CSS (0) — — · — · —
TopNavMegaMenu 54.8 kB (+10 B) · 11.2 kB CSS (0) — — · — · —
Typeahead 63.6 kB (+10 B) · 11.8 kB CSS (0) — — · — · —
BaseTypeahead 55.9 kB (+9 B) · 10.3 kB CSS (0) — — · — · —
Popover 53.3 kB (+9 B) · 12.7 kB CSS (0) — — · — · —
TopNavMenu 58.9 kB (+9 B) · 10.5 kB CSS (0) — — · — · —
lab/ChatEmojiPicker 66.9 kB (+9 B) · 773 B CSS (0) — — · — · —
lab/ChatReactionBar 67.5 kB (+9 B) · 1.1 kB CSS (0) — — · — · —
lab/TourStep 58.6 kB (+8 B) · 590 B CSS (0) — — · — · —
DateRangeInput 72.1 kB (+7 B) · 11.9 kB CSS (0) — — · — · —
Tokenizer 71.3 kB (+7 B) · 17.1 kB CSS (0) — — · — · —
Pagination 87.3 kB (+6 B) · 16.1 kB CSS (0) — — · — · —
lab/CommentThread 254.2 kB (+6 B) · 2.4 kB CSS (0) — — · — · —
ChatComposer 62.0 kB (+5 B) · 11.2 kB CSS (0) — — · — · —
Drawer 39.3 kB (+5 B) · 8.2 kB CSS (0) — — · — · —
MultiSelector 73.7 kB (+5 B) · 15.3 kB CSS (0) — — · — · —
Selector 78.4 kB (+5 B) · 15.7 kB CSS (0) — — · — · —
BreadcrumbItem 68.4 kB (+4 B) · 11.5 kB CSS (0) — — · — · —
DateInput 106.0 kB (+4 B) · 15.4 kB CSS (0) — — · — · —
PowerSearch 194.3 kB (+4 B) · 21.6 kB CSS (0) — — · — · —
ComplexSelector 56.3 kB (+3 B) · 11.1 kB CSS (0) — — · — · —
DateTimeInput 162.7 kB (+3 B) · 17.1 kB CSS (0) — — · — · —
lab/CommentComposer 105.5 kB (+3 B) · 1.9 kB CSS (0) — — · — · —
lab/TransferListSelector 83.2 kB (+3 B) · 1.9 kB CSS (0) — — · — · —
MoreMenu 74.4 kB (+2 B) · 15.5 kB CSS (0) — — · — · —
richtext/RichTextEditorToolbar 225.6 kB (+2 B) · 454 B CSS (0) — — · — · —
lab/Comments 259.4 kB (+1 B) · 2.5 kB CSS (0) — — · — · —
BottomSheetSwitcher 17.0 kB (0) · 3.4 kB CSS (0) — — · — · —
ContextMenu 75.9 kB (0) · 15.4 kB CSS (0) — — · — · —
ContextMenuItem 26.2 kB (0) · 5.0 kB CSS (0) — — · — · —
DropdownMenuCheckboxItem 18.3 kB (0) · 5.9 kB CSS (0) — ✓ · ✓ · ✓
DropdownMenuDivider 5.2 kB (0) · 548 B CSS (0) — ✓ · ✓ · —
DropdownMenuGroup 6.2 kB (0) · 3.4 kB CSS (0) — ✓ · ✓ · —
DropdownMenuItem 17.5 kB (0) · 5.5 kB CSS (0) — ✓ · ✓ · —
DropdownMenuRadioGroup 6.1 kB (0) · 3.3 kB CSS (0) — ✓ · ✓ · —
DropdownMenuRadioItem 18.3 kB (0) · 5.9 kB CSS (0) — ✓ · ✓ · —
DropdownMenuSubMenu 56.5 kB (0) · 10.0 kB CSS (0) — ✓ · ✓ · ✓
Markdown 146.3 kB (0) · 18.4 kB CSS (0) — — · — · —
SideNav 68.8 kB (0) · 11.2 kB CSS (0) — — · — · —
Table 85.3 kB (0) · 16.1 kB CSS (0) — — · — · —
TableCell 77.1 kB (0) · 15.9 kB CSS (0) — — · — · —
TableHeaderCell 76.7 kB (0) · 15.6 kB CSS (0) — — · — · —

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

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

main's focus-region containment (#8883) already leaves a Tab whose default is prevented to the content that handled it. Resolve useFocusTrap.ts to main's handler and keep this branch's regression tests: the DropdownMenu document-root test and the useFocusTrap tests. Drop the changeset, the hook doc sentence and the source comment edits, so the branch changes tests only.
@cixzhang cixzhang changed the title fix(useFocusTrap): the trap no longer undoes a Tab that content has handled, so a DropdownMenu footer is reachable in a Next.js app test(DropdownMenu): a footer stays reachable by Tab when the React root is the document Nov 4, 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.

Approved. The new tests lock in the DropdownMenu footer Tab walk for apps whose React root is the document, which #8883 fixed. All 38 tests in the two files pass, and removing the focus trap's early return for a prevented Tab makes 5 of the 7 new tests fail.

Optional follow-up, not blocking: the comments at DropdownMenu.tsx:1207-1209 and menuRegionStops.ts:120-121 still say the document-level trap never reads the menu's Tab a second time, which the new test file shows is not true when the React root is the document.

[Automated review]

@cixzhang
cixzhang merged commit b215c3e into main Nov 4, 2026
63 checks passed
@github-actions
github-actions Bot deleted the fix/focus-trap-handled-tab branch November 5, 2026 06:03

This branch was successfully deployed

1 active deployment
Preview — f47c513c Deployed Nov 4, 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