diff --git a/change/@fluentui-react-menu-918e66fd-6898-4f31-be79-1d333bff7781.json b/change/@fluentui-react-menu-918e66fd-6898-4f31-be79-1d333bff7781.json new file mode 100644 index 0000000000000..e36e50331d4dc --- /dev/null +++ b/change/@fluentui-react-menu-918e66fd-6898-4f31-be79-1d333bff7781.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: Menu should not steal focus on close", + "packageName": "@fluentui/react-menu", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-menu/src/components/Menu/Menu.cy.tsx b/packages/react-components/react-menu/src/components/Menu/Menu.cy.tsx index aea5ce4afc27f..6aec751ad2b68 100644 --- a/packages/react-components/react-menu/src/components/Menu/Menu.cy.tsx +++ b/packages/react-components/react-menu/src/components/Menu/Menu.cy.tsx @@ -391,6 +391,27 @@ describe('MenuItemRadio', () => { }); describe('Menu', () => { + it('should not focus trigger on dismiss if another elemnt is focused', () => { + mount( + <> + + + + + + + Item + + + + + , + ); + cy.get(menuTriggerSelector).click().get(menuSelector).should('exist').get('input').realClick(); + + cy.get('input').should('be.focused'); + }); + it('should be dismissed with Escape', () => { mount( diff --git a/packages/react-components/react-menu/src/components/Menu/useMenu.tsx b/packages/react-components/react-menu/src/components/Menu/useMenu.tsx index 932254e218b5f..5030194e69e75 100644 --- a/packages/react-components/react-menu/src/components/Menu/useMenu.tsx +++ b/packages/react-components/react-menu/src/components/Menu/useMenu.tsx @@ -11,6 +11,7 @@ import { useOnClickOutside, useEventCallback, useOnScrollOutside, + useFirstMount, } from '@fluentui/react-utilities'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { elementContains } from '@fluentui/react-portal'; @@ -176,7 +177,6 @@ const useMenuOpenState = ( const parentSetOpen = useMenuContext_unstable(context => context.setOpen); const onOpenChange: MenuProps['onOpenChange'] = useEventCallback((e, data) => state.onOpenChange?.(e, data)); - const shouldHandleCloseRef = React.useRef(false); const setOpenTimeout = React.useRef(0); const enteringTriggerRef = React.useRef(false); @@ -195,7 +195,6 @@ const useMenuOpenState = ( if (!data.open) { state.setContextTarget(undefined); - shouldHandleCloseRef.current = true; } if (data.bubble) { @@ -276,23 +275,24 @@ const useMenuOpenState = ( firstFocusable?.focus(); }, [findFirstFocusable, state.menuPopoverRef]); + const firstMount = useFirstMount(); React.useEffect(() => { if (open) { focusFirst(); } else { - if (shouldHandleCloseRef.current) { - // We know that React effects are sync so we focus the trigger here - // after any event handler (event handlers will update state and re-render). - // Since the browser only performs the default behaviour for the Tab key once - // keyboard events have fully bubbled up the window, the browser will move - // focus to the next tabbable element before/after the trigger if needed. - // If the Tab key was not pressed, focus will remain on the trigger as expected. - state.triggerRef.current?.focus(); + if (!firstMount) { + if (targetDocument?.activeElement === targetDocument?.body) { + // We know that React effects are sync so we focus the trigger here + // after any event handler (event handlers will update state and re-render). + // Since the browser only performs the default behaviour for the Tab key once + // keyboard events have fully bubbled up the window, the browser will move + // focus to the next tabbable element before/after the trigger if needed. + // If the Tab key was not pressed, focus will remain on the trigger as expected. + state.triggerRef.current?.focus(); + } } } - - shouldHandleCloseRef.current = false; - }, [state.triggerRef, state.isSubmenu, open, focusFirst]); + }, [state.triggerRef, state.isSubmenu, open, focusFirst, firstMount, targetDocument, state.menuPopoverRef]); return [open, setOpen] as const; };