Skip to content

fix: close Popover when focus escapes outside while trapFocus is enabled#35995

Open
petdud wants to merge 2 commits intomicrosoft:masterfrom
petdud:fix/popover-close-on-focus-escape
Open

fix: close Popover when focus escapes outside while trapFocus is enabled#35995
petdud wants to merge 2 commits intomicrosoft:masterfrom
petdud:fix/popover-close-on-focus-escape

Conversation

@petdud
Copy link
Copy Markdown
Contributor

@petdud petdud commented Apr 16, 2026

Previous Behavior

When a Popover has trapFocus enabled and focus is programmatically moved outside (e.g. via element.focus(), a keyboard shortcut, or OS-level focus changes), the popover remains open in a broken "zombie" state.

This is especially problematic for popovers with interactive content like search inputs — once focus escapes, Tabster's focus trap fights the user: clicking back into the page outside the popover causes Tabster to redirect focus within ~100ms, making it impossible to type or interact. The popover is visually present but functionally dead, and the user has no recovery path other than pressing Escape (if they know about it).

New Behavior

When trapFocus is enabled (both legacy and inertTrapFocus) and focus moves outside both the popover content and trigger, the popover automatically closes via a document-level focusin listener. This fires before Tabster can redirect focus back, so the popover dismisses cleanly instead of entering the broken state.

  • Uses elementContains (virtual-parent-aware) so portaled content (nested menus, dropdowns, dialogs) opened from inside the popover is correctly treated as "inside"
  • Guards against race conditions during initial open (skips when contentRef is not yet mounted)
  • Added native FocusEvent to OpenPopoverEvents union type
  • No new props needed

