Skip to content

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

Open
petdud wants to merge 7 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 7 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.

  • Enabled by default for all trapFocus popovers
  • Internal closeOnFocusOutside prop (not on public API) allows consumers to opt out during gradual rollout: {...({ closeOnFocusOutside: false } as unknown as PopoverProps)}
  • 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 public props

Test Coverage

  • Unit tests (6 new): close on focus outside, no close without trapFocus, no close when not open, close with inertTrapFocus, no close with closeOnFocusOutside: false, no close when focus moves to trigger
  • E2E tests (5 new): same scenarios in Cypress component tests, plus trigger focus and opt-out verification

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

github-actions Bot commented Apr 17, 2026

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-charts
AreaChart
412.997 kB
126.642 kB
413.437 kB
126.767 kB
440 B
125 B
react-charts
DeclarativeChart
763.759 kB
220.641 kB
764.199 kB
220.767 kB
440 B
126 B
react-charts
DonutChart
323.416 kB
97.131 kB
323.856 kB
97.334 kB
440 B
203 B
react-charts
FunnelChart
314.963 kB
94.145 kB
315.403 kB
94.346 kB
440 B
201 B
react-charts
GanttChart
396.105 kB
120.154 kB
396.544 kB
120.339 kB
439 B
185 B
react-charts
GaugeChart
322.849 kB
96.554 kB
323.289 kB
96.763 kB
440 B
209 B
react-charts
GroupedVerticalBarChart
403.98 kB
122.722 kB
404.42 kB
122.912 kB
440 B
190 B
react-charts
HeatMapChart
398.177 kB
122.047 kB
398.617 kB
122.173 kB
440 B
126 B
react-charts
HorizontalBarChart
303.146 kB
89.291 kB
303.586 kB
89.414 kB
440 B
123 B
react-charts
LineChart
424.337 kB
128.724 kB
424.777 kB
128.912 kB
440 B
188 B
react-charts
PolarChart
351.985 kB
107.563 kB
352.425 kB
107.713 kB
440 B
150 B
react-charts
SankeyChart
220.864 kB
67.971 kB
221.304 kB
68.131 kB
440 B
160 B
react-charts
ScatterChart
403.719 kB
122.848 kB
404.159 kB
123.035 kB
440 B
187 B
react-charts
VerticalBarChart
440.449 kB
128.312 kB
440.889 kB
128.437 kB
440 B
125 B
react-charts
VerticalStackedBarChart
409.991 kB
124.241 kB
410.431 kB
124.364 kB
440 B
123 B
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
237.29 kB
68.851 kB
237.729 kB
68.993 kB
439 B
142 B
react-components
react-components: entire library
1.302 MB
325.338 kB
1.302 MB
325.473 kB
440 B
135 B
react-popover
Popover
134.15 kB
41.538 kB
134.587 kB
41.657 kB
437 B
119 B
react-teaching-popover
TeachingPopover
112.714 kB
34.302 kB
113.151 kB
34.428 kB
437 B
126 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-avatar
Avatar
48.492 kB
15.379 kB
react-avatar
AvatarGroup
17.468 kB
6.999 kB
react-avatar
AvatarGroupItem
61.513 kB
19.251 kB
react-charts
HorizontalBarChartWithAxis
63 B
83 B
react-charts
Legends
242.887 kB
71.756 kB
react-charts
Sparkline
91.4 kB
28.708 kB
react-components
react-components: Button, FluentProvider & webLightTheme
70.415 kB
19.963 kB
react-components
react-components: FluentProvider & webLightTheme
43.63 kB
14.026 kB
react-datepicker-compat
DatePicker Compat
225.645 kB
63.659 kB
react-dialog
Dialog (including children components)
101.984 kB
30.317 kB
react-headless-components-preview
react-headless-components-preview: entire library
84.122 kB
24.635 kB
react-persona
Persona
55.447 kB
17.311 kB
react-portal-compat
PortalCompatProvider
8.386 kB
2.624 kB
react-table
DataGrid
159.681 kB
44.965 kB
react-table
Table (Primitives only)
41.015 kB
13.174 kB
react-table
Table as DataGrid
131.023 kB
36.014 kB
react-table
Table (Selection only)
69.409 kB
19.408 kB
react-table
Table (Sort only)
68.052 kB
19.026 kB
react-tag-picker
@fluentui/react-tag-picker - package
187.014 kB
55.911 kB
react-tags
InteractionTag
13.742 kB
5.473 kB
react-tags
Tag
29.666 kB
9.433 kB
react-tags
TagGroup
82.265 kB
24.156 kB
react-timepicker-compat
TimePicker
109.674 kB
36.193 kB
react-tree
FlatTree
148.056 kB
42.19 kB
react-tree
PersonaFlatTree
149.884 kB
42.567 kB
react-tree
PersonaTree
145.944 kB
41.374 kB
react-tree
Tree
144.122 kB
41.003 kB
🤖 This report was generated against 0392152eacf970d6d7063c12af84b20b7d45c8e1

@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

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.

3 participants