From 961baf924f9687f70260cb1c6a0255dbbb9eb65a 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: Wed, 2 Sep 2026 13:59:35 +0800 Subject: [PATCH 1/3] fix: focus wrapped dropdown menus --- src/hooks/useAccessibility.ts | 20 +++++++++++++++----- tests/basic.test.tsx | 9 ++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/hooks/useAccessibility.ts b/src/hooks/useAccessibility.ts index a946e39..84b562c 100644 --- a/src/hooks/useAccessibility.ts +++ b/src/hooks/useAccessibility.ts @@ -28,12 +28,22 @@ export default function useAccessibility({ }; const focusMenu = (options?: FocusOptions) => { - if (overlayRef.current?.focus) { - overlayRef.current.focus(options); - focusMenuRef.current = true; - return true; + const overlay = overlayRef.current; + if (!overlay?.focus) { + return false; } - return false; + + const activeElement = document.activeElement; + overlay.focus(options); + if (document.activeElement === activeElement) { + const focusTarget = (overlay.querySelector?.('[role="menu"]') ?? + overlay.querySelector?.('[tabindex]')) as HTMLElement | null; + focusTarget?.focus(options); + } + + const focused = document.activeElement !== activeElement; + focusMenuRef.current = focused; + return focused; }; const handleKeyDown = (event) => { diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx index ed7e5cf..937533f 100644 --- a/tests/basic.test.tsx +++ b/tests/basic.test.tsx @@ -492,9 +492,16 @@ describe('dropdown', () => { // Focus menu with Tab window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab + expect(document.activeElement).toHaveClass('rc-menu'); + fireEvent.keyDown(document.activeElement, { + key: 'ArrowDown', + keyCode: 40, + }); + await sleep(50); + expect(document.activeElement).toHaveTextContent('one'); // Close menu with Tab - window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab + fireEvent.keyDown(document.activeElement, { key: 'Tab', keyCode: 9 }); await sleep(200); expect(document.activeElement.className).toContain('my-button'); }); From a3263300d3ac45f7eaffcf16eaf3c5e4e9c2073d 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: Wed, 2 Sep 2026 15:36:29 +0800 Subject: [PATCH 2/3] fix: preserve optional overlay refs --- src/hooks/useAccessibility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useAccessibility.ts b/src/hooks/useAccessibility.ts index 84b562c..0f6d26f 100644 --- a/src/hooks/useAccessibility.ts +++ b/src/hooks/useAccessibility.ts @@ -28,7 +28,7 @@ export default function useAccessibility({ }; const focusMenu = (options?: FocusOptions) => { - const overlay = overlayRef.current; + const overlay = overlayRef?.current; if (!overlay?.focus) { return false; } From e6c87ed9e7bdedc9eb27a098a203c7cf278b752f 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: Thu, 17 Sep 2026 16:19:39 +0800 Subject: [PATCH 3/3] test: preserve wrapped menu autoFocus scroll options --- tests/basic.test.tsx | 93 ++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx index 937533f..a5987d0 100644 --- a/tests/basic.test.tsx +++ b/tests/basic.test.tsx @@ -591,50 +591,57 @@ describe('dropdown', () => { jest.useRealTimers(); }); - it('should support autoFocus', async () => { - jest.useFakeTimers(); - const focusSpy = jest.spyOn(HTMLElement.prototype, 'focus'); + it.each(['direct', 'wrapped'])( + 'should support autoFocus for a %s menu', + async (mode) => { + jest.useFakeTimers(); + const focusSpy = jest.spyOn(HTMLElement.prototype, 'focus'); + + try { + const overlay = ( + + + one + + two + + ); + const { container } = render( + {overlay} : overlay} + > + + , + ); + const trigger = container.querySelector('.my-button'); - try { - const overlay = ( - - - one - - two - - ); - const { container } = render( - - - , - ); - const trigger = container.querySelector('.my-button'); - - // Open menu - fireEvent.click(trigger); - - await waitForTime(); - - expect( - container - .querySelector('.rc-dropdown') - .classList.contains('rc-dropdown-hidden'), - ).toBeFalsy(); - expect(document.activeElement.className).toContain('menu'); - expect(focusSpy).toHaveBeenCalledWith({ preventScroll: true }); - - // Close menu with Tab - window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab - - await waitForTime(); - - expect(document.activeElement.className).toContain('my-button'); - } finally { - focusSpy.mockRestore(); - jest.useRealTimers(); - } - }); + // Open menu + fireEvent.click(trigger); + + await waitForTime(); + + expect( + container + .querySelector('.rc-dropdown') + .classList.contains('rc-dropdown-hidden'), + ).toBeFalsy(); + expect(document.activeElement.className).toContain('menu'); + expect(focusSpy).toHaveBeenLastCalledWith({ preventScroll: true }); + + // Close menu with Tab + window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 9 })); // Tab + + await waitForTime(); + + expect(document.activeElement.className).toContain('my-button'); + } finally { + focusSpy.mockRestore(); + jest.useRealTimers(); + } + }, + ); it('children cannot be given ref should not throw', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});