@petdud petdud force-pushed the fix/popover-close-on-focus-escape branch 2 times, most recently from 9ad4d97 to 20020e9 Compare April 16, 2026 13:31
@@ -0,0 +1,7 @@
{
Copy link
Copy Markdown

@github-actions github-actions bot Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Avatar Converged 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Avatar Converged.badgeMask.normal.chromium.png 4 Changed
vr-tests-react-components/Avatar Converged.badgeMask - RTL.normal.chromium.png 2 Changed
vr-tests-react-components/CalendarCompat 4 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/CalendarCompat.multiDayView - High Contrast.default.chromium.png 1214 Changed
vr-tests-react-components/CalendarCompat.multiDayView.default.chromium_1.png 489 Changed
vr-tests-react-components/CalendarCompat.multiDayView - Dark Mode.default.chromium.png 1101 Changed
vr-tests-react-components/CalendarCompat.multiDayView - RTL.default.chromium.png 488 Changed
vr-tests-react-components/Charts-DonutChart 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Charts-DonutChart.Dynamic - Dark Mode.default.chromium.png 7530 Changed
vr-tests-react-components/Charts-DonutChart.Dynamic - RTL.default.chromium.png 5570 Changed
vr-tests-react-components/Menu 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu.Nested Submenus Small Viewport Flipped.nested menu.chromium.png 743 Changed
vr-tests-react-components/Portal 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Portal.Apply Class Names.should have green border.chromium.png 15 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 132 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 49 Changed
vr-tests-react-components/ProgressBar converged 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - Dark Mode.default.chromium.png 100 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png 194 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 187 Changed
vr-tests-react-components/TagPicker 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled.disabled input hover.chromium.png 677 Changed

There were 2 duplicate changes discarded. Check the build logs for more information.

@petdud petdud force-pushed the fix/popover-close-on-focus-escape branch from 20020e9 to 944f25a Compare April 17, 2026 12:37
@petdud petdud force-pushed the fix/popover-close-on-focus-escape branch from 944f25a to 998e1e3 Compare April 17, 2026 12:40
@petdud petdud force-pushed the fix/popover-close-on-focus-escape branch from 998e1e3 to 31ceae1 Compare April 17, 2026 12:45
@petdud petdud marked this pull request as ready for review April 17, 2026 12:45
@petdud petdud requested a review from a team as a code owner April 17, 2026 12:45
@github-actions
Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-charts
AreaChart
412.925 kB
126.644 kB
413.213 kB
126.725 kB
288 B
81 B
react-charts
DeclarativeChart
763.687 kB
220.664 kB
763.975 kB
220.739 kB
288 B
75 B
react-charts
DonutChart
323.344 kB
97.162 kB
323.632 kB
97.24 kB
288 B
78 B
react-charts
FunnelChart
314.891 kB
94.176 kB
315.179 kB
94.264 kB
288 B
88 B
react-charts
GanttChart
396.033 kB
120.15 kB
396.32 kB
120.291 kB
287 B
141 B
react-charts
GaugeChart
322.777 kB
96.591 kB
323.065 kB
96.676 kB
288 B
85 B
react-charts
GroupedVerticalBarChart
403.908 kB
122.719 kB
404.196 kB
122.86 kB
288 B
141 B
react-charts
HeatMapChart
398.105 kB
122.05 kB
398.393 kB
122.132 kB
288 B
82 B
react-charts
HorizontalBarChart
303.074 kB
89.306 kB
303.362 kB
89.386 kB
288 B
80 B
react-charts
LineChart
424.265 kB
128.706 kB
424.553 kB
128.846 kB
288 B
140 B
react-charts
PolarChart
351.913 kB
107.57 kB
352.201 kB
107.671 kB
288 B
101 B
react-charts
SankeyChart
220.845 kB
68 kB
221.133 kB
68.11 kB
288 B
110 B
react-charts
ScatterChart
403.647 kB
122.833 kB
403.935 kB
122.973 kB
288 B
140 B
react-charts
VerticalBarChart
440.377 kB
128.322 kB
440.665 kB
128.401 kB
288 B
79 B
react-charts
VerticalStackedBarChart
409.919 kB
124.239 kB
410.207 kB
124.306 kB
288 B
67 B
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
237.187 kB
68.891 kB
237.474 kB
68.972 kB
287 B
81 B
react-components
react-components: entire library
1.3 MB
324.941 kB
1.3 MB
325.023 kB
288 B
82 B
react-popover
Popover
134.131 kB
41.552 kB
134.416 kB
41.623 kB
285 B
71 B
react-teaching-popover
TeachingPopover
112.414 kB
34.219 kB
112.699 kB
34.297 kB
285 B
78 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-avatar
Avatar
48.479 kB
15.359 kB
react-avatar
AvatarGroup
17.482 kB
7.002 kB
react-avatar
AvatarGroupItem
61.867 kB
19.384 kB
react-charts
HorizontalBarChartWithAxis
63 B
83 B
react-charts
Legends
242.965 kB
71.798 kB
react-charts
Sparkline
91.4 kB
28.708 kB
react-components
react-components: Button, FluentProvider & webLightTheme
70.397 kB
19.96 kB
react-components
react-components: FluentProvider & webLightTheme
43.612 kB
14.022 kB
react-datepicker-compat
DatePicker Compat
225.627 kB
63.656 kB
react-dialog
Dialog (including children components)
102.117 kB
30.367 kB
react-headless-components-preview
react-headless-components-preview: entire library
64.854 kB
19.302 kB
react-persona
Persona
55.434 kB
17.299 kB
react-portal-compat
PortalCompatProvider
8.386 kB
2.624 kB
react-table
DataGrid
159.783 kB
45.012 kB
react-table
Table (Primitives only)
40.997 kB
13.172 kB
react-table
Table as DataGrid
131.005 kB
36.012 kB
react-table
Table (Selection only)
69.391 kB
19.404 kB
react-table
Table (Sort only)
68.034 kB
19.022 kB
react-tag-picker
@fluentui/react-tag-picker - package
187.178 kB
55.977 kB
react-tags
InteractionTag
13.724 kB
5.47 kB
react-tags
Tag
29.648 kB
9.429 kB
react-tags
TagGroup
82.247 kB
24.152 kB
react-timepicker-compat
TimePicker
108.977 kB
36.038 kB
react-tree
FlatTree
148.099 kB
42.211 kB
react-tree
PersonaFlatTree
149.927 kB
42.585 kB
react-tree
PersonaTree
145.987 kB
41.411 kB
react-tree
Tree
144.165 kB
41.037 kB
🤖 This report was generated against 3d2d9bf0e07e05f2320e5400efe6c01759426534

@github-actions
Copy link
Copy Markdown

Pull request demo site: URL

Copy link
Copy Markdown
Contributor

@bsunderhus bsunderhus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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.

Won't this be better as an e2e test?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants