diff --git a/packages/@react-aria/menu/src/useSubmenuTrigger.ts b/packages/@react-aria/menu/src/useSubmenuTrigger.ts index 4d9c5684305..590ce43a213 100644 --- a/packages/@react-aria/menu/src/useSubmenuTrigger.ts +++ b/packages/@react-aria/menu/src/useSubmenuTrigger.ts @@ -14,7 +14,7 @@ import {AriaMenuItemProps} from './useMenuItem'; import {AriaMenuOptions} from './useMenu'; import type {AriaPopoverProps, OverlayProps} from '@react-aria/overlays'; import {FocusableElement, FocusStrategy, KeyboardEvent, Node, PressEvent, RefObject} from '@react-types/shared'; -import {focusWithoutScrolling, useEffectEvent, useId, useLayoutEffect} from '@react-aria/utils'; +import {focusWithoutScrolling, useEffectEvent, useEvent, useId, useLayoutEffect} from '@react-aria/utils'; import type {SubmenuTriggerState} from '@react-stately/menu'; import {useCallback, useRef} from 'react'; import {useLocale} from '@react-aria/i18n'; @@ -223,11 +223,13 @@ export function useSubmenuTrigger(props: AriaSubmenuTriggerProps, state: Subm } }; - let onBlur = (e) => { - if (state.isOpen && (parentMenuRef.current?.contains(e.relatedTarget))) { + useEvent(parentMenuRef, 'focusin', (e) => { + // If we detect focus moved to a different item in the same menu that the currently open submenu trigger is in + // then close the submenu. This is for a case where the user hovers a root menu item when multiple submenus are open + if (state.isOpen && (parentMenuRef.current?.contains(e.target as HTMLElement) && e.target !== ref.current)) { onSubmenuClose(); } - }; + }); let shouldCloseOnInteractOutside = (target) => { if (target !== ref.current) { @@ -249,7 +251,6 @@ export function useSubmenuTrigger(props: AriaSubmenuTriggerProps, state: Subm onPress, onHoverChange, onKeyDown: submenuTriggerKeyDown, - onBlur, isOpen: state.isOpen }, submenuProps, diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index cda7ba63bd1..d180bb19d51 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -131,10 +131,12 @@ export const SubmenuTrigger = /*#__PURE__*/ createBranchComponent('submenutrigg { - if (isDialog && ref.current && !ref.current.contains(document.activeElement)) { + if (isDialog && props.trigger !== 'SubmenuTrigger' && ref.current && !ref.current.contains(document.activeElement)) { focusSafely(ref.current); } - }, [isDialog, ref]); + }, [isDialog, ref, props.trigger]); let children = useMemo(() => { let children = renderProps.children; diff --git a/packages/react-aria-components/test/AriaMenu.test-util.tsx b/packages/react-aria-components/test/AriaMenu.test-util.tsx index d3b70bc9de9..087821d8941 100644 --- a/packages/react-aria-components/test/AriaMenu.test-util.tsx +++ b/packages/react-aria-components/test/AriaMenu.test-util.tsx @@ -693,6 +693,43 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo expect(nestedSubmenu).not.toBeInTheDocument(); expect(document.activeElement).toBe(nestedSubmenuTrigger); }); + + it('should close the submenu if another item in the same menu is focused', async () => { + let tree = (renderers.submenus!)(); + let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container}); + await menuTester.open(); + let menu = menuTester.menu; + let submenuTrigger = menuTester.submenuTriggers[0]; + let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!; + act(() => {jest.runAllTimers();}); + let submenu = submenuUtil.menu; + expect(submenu).toBeInTheDocument(); + let nestedSubmenuTrigger = submenuUtil.submenuTriggers[0]; + let nestedSubmenuUtil = (await submenuUtil.openSubmenu({submenuTrigger: nestedSubmenuTrigger}))!; + act(() => {jest.runAllTimers();}); + let nestedSubmenu = nestedSubmenuUtil.menu; + expect(submenu).toBeInTheDocument(); + await user.hover(menuTester.options()[0]); + act(() => {jest.runAllTimers();}); + expect(nestedSubmenu).not.toBeInTheDocument(); + expect(submenu).not.toBeInTheDocument(); + expect(menu).toBeInTheDocument(); + }); + + it('should retain focus on the submenu trigger when hovering it', async () => { + let tree = (renderers.submenus!)(); + let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container}); + await menuTester.open(); + await user.hover(menuTester.submenuTriggers[0]); + act(() => {jest.runAllTimers();}); + expect(menuTester.submenuTriggers[0]).toHaveAttribute('aria-expanded', 'true'); + expect(document.activeElement).toBe(menuTester.submenuTriggers[0]); + + // It should also allow the user to move focus into the submenu via ArrowRight + await user.keyboard('{ArrowRight}'); + let submenu = tree.getAllByRole('menu')[1]; + expect(document.activeElement).toBe(within(submenu).getAllByRole('menuitem')[0]); + }); }); }