diff --git a/src/hooks/useKeyRecords.ts b/src/hooks/useKeyRecords.ts index d9af87d7..638b7919 100644 --- a/src/hooks/useKeyRecords.ts +++ b/src/hooks/useKeyRecords.ts @@ -11,7 +11,7 @@ const getPathKeys = (keyPathStr: string) => keyPathStr.split(PATH_SPLIT); export const OVERFLOW_KEY = 'rc-menu-more'; export default function useKeyRecords() { - const [, internalForceUpdate] = React.useState({}); + const [keyPathVersion, internalForceUpdate] = React.useState({}); const key2pathRef = useRef(new Map()); const path2keyRef = useRef(new Map()); const [overflowKeys, setOverflowKeys] = React.useState([]); @@ -80,7 +80,9 @@ export default function useKeyRecords() { const pathKeyList = getKeyPath(pathKey, true); return pathKeyList.includes(eventKey); }), - [getKeyPath], + // Path records live in refs, so updates must also refresh context consumers. + // eslint-disable-next-line react-hooks/exhaustive-deps + [getKeyPath, keyPathVersion], ); const getKeys = () => { const keys = [...key2pathRef.current.keys()]; @@ -107,12 +109,12 @@ export default function useKeyRecords() { return pathKeys; }, []); - React.useEffect( - () => () => { + React.useEffect(() => { + destroyRef.current = false; + return () => { destroyRef.current = true; - }, - [], - ); + }; + }, []); return { // Register diff --git a/tests/DynamicSelection.spec.tsx b/tests/DynamicSelection.spec.tsx new file mode 100644 index 00000000..70492657 --- /dev/null +++ b/tests/DynamicSelection.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; +import Menu from '../src'; +import type { MenuProps } from '../src'; + +describe.each([false, true])('dynamic selection with StrictMode=%s', strictMode => { + const wrapper = strictMode ? React.StrictMode : React.Fragment; + + describe.each(['defaultSelectedKeys', 'selectedKeys'] as const)('%s', selectionProp => { + it.each(['inline', 'vertical', 'horizontal'])( + 'highlights the parent and child when items load in %s mode', + async mode => { + const selectionProps = { [selectionProp]: ['child'] }; + const { container, getAllByText, rerender } = render( + , + { wrapper }, + ); + + await act(async () => { + rerender( + , + ); + }); + + expect(container.querySelector('.rc-menu-submenu')).toHaveClass('rc-menu-submenu-selected'); + getAllByText('Child').forEach(child => { + expect(child.closest('.rc-menu-item')).toHaveClass('rc-menu-item-selected'); + }); + }, + ); + }); +});