diff --git a/change/@fluentui-react-menu-11ee527c-b6cc-48eb-b8dd-b85a8d84ca09.json b/change/@fluentui-react-menu-11ee527c-b6cc-48eb-b8dd-b85a8d84ca09.json new file mode 100644 index 0000000000000..8c0eda457cce6 --- /dev/null +++ b/change/@fluentui-react-menu-11ee527c-b6cc-48eb-b8dd-b85a8d84ca09.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Fix: Left arrow behavior in MenuGrid submenus", + "packageName": "@fluentui/react-menu", + "email": "email not defined", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-menu-grid-preview-785678f1-ead6-4795-bb56-a75f0d153668.json b/change/@fluentui-react-menu-grid-preview-785678f1-ead6-4795-bb56-a75f0d153668.json new file mode 100644 index 0000000000000..6d4c8208da6c4 --- /dev/null +++ b/change/@fluentui-react-menu-grid-preview-785678f1-ead6-4795-bb56-a75f0d153668.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Fix: Left arrow behavior in MenuGrid submenus", + "packageName": "@fluentui/react-menu-grid-preview", + "email": "email not defined", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/MenuGrid.cy.tsx b/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/MenuGrid.cy.tsx index 097503f772d7c..685959ad1f79d 100644 --- a/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/MenuGrid.cy.tsx +++ b/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/MenuGrid.cy.tsx @@ -308,4 +308,12 @@ describe('With MenuList submenus', () => { cy.get(menuSelector).should('have.length', 1); cy.focused().should('have.attr', 'role', 'menuitem'); }); + + it('should not close submenu when pressing ArrowLeft inside it', () => { + mount(); + cy.get(menuGridItemSelector).first().focus().realPress('ArrowRight').realPress('Enter'); + cy.get(menuSelector).should('have.length', 1); + cy.focused().should('have.attr', 'role', 'menuitem').realPress('ArrowLeft'); + cy.get(menuSelector).should('have.length', 1); + }); }); diff --git a/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGrid.test.ts b/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGrid.test.ts index 2100a83032585..711f41d78441a 100644 --- a/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGrid.test.ts +++ b/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGrid.test.ts @@ -22,11 +22,11 @@ describe('useMenuGrid_unstable', () => { beforeEach(() => { rows = [ - { textContent: 'Apple', focus: jest.fn(), hasAttribute: () => true, getAttribute: () => 'row' }, - { textContent: 'Banana', focus: jest.fn(), hasAttribute: () => true, getAttribute: () => 'row' }, - { textContent: 'Cherry', focus: jest.fn(), hasAttribute: () => true, getAttribute: () => 'row' }, - { textContent: 'Apricot', focus: jest.fn(), hasAttribute: () => true, getAttribute: () => 'row' }, - { textContent: 'Date', focus: jest.fn(), hasAttribute: () => true, getAttribute: () => 'row' }, + { textContent: 'Apple', focus: jest.fn(), role: 'row' }, + { textContent: 'Banana', focus: jest.fn(), role: 'row' }, + { textContent: 'Cherry', focus: jest.fn(), role: 'row' }, + { textContent: 'Apricot', focus: jest.fn(), role: 'row' }, + { textContent: 'Date', focus: jest.fn(), role: 'row' }, ]; (useFocusFinders as jest.Mock).mockReturnValue({ @@ -37,7 +37,7 @@ describe('useMenuGrid_unstable', () => { const createEvent = (key: string, target?: Record): React.KeyboardEvent => ({ key, - target: target ?? { hasAttribute: () => true, getAttribute: () => 'row' }, + target: target ?? { role: 'row' }, } as unknown as React.KeyboardEvent); it('should focus the next row matching the pressed character', () => { @@ -97,7 +97,7 @@ describe('useMenuGrid_unstable', () => { it('should not apply first-letter navigation when event target is not a row', () => { const current = rows[0]; // Apple - const nonRowTarget = { hasAttribute: () => true, getAttribute: () => 'gridcell' }; + const nonRowTarget = { role: 'gridcell' }; const { result } = renderHook(() => useMenuGrid_unstable({}, React.createRef())); (result.current.root.ref as React.RefCallback)?.(document.createElement('div')); diff --git a/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGrid.ts b/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGrid.ts index dda14d5a5e0bd..97fd7056fca84 100644 --- a/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGrid.ts +++ b/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGrid.ts @@ -56,14 +56,11 @@ export const useMenuGrid_unstable = (props: MenuGridProps, ref: React.Ref el.hasAttribute('role') && el.getAttribute('role') === 'row', - ); + const rows = findAllFocusable(innerRef.current, (el: HTMLElement) => el.role === 'row'); let startIndex = rows.indexOf(itemEl) + 1; if (startIndex === rows.length) { diff --git a/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGridContextValues.ts b/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGridContextValues.ts index 19d7efea7b0c0..0bcccedfce442 100644 --- a/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGridContextValues.ts +++ b/packages/react-components/react-menu-grid-preview/library/src/components/MenuGrid/useMenuGridContextValues.ts @@ -8,6 +8,7 @@ const menuList = { hasIcons: false, hasCheckmarks: false, shouldOpenOnArrowRight: false, + shouldCloseOnArrowLeft: false, }; export function useMenuGridContextValues_unstable(state: MenuGridState): MenuGridContextValues { diff --git a/packages/react-components/react-menu/library/etc/react-menu.api.md b/packages/react-components/react-menu/library/etc/react-menu.api.md index fb505122c31a3..3cfd08cfe69df 100644 --- a/packages/react-components/react-menu/library/etc/react-menu.api.md +++ b/packages/react-components/react-menu/library/etc/react-menu.api.md @@ -231,6 +231,7 @@ export type MenuListContextValue = Pick void; shouldOpenOnArrowRight?: boolean; + shouldCloseOnArrowLeft?: boolean; }; // @public (undocumented) 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 70819a0ce1772..bd1a12031efc5 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 @@ -8,6 +8,7 @@ import { getIntrinsicElementProps, useEventCallback, useMergedRefs, slot, useTim import * as React from 'react'; import { useMenuContext_unstable } from '../../contexts/menuContext'; +import { useMenuListContext_unstable } from '../../contexts/menuListContext'; import { dispatchMenuEnterEvent, useIsSubmenu } from '../../utils/index'; import { MenuPopoverProps, MenuPopoverState } from './MenuPopover.types'; @@ -31,6 +32,8 @@ export const useMenuPopover_unstable = (props: MenuPopoverProps, ref: React.Ref< const triggerRef = useMenuContext_unstable(context => context.triggerRef); const isSubmenu = useIsSubmenu(); + const shouldCloseOnArrowLeft = useMenuListContext_unstable(ctx => ctx.shouldCloseOnArrowLeft ?? true); + const canDispatchCustomEventRef = React.useRef(true); const restoreFocusSourceAttributes = useRestoreFocusSource(); const [setThrottleTimeout, clearThrottleTimeout] = useTimeout(); @@ -93,7 +96,7 @@ export const useMenuPopover_unstable = (props: MenuPopoverProps, ref: React.Ref< }); rootProps.onKeyDown = useEventCallback((event: React.KeyboardEvent) => { const key = event.key; - if (key === Escape || (isSubmenu && key === CloseArrowKey)) { + if (key === Escape || (isSubmenu && shouldCloseOnArrowLeft && key === CloseArrowKey)) { if (open && popoverRef.current?.contains(event.target as HTMLElement) && !event.isDefaultPrevented()) { setOpen(event, { open: false, keyboard: true, type: 'menuPopoverKeyDown', event }); // stop propagation to avoid conflicting with other elements that listen for `Escape` diff --git a/packages/react-components/react-menu/library/src/contexts/menuListContext.tsx b/packages/react-components/react-menu/library/src/contexts/menuListContext.tsx index 4274fc8d9e61a..8d1e6020fccba 100644 --- a/packages/react-components/react-menu/library/src/contexts/menuListContext.tsx +++ b/packages/react-components/react-menu/library/src/contexts/menuListContext.tsx @@ -44,6 +44,14 @@ export type MenuListContextValue = Pick