From e00e0c2a5b87dd2b7408156bf68eb808cf672b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=B3=E5=BB=B7=E5=AE=89?= <73953029+nrps9909@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:54:56 +0800 Subject: [PATCH] fix: consume Escape when returning from a submenu --- src/hooks/useAccessibility.ts | 5 +++++ tests/Keyboard.spec.tsx | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/hooks/useAccessibility.ts b/src/hooks/useAccessibility.ts index 259130cb..a3b7334b 100644 --- a/src/hooks/useAccessibility.ts +++ b/src/hooks/useAccessibility.ts @@ -302,6 +302,11 @@ export function useAccessibility( const parentMenuElement = key2element.get(parentKey); + // A submenu consumes Escape; the surrounding popup handles root Escape. + if (which === ESC && parentMenuElement) { + e.stopPropagation(); + } + // Focus menu item triggerAccessibilityOpen(parentKey, false); tryFocus(parentMenuElement); diff --git a/tests/Keyboard.spec.tsx b/tests/Keyboard.spec.tsx index e1b89ef4..5cb27720 100644 --- a/tests/Keyboard.spec.tsx +++ b/tests/Keyboard.spec.tsx @@ -44,6 +44,47 @@ describe('Menu.Keyboard', () => { }); } + it.each([1, 2])('consumes submenu Escape at depth %s but lets root Escape bubble', depth => { + const onOpenChange = jest.fn(); + const onWindowKeyDown = jest.fn(); + window.addEventListener('keydown', onWindowKeyDown); + const { container, getByText, unmount } = render( + + + {depth === 2 ? ( + + Child + + ) : ( + Child + )} + + , + ); + for (let i = 0; i < 3; i += 1) { + act(() => jest.runAllTimers()); + } + try { + const child = getByText('Child').closest('[role="menuitem"]'); + act(() => child.focus()); + fireEvent.keyDown(child, { which: KeyCode.ESC, keyCode: KeyCode.ESC }); + act(() => jest.runAllTimers()); + expect(onOpenChange).toHaveBeenLastCalledWith(depth === 2 ? ['sub'] : []); + expect(document.activeElement).toBe( + getByText(depth === 2 ? 'Inner' : 'Submenu').closest('[role="menuitem"]'), + ); + expect(onWindowKeyDown).not.toHaveBeenCalled(); + + const rootItem = getByText('Submenu').closest('[role="menuitem"]'); + act(() => rootItem.focus()); + fireEvent.keyDown(rootItem, { which: KeyCode.ESC, keyCode: KeyCode.ESC }); + expect(onWindowKeyDown).toHaveBeenCalledTimes(1); + } finally { + window.removeEventListener('keydown', onWindowKeyDown); + unmount(); + } + }); + it('no data-menu-id by init', () => { const { container } = render(