diff --git a/package.json b/package.json index 18e2bc15..cab332cb 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "devDependencies": { "@rc-component/father-plugin": "^1.0.0", "@testing-library/jest-dom": "^6.1.5", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^15.0.7", "@types/jest": "^29.5.2", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", @@ -68,8 +68,8 @@ "less": "^4.1.3", "np": "^9.0.0", "rc-test": "^7.0.14", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "regenerator-runtime": "^0.14.0", "typescript": "^5.1.6" }, diff --git a/src/Menu.tsx b/src/Menu.tsx index aa50e63b..41ecec5f 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -21,6 +21,7 @@ import useMemoCallback from './hooks/useMemoCallback'; import useUUID from './hooks/useUUID'; import type { BuiltinPlacements, + Components, ItemType, MenuClickEventHandler, MenuInfo, @@ -60,6 +61,7 @@ export interface MenuProps prefixCls?: string; rootClassName?: string; items?: ItemType[]; + /** @deprecated Please use `items` instead */ children?: React.ReactNode; @@ -148,6 +150,14 @@ export interface MenuProps disabled: boolean; }, ) => React.ReactElement; + + /** + * @private NEVER! EVER! USE IN PRODUCTION!!! + * This is a hack API for `antd` to fix `findDOMNode` issue. + * Not use it! Not accept any PR try to make it as normal API. + * By zombieJ + */ + _internalComponents?: Components; } interface LegacyMenuProps extends MenuProps { @@ -228,12 +238,20 @@ const Menu = React.forwardRef((props, ref) => { _internalRenderMenuItem, _internalRenderSubMenuItem, + _internalComponents, + ...restProps } = props as LegacyMenuProps; - const childList: React.ReactElement[] = React.useMemo( - () => parseItems(children, items, EMPTY_LIST), - [children, items], + const [childList, measureChildList]: [ + visibleChildList: React.ReactElement[], + measureChildList: React.ReactElement[], + ] = React.useMemo( + () => [ + parseItems(children, items, EMPTY_LIST, _internalComponents), + parseItems(children, items, EMPTY_LIST, {}), + ], + [children, items, _internalComponents], ); const [mounted, setMounted] = React.useState(false); @@ -274,9 +292,8 @@ const Menu = React.forwardRef((props, ref) => { }; // >>>>> Cache & Reset open keys when inlineCollapsed changed - const [inlineCacheOpenKeys, setInlineCacheOpenKeys] = React.useState( - mergedOpenKeys, - ); + const [inlineCacheOpenKeys, setInlineCacheOpenKeys] = + React.useState(mergedOpenKeys); const mountRef = React.useRef(false); @@ -352,9 +369,10 @@ const Menu = React.forwardRef((props, ref) => { [registerPath, unregisterPath], ); - const pathUserContext = React.useMemo(() => ({ isSubPathKey }), [ - isSubPathKey, - ]); + const pathUserContext = React.useMemo( + () => ({ isSubPathKey }), + [isSubPathKey], + ); React.useEffect(() => { refreshOverflowKeys( @@ -653,7 +671,7 @@ const Menu = React.forwardRef((props, ref) => { {/* Measure menu keys. Add `display: none` to avoid some developer miss use the Menu */}
- {childList} + {measureChildList}
diff --git a/src/interface.ts b/src/interface.ts index 108632dc..59419342 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -8,7 +8,7 @@ interface ItemSharedProps { } export interface SubMenuType extends ItemSharedProps { - type?: "submenu"; + type?: 'submenu'; label?: React.ReactNode; @@ -133,3 +133,8 @@ export type MenuRef = { focus: (options?: FocusOptions) => void; list: HTMLUListElement; }; + +// ======================== Component ======================== +export type ComponentType = 'submenu' | 'item' | 'group' | 'divider'; + +export type Components = Partial>>; diff --git a/src/utils/nodeUtil.tsx b/src/utils/nodeUtil.tsx index eac86f49..21079fc5 100644 --- a/src/utils/nodeUtil.tsx +++ b/src/utils/nodeUtil.tsx @@ -1,13 +1,22 @@ import * as React from 'react'; -import type { ItemType } from '../interface'; -import MenuItemGroup from '../MenuItemGroup'; -import SubMenu from '../SubMenu'; import Divider from '../Divider'; +import type { Components, ItemType } from '../interface'; import MenuItem from '../MenuItem'; +import MenuItemGroup from '../MenuItemGroup'; +import SubMenu from '../SubMenu'; import { parseChildren } from './commonUtil'; +function convertItemsToNodes( + list: ItemType[], + components: Required, +) { + const { + item: MergedMenuItem, + group: MergedMenuItemGroup, + submenu: MergedSubMenu, + divider: MergedDivider, + } = components; -function convertItemsToNodes(list: ItemType[]) { return (list || []) .map((opt, index) => { if (opt && typeof opt === 'object') { @@ -19,29 +28,29 @@ function convertItemsToNodes(list: ItemType[]) { if (type === 'group') { // Group return ( - - {convertItemsToNodes(children)} - + + {convertItemsToNodes(children, components)} + ); } // Sub Menu return ( - - {convertItemsToNodes(children)} - + + {convertItemsToNodes(children, components)} + ); } // MenuItem & Divider if (type === 'divider') { - return ; + return ; } return ( - + {label} - + ); } @@ -54,11 +63,20 @@ export function parseItems( children: React.ReactNode | undefined, items: ItemType[] | undefined, keyPath: string[], + components: Components, ) { let childNodes = children; + const mergedComponents: Required = { + divider: Divider, + item: MenuItem, + group: MenuItemGroup, + submenu: SubMenu, + ...components, + }; + if (items) { - childNodes = convertItemsToNodes(items); + childNodes = convertItemsToNodes(items, mergedComponents); } return parseChildren(childNodes, keyPath); diff --git a/tests/Responsive.spec.tsx b/tests/Responsive.spec.tsx index ba443cfa..9d1e6ff6 100644 --- a/tests/Responsive.spec.tsx +++ b/tests/Responsive.spec.tsx @@ -1,13 +1,12 @@ /* eslint-disable no-undef, react/no-multi-comp, react/jsx-curly-brace-presence, max-classes-per-file */ -import { fireEvent, render } from '@testing-library/react'; +import { act, fireEvent, render } from '@testing-library/react'; import ResizeObserver from 'rc-resize-observer'; import KeyCode from 'rc-util/lib/KeyCode'; +import { spyElementPrototype } from 'rc-util/lib/test/domHook'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import Menu, { MenuItem, SubMenu } from '../src'; import { OVERFLOW_KEY } from '../src/hooks/useKeyRecords'; import { last } from './util'; -import { spyElementPrototype } from 'rc-util/lib/test/domHook'; jest.mock('rc-resize-observer', () => { const R = require('react'); @@ -29,7 +28,6 @@ jest.mock('rc-resize-observer', () => { }); }); - describe('Menu.Responsive', () => { beforeEach(() => { global.resizeProps = null; @@ -61,9 +59,18 @@ describe('Menu.Responsive', () => { render( - - Good - + , ); @@ -115,8 +122,8 @@ describe('Menu.Responsive', () => { get() { return () => ({ width: 41, - }) - } + }); + }, })); // Set container width act(() => { @@ -129,8 +136,8 @@ describe('Menu.Responsive', () => { get() { return () => ({ width: 20, - }) - } + }); + }, })); // Resize every item getResizeProps()