diff --git a/change/@fluentui-react-menu-ca4cf597-e985-4c1d-aa1d-c300ae041879.json b/change/@fluentui-react-menu-ca4cf597-e985-4c1d-aa1d-c300ae041879.json new file mode 100644 index 00000000000000..43c62f452008dc --- /dev/null +++ b/change/@fluentui-react-menu-ca4cf597-e985-4c1d-aa1d-c300ae041879.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: Escape in an open Menu does not trigger tabster actions", + "packageName": "@fluentui/react-menu", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-menu/library/src/components/MenuPopover/useMenuPopover.test.tsx b/packages/react-components/react-menu/library/src/components/MenuPopover/useMenuPopover.test.tsx index 178ff21ca3081b..6dcc89c06c7d9e 100644 --- a/packages/react-components/react-menu/library/src/components/MenuPopover/useMenuPopover.test.tsx +++ b/packages/react-components/react-menu/library/src/components/MenuPopover/useMenuPopover.test.tsx @@ -140,12 +140,16 @@ describe('useMenuPopover_unstable', () => { expect((result.current.root as Record)['data-testid']).toBe('popover'); }); - it('spreads restoreFocusSource attributes onto root', () => { + it('spreads restoreFocusSource and ignoreKeydown attributes onto root', () => { const { wrapper } = makeWrapper(); const { result } = renderHook(() => useMenuPopover_unstable({}, null), { wrapper }); - expect((result.current.root as Record)['data-tabster']).toBe('{"restorer":{"type":1}}'); + // tabster is told to ignore Escape so the menu (which closes itself on Escape) + // doesn't also escape a parent groupper/modalizer + expect((result.current.root as Record)['data-tabster']).toBe( + '{"restorer":{"type":1},"focusable":{"ignoreKeydown":{"Escape":true}}}', + ); }); }); diff --git a/packages/react-components/react-menu/library/src/components/MenuPopover/useMenuPopover.ts b/packages/react-components/react-menu/library/src/components/MenuPopover/useMenuPopover.ts index 5fed05c1f79f7c..be8ce713a916e8 100644 --- a/packages/react-components/react-menu/library/src/components/MenuPopover/useMenuPopover.ts +++ b/packages/react-components/react-menu/library/src/components/MenuPopover/useMenuPopover.ts @@ -3,7 +3,11 @@ import { ArrowLeft, Tab, ArrowRight, Escape } from '@fluentui/keyboard-keys'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; import { useMotionForwardedRef } from '@fluentui/react-motion'; -import { useRestoreFocusSource } from '@fluentui/react-tabster'; +import { + useRestoreFocusSource, + useTabsterAttributes, + useMergedTabsterAttributes_unstable, +} from '@fluentui/react-tabster'; import { getIntrinsicElementProps, useEventCallback, useMergedRefs, slot, useTimeout } from '@fluentui/react-utilities'; import * as React from 'react'; @@ -129,13 +133,23 @@ export const useMenuPopoverBase_unstable = (props: MenuPopoverProps, ref: React. */ export const useMenuPopover_unstable = (props: MenuPopoverProps, ref: React.Ref): MenuPopoverState => { const restoreFocusSourceAttributes = useRestoreFocusSource(); + + // Opt the menu's popover out of tabster's Escape handling. The menu closes itself on + // Escape; without this tabster would also act on Escape (e.g. escaping a parent + // groupper/modalizer) and move focus away from the trigger instead of restoring it. + const ignoreEscapeKeyAttribute = useTabsterAttributes({ + focusable: { ignoreKeydown: { Escape: true } }, + }); + + const tabsterAttributes = useMergedTabsterAttributes_unstable(restoreFocusSourceAttributes, ignoreEscapeKeyAttribute); + const motionRef = useMotionForwardedRef(); const baseState = useMenuPopoverBase_unstable(props, ref); return { ...baseState, root: { - ...restoreFocusSourceAttributes, + ...tabsterAttributes, ...baseState.root, ref: useMergedRefs(baseState.root.ref, motionRef) as React.Ref, },