Skip to content

feat(react-menu): add opt-in autofocus suppression - #36864

Open
Paul Mardling (PaulGMardling) wants to merge 13 commits into
microsoft:masterfrom
PaulGMardling:paulgmardling-menu-composition-apis
Open

Paul Mardling (PaulGMardling) wants to merge 13 commits into
microsoft:masterfrom
PaulGMardling:paulgmardling-menu-composition-apis

Conversation

@PaulGMardling

@PaulGMardling Paul Mardling (PaulGMardling) commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Previous Behavior

The WXP Ribbon Search/TellMe migration from v8 ContextualMenu needs command results to open without taking focus from its editable input. Users must be able to continue typing and selecting text until the Ribbon explicitly moves focus into the results.

V9 Menu automatically focuses its first item when opened, so the WXP migration currently needs a dependency patch to suppress that behavior. Headless Menu also supplies automatic opening focus through its native popover implementation.

New Behavior

Adds one opt-in experimental Menu prop, unstable_disableAutoFocus, matching the capability needed by the WXP patch. It defaults to false; when enabled, opening the menu does not automatically focus its first item. The same prop works in both styled v9 Menu and headless Menu, with actual native-popover focus handling rather than type-only support. Nested menus retain their own defaults.

This PR is limited to autofocus suppression, not external-trigger registration or a complete editable-search widget. A focused styled API example explicitly implements keyboard entry. Browser regressions cover continued typing and text selection while externally controlled results open in both implementations.

Existing opening-focus defaults and close-focus restoration are preserved. Equivalent omitted/false values do not reset the focused item, and changing the flag while closed does not move focus. Turning suppression off while open focuses the first item. Headless Menu cleans up its own autofocus attribute without removing consumer-authored attributes.

The expanded keyboard coverage reproduced a forward-Tab failure on React 17/Electron 118 with both default and suppressed autofocus: the native surface remained open through the browser's default Tab action, and focus ended on the document body. Accepted headless closes now hide the surface during layout, before native Tab navigation. Opening focus remains in its existing effect; controlled owners can still decline closing. Regressions cover root/nested Tab and Shift+Tab, controlled close refusal, and duplicate close notifications.

Menu remains appropriate for the Ribbon's executable command hierarchy; this is not a drop-in v8 ContextualMenu replacement. Keyboard entry, asynchronous results, host-specific restoration, and actual Ribbon/screen-reader validation remain consumer responsibilities. Shared interaction behavior is available to headless consumers without promising identical styled/design APIs or verified Fluent Modern integration.

Validation:

  • yarn nx run react-menu:test --runInBand — 494 passed; all 16 snapshots passed.
  • yarn nx run react-headless-components-preview:test --runInBand --runTestsByPath src/components/Menu/Menu.test.tsx --skipNxCache — 23 passed.
  • yarn nx run react-menu:e2e --spec src/components/Menu/Menu.cy.tsx --config retries=0 --browser chrome --skipNxCache — 83 passed.
  • yarn nx run react-headless-components-preview:e2e --spec src/components/Menu/Menu.cy.tsx --config retries=0 --browser chrome --skipNxCache — 30 passed.
  • yarn nx run react-headless-components-preview:test-rit--17--e2e --skipNxCache — all 13 specs passed on React 17/Electron 118: 258 passed, 9 pending; all 30 Menu tests passed.

Related Issue(s)

  • Fixes # — N/A; no linked GitHub issue.
  • office/1JS#5780138 — WXP Ribbon migration; the autofocus patch this PR is intended to replace.
  • office/1JS#5828578 — earlier composition prototype; background context, not the scope of this change.

