diff --git a/src/Menu.tsx b/src/Menu.tsx index d582eb86..fc112af7 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -30,6 +30,7 @@ import MenuItem from './MenuItem'; import SubMenu, { SemanticName } from './SubMenu'; import { parseItems } from './utils/nodeUtil'; import { warnItemProp } from './utils/warnUtil'; +import { getFocusTarget } from './utils/commonUtil'; /** * Menu modify after refactor: @@ -406,7 +407,8 @@ const Menu = React.forwardRef((props, ref) => { const elementToFocus = key2element.get(shouldFocusKey); if (shouldFocusKey && elementToFocus) { - elementToFocus?.focus?.(options); + const focusTargetElement = getFocusTarget(elementToFocus); + focusTargetElement?.focus?.(options); } }, findItem: ({ key: itemKey }) => { diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index cda3d0c4..bdb02321 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -201,6 +201,13 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< const onInternalFocus: React.FocusEventHandler = e => { onActive(eventKey); onFocus?.(e); + + if (e.target === elementRef.current) { + const link = elementRef.current?.querySelector('a[href]'); + if (link) { + link.focus(); + } + } }; // ============================ Render ============================ diff --git a/src/hooks/useAccessibility.ts b/src/hooks/useAccessibility.ts index 259130cb..e1d6cdba 100644 --- a/src/hooks/useAccessibility.ts +++ b/src/hooks/useAccessibility.ts @@ -2,6 +2,7 @@ import { KeyCode, getFocusNodeList, raf } from '@rc-component/util'; import * as React from 'react'; import { getMenuId } from '../context/IdContext'; import type { MenuMode } from '../interface'; +import { getFocusTarget } from '../utils/commonUtil'; // destruct to reduce minify size const { LEFT, RIGHT, UP, DOWN, ENTER, ESC, HOME, END } = KeyCode; @@ -221,13 +222,7 @@ export function useAccessibility( const tryFocus = (menuElement: HTMLElement) => { if (menuElement) { - let focusTargetElement = menuElement; - - // Focus to link instead of menu item if possible - const link = menuElement.querySelector('a'); - if (link?.getAttribute('href')) { - focusTargetElement = link; - } + const focusTargetElement = getFocusTarget(menuElement); const targetKey = element2key.get(menuElement); triggerActiveKey(targetKey); @@ -240,7 +235,7 @@ export function useAccessibility( cleanRaf(); rafRef.current = raf(() => { if (activeRef.current === targetKey) { - focusTargetElement.focus(); + focusTargetElement?.focus(); } }); } diff --git a/src/utils/commonUtil.ts b/src/utils/commonUtil.ts index 8054c8c4..5810f4d2 100644 --- a/src/utils/commonUtil.ts +++ b/src/utils/commonUtil.ts @@ -25,3 +25,15 @@ export function parseChildren(children: React.ReactNode | undefined, keyPath: st return child; }); } + +/** + * Find the focus target within a menu element. + * If the menu element contains an anchor with href, focus the anchor instead. + */ +export function getFocusTarget(element?: HTMLElement | null): HTMLElement | null { + if (!element) { + return null; + } + const link = element.querySelector('a[href]'); + return link || element; +} diff --git a/tests/Focus.spec.tsx b/tests/Focus.spec.tsx index 3d90507f..e8515e92 100644 --- a/tests/Focus.spec.tsx +++ b/tests/Focus.spec.tsx @@ -186,5 +186,41 @@ describe('Focus', () => { expect(document.activeElement).toBe(getByTitle('Submenu')); expect(getByTestId('sub-menu')).toHaveClass('rc-menu-submenu-active'); }); + + it('should focus anchor link inside menu item through ref', async () => { + const menuRef = React.createRef(); + const { container } = await act(async () => + render( + + + Light + + , + ), + ); + + act(() => menuRef.current.focus()); + + const anchor = container.querySelector('a'); + expect(document.activeElement).toBe(anchor); + expect(container.querySelector('.rc-menu-item')).toHaveClass('rc-menu-item-active'); + }); + + it('should delegate focus to anchor link when menu item li is focused', async () => { + const { container } = await act(async () => + render( + + + Light + + , + ), + ); + + fireEvent.focus(container.querySelector('.rc-menu-item')); + + const anchor = container.querySelector('a'); + expect(document.activeElement).toBe(anchor); + }); }); /* eslint-enable */