Skip to content

fix(Popover): an open popover no longer pulls in a Tab pressed elsewhere on the page - #8611

Merged
cixzhang merged 3 commits into
mainfrom
fix/7687-popover-outside-tab
Oct 27, 2026
Merged

cixzhang merged 3 commits into
mainfrom
fix/7687-popover-outside-tab

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

User impact

Keyboard users on any page with an open Popover. Today a Tab pressed anywhere on the page, even on a control nowhere near the popover, is pulled into the popover. For example, a list next to an open non-modal comment popover can't be reached by Tab. After this change a Tab outside the popover moves to the next control on the page. Focus inside the popover still cycles, and Escape still closes it and returns focus to its trigger.

Problem and solution fit

  • Why does it happen? useFocusTrap marks every Tab on the document as keyboard navigation before it checks where focus is. Its focus listener then sees focus land outside the container and redirects it inside.
  • Why does it hurt? The trap is meant to stop focus leaving the popover. Instead it captures Tabs that never started there, so it takes over the whole page's focus order.
  • Why does that matter? Keyboard and switch users can't reach the rest of the page in order while a popover is open (WCAG 2.4.3 Focus Order, 2.1.1 Keyboard).

The fix marks keyboard navigation only when the Tab starts inside the container. Tabs that start outside are left to the page. Containment of focus that is already inside does not change.

Expected behavior and authority

component:Popover DEC-9 and AR3 (#8608): every Popover is a focus region, modal or not. Containment holds focus that is inside the popup, and a Tab that starts outside it is never redirected into it.

Smallest restoration

One early return in useFocusTrap's Tab handler, plus doc wording for the hook. No new prop, no change to isModal, Escape, light dismiss, initial focus, or focus return. Role-neutral popups (menus, listboxes) keep their own Tab handling.

Evidence

  • Reproduction before the change: the first commit (fc725f7) adds only the tests. Run locally on it, the four "outside" Popover tests (modal and non-modal, Tab and Shift+Tab) and the useFocusTrap "outside a trap with tabbable controls" test fail, because focus is pulled into the popover.
  • Result after the change: all pass.
  • Representative unchanged path: Tab and Shift+Tab cycle inside the popover, Escape returns focus to the trigger (both modes), and the existing trap, Popover, Drawer, BottomSheet, menu, combobox and date-input suites pass.
  • Regression test or other mutation-sensitive proof: removing the early return turns the outside-Tab tests red again.

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

  • pnpm vitest run packages/core/src/hooks packages/core/src/Popover plus the Drawer, BottomSheet, DropdownMenu, TabList, Breadcrumbs, SideNav, TopNav, Typeahead, PowerSearch, Selector, ComplexSelector, Chat, and date input suites.
  • Real browsers: in [Bug] Popover with isModal={false} pulls focus into itself on any Tab on the page #7687, contributors checked this same change in Chrome and Safari. Tabs from elsewhere on the page move on through the page, and Tab from the anchor still reaches the popover through the browser's own popover order, which DEC-9 allows.

Fixes #7687

…ere on the page

useFocusTrap holds focus that is inside its container and leaves a Tab that starts outside to the page (component:Popover DEC-9). Fixes #7687.
@vercel

vercel Bot commented Oct 26, 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 27, 2026 1:35am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 27, 2026, 2:02 AM UTC

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Previews ready: Storybook and Sandbox were built with this PR's docsite. CI evidence will be added when available.

View Storybook for this PR · View Sandbox for this PR

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

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

Changes requested: Tabs that start on a page control now follow the page instead of being pulled into an open popover, as DEC-9 asks, and Tab inside the popover still cycles. One regression needs fixing first: the new guard at packages/core/src/hooks/useFocusTrap.ts:429 treats a focused SVG control inside the popover (for example an <svg tabindex="0"> chart point) as outside, so Tab from it as the last control leaves the popover instead of wrapping, which AR3 does not allow. Checking container.contains(active) without requiring an HTMLElement, plus a test with an SVG control as the last tabbable, should resolve it.

[Automated review]

Comment thread packages/core/src/hooks/useFocusTrap.ts Outdated
if (event.key === 'Tab') {
// Mark that keyboard navigation is happening
const active = doc.activeElement;
if (!isHTMLElement(active) || !container.contains(active)) {

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 guard also treats focus on a non-HTML control inside the container as outside: an SVGElement (an <svg tabindex="0">, an SVG <a href>) is not an HTMLElement, although the trap's focusable selector lists it. With such a control as the popover's last tabbable, Tab from it now leaves the popover (to the trigger, in Chromium and WebKit), where the base wrapped to the first control. Testing the node is enough here:

const active = doc.activeElement;
if (active == null || !container.contains(active)) {
  return;
}

A useFocusTrap.test.tsx case whose last tabbable is an SVG control would pin it.

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

Recommending approval: a Tab or Shift+Tab pressed on a page control now stays with the page while focus inside an open popover still cycles, as component:Popover DEC-9 and AR3 require, and the earlier escape from a focused SVG control is fixed and covered by a test. The new outside-Tab tests fail against the previous focus trap and pass with this change, and Chromium and WebKit checks show the same result. One note for a later change, not this one: with hasAutoFocus={false} and Popover's default wrapped trigger, Tab from the trigger no longer reaches the open popover, because the popover's invoker is the trigger's wrapper rather than the button, so passing the button as the invoker would be a worthwhile follow-up.

[Automated review]

@cixzhang
cixzhang enabled auto-merge (squash) October 27, 2026 02:03
@cixzhang
cixzhang merged commit 81eeb6a into main Oct 27, 2026
69 checks passed
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
Main's meta#8611 (component:Popover DEC-9) made useFocusTrap leave a Tab that starts outside its container to the page. This branch's nested-trap check runs after it: a Tab inside a trap nested in this one's container is still inside this container, so the outer trap still leaves it to the inner one. The doc keeps both sentences.
@github-actions
github-actions Bot deleted the fix/7687-popover-outside-tab branch October 27, 2026 06:22

This branch was successfully deployed

1 active deployment
Preview — e517a833 Deployed Oct 27, 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.

[Bug] Popover with isModal={false} pulls focus into itself on any Tab on the page

1 participant