Add experimental opt-in Menu props for opening autofocus suppression and external trigger registration. Preserve trigger ownership across lifecycle transitions and restore focus only on close. Include regression coverage, editable-trigger guidance, API report, and minor release metadata.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-charts
AreaChart
403.243 kB
125.989 kB
403.474 kB
126.088 kB
231 B
99 B
react-charts
DeclarativeChart
757.029 kB
222.016 kB
757.254 kB
222.101 kB
225 B
85 B
react-charts
DonutChart
313.568 kB
96.743 kB
313.793 kB
96.819 kB
225 B
76 B
react-charts
FunnelChart
304.983 kB
93.455 kB
305.208 kB
93.531 kB
225 B
76 B
react-charts
GanttChart
386.151 kB
120.413 kB
386.376 kB
120.478 kB
225 B
65 B
react-charts
GaugeChart
313.535 kB
96.295 kB
313.76 kB
96.36 kB
225 B
65 B
react-charts
GroupedVerticalBarChart
394.478 kB
123.274 kB
394.703 kB
123.349 kB
225 B
75 B
react-charts
HeatMapChart
388.351 kB
121.402 kB
388.582 kB
121.488 kB
231 B
86 B
react-charts
HorizontalBarChart
293.252 kB
89.12 kB
293.483 kB
89.205 kB
231 B
85 B
react-charts
Legends
232.643 kB
69.738 kB
232.874 kB
69.801 kB
231 B
63 B
react-charts
LineChart
414.525 kB
129.128 kB
414.75 kB
129.187 kB
225 B
59 B
react-charts
PolarChart
341.42 kB
106.647 kB
341.645 kB
106.706 kB
225 B
59 B
react-charts
ScatterChart
393.719 kB
123.111 kB
393.944 kB
123.168 kB
225 B
57 B
react-charts
VerticalBarChart
430.679 kB
128.132 kB
430.91 kB
128.222 kB
231 B
90 B
react-charts
VerticalStackedBarChart
400.254 kB
124.616 kB
400.479 kB
124.676 kB
225 B
60 B
react-components
react-components: all base hooks
219.371 kB
68.538 kB
219.564 kB
68.601 kB
193 B
63 B
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
223.356 kB
67.405 kB
223.587 kB
67.474 kB
231 B
69 B
react-components
react-components: entire library
1.282 MB
322.152 kB
1.283 MB
322.249 kB
236 B
97 B
react-headless-components-preview
react-headless-components-preview: entire library
224.967 kB
64.979 kB
225.397 kB
65.1 kB
430 B
121 B
react-menu
Menu (including children components)
156.759 kB
49.835 kB
156.99 kB
49.89 kB
231 B
55 B
react-menu
Menu (including selectable components)
159.924 kB
50.475 kB
160.155 kB
50.526 kB
231 B
51 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-avatar
Avatar
49.178 kB
15.656 kB
react-avatar
AvatarGroup
14.126 kB
5.693 kB
react-avatar
AvatarGroupItem
58.655 kB
18.173 kB
react-breadcrumb
@fluentui/react-breadcrumb - package
103.23 kB
29.068 kB
react-charts
HorizontalBarChartWithAxis
63 B
83 B
react-charts
SankeyChart
209.167 kB
67.254 kB
react-charts
Sparkline
79.8 kB
26.591 kB
react-components
react-components: Button, FluentProvider & webLightTheme
68.229 kB
19.872 kB
react-components
react-components: FluentProvider & webLightTheme
41.459 kB
13.908 kB
react-datepicker-compat
DatePicker Compat
216.697 kB
64.458 kB
react-dialog
Dialog (including children components)
86.881 kB
26.936 kB
react-headless-components-preview
@fluentui/react-headless-components-preview/tag-picker
50.594 kB
16.406 kB
react-headless-components-preview
@fluentui/react-headless-components-preview/teaching-popover
32.647 kB
10.67 kB
react-overflow
hooks only
10.203 kB
3.694 kB
react-persona
Persona
56.131 kB
17.602 kB
react-popover
Popover
122.126 kB
39.376 kB
react-portal-compat
PortalCompatProvider
5.202 kB
2.106 kB
react-table
DataGrid
143.692 kB
42.585 kB
react-table
Table (Primitives only)
38.955 kB
13.033 kB
react-table
Table as DataGrid
119.198 kB
33.788 kB
react-table
Table (Selection only)
67.461 kB
19.3 kB
react-table
Table (Sort only)
66.124 kB
18.925 kB
react-tag-picker
@fluentui/react-tag-picker - package
171.186 kB
53.362 kB
react-tags
InteractionTag
13.986 kB
5.529 kB
react-tags
Tag
30.614 kB
9.763 kB
react-tags
TagGroup
69.63 kB
21.544 kB
react-teaching-popover
TeachingPopover
97.09 kB
30.614 kB
react-timepicker-compat
TimePicker
139.8 kB
45.595 kB
react-tree
FlatTree
132.727 kB
39.465 kB
react-tree
PersonaFlatTree
134.584 kB
39.965 kB
react-tree
PersonaTree
130.671 kB
38.775 kB
react-tree
Tree
128.814 kB
38.289 kB
🤖 This report was generated against 84abdb5fd25a10c627a08465ae82a96e17927270

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Pull request demo site: URL

Copilot AI 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.

🟡 Changes recommended

The autofocus flag can refocus an open menu when its effective value has not changed, and the public child-shape documentation is inconsistent.

2 open findings
What changed in this PR

Adds experimental Menu support for external editable triggers and optional autofocus suppression.

Changes:

  • Adds external trigger registration and autofocus control.
  • Adds lifecycle, focus, navigation, and containment tests.
  • Adds documentation, API report, story, and change file.
File Description
MenuExternalEditableTrigger.stories.tsx Demonstrates editable external-trigger composition.
MenuBestPractices.md Documents experimental usage guidance.
index.stories.tsx Exports the new story.
useMenu.tsx Implements trigger registration and autofocus suppression.
useMenu.test.tsx Tests external registration lifecycle.
Menu.types.ts Adds the experimental public props.
Menu.test.tsx Tests composition transitions and Strict Mode.
Menu.cy.tsx Covers focus, navigation, dismissal, and positioning.
react-menu.api.md Updates the generated API report.
Change file Records a minor react-menu feature release.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/react-components/react-menu/library/src/components/Menu/useMenu.tsx Outdated
Comment thread packages/react-components/react-menu/library/src/components/Menu/Menu.types.ts Outdated
Normalize omitted autofocus suppression to false, cover transitions in both directions, and document external trigger single-child composition.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16
Demonstrate query-bound asynchronous filtering, explicit focus entry, supported textbox ARIA, and native menu Tab behavior. Keep unsupported composition props out of the separate headless Menu contract.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16
Place the persistent live status above the editable trigger so command results do not cover its text. Cover the visible status bounds with a native browser regression.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16

Copilot AI 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.

🔵 Needs a closer look

The experimental focus-management APIs and nonstandard editable-trigger composition need final human API and accessibility review.

0 open findings

2 resolved since last review

🧠 Review effort: Balanced

Copilot AI 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.

🔵 Needs a closer look

The experimental focus and accessibility composition needs maintainer review, and popup status visibility remains unresolved.

1 open finding

🧠 Review effort: Balanced

Remove external trigger registration and its editable-search composition. Retain the single experimental autofocus flag needed by the WXP patch, with focused examples, regression coverage, and the independent headless API boundary.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16
@PaulGMardling Paul Mardling (PaulGMardling) changed the title feat(react-menu): support external triggers and autofocus suppression feat(react-menu): add opt-in autofocus suppression Oct 15, 2026
Assert that the styled Menu autofocus prop is absent from the headless component's public props without constructing invalid JSX or suppressing a TypeScript diagnostic.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16
Keep the explicit Menu prop exclusion and generated API contract without wrapping a compile-time key assertion in a tautological runtime test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16
…ession

Share the normalized autofocus policy through Menu state and context, and honor it in native popover focus handling. Preserve default opening focus, caller-authored attributes, and nested-menu behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16
…navigation

Synchronize accepted native Menu closure in a layout effect so React 17/Electron 118 does not lose focus to body before the popover hides. Keep opening autofocus in its existing passive effect.

Cover root and nested Tab/Shift+Tab with default and suppressed autofocus, controlled close refusal, and duplicate close notifications.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3f91ed3f-5fa6-4307-b505-a6ac353feb16
@PaulGMardling
Paul Mardling (PaulGMardling) marked this pull request as ready for review October 16, 2026 08:36

This branch has not been deployed

No deployments
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