From d45697e9e076c225d616fcd424921ba1bbd4389e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Sun, 17 Aug 2025 22:20:25 +0800 Subject: [PATCH 1/9] feat: Support custom rendering --- docs/examples/items.tsx | 19 +++++++++++++++ src/Menu.tsx | 13 +++++++--- src/utils/nodeUtil.tsx | 54 +++++++++++++++++++++++------------------ tests/MenuItem.spec.tsx | 35 ++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 27 deletions(-) diff --git a/docs/examples/items.tsx b/docs/examples/items.tsx index a0044731..7dbfb07d 100644 --- a/docs/examples/items.tsx +++ b/docs/examples/items.tsx @@ -6,6 +6,16 @@ import '../../assets/index.less'; export default () => ( { + if (item.type === 'item') { + return ( + + {originNode} + + ); + } + return originNode; + }} items={[ { // MenuItem @@ -13,6 +23,15 @@ export default () => ( key: 'top', extra: '⌘B', }, + { + key: 'ToOriginNode', + type: 'item', + label: 'Navigation Two', + }, + { + key: 'ToOriginNode1', + label: 'SubMenu', + }, { // MenuGroup type: 'group', diff --git a/src/Menu.tsx b/src/Menu.tsx index 6d1cdf36..b7dd9ae4 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -30,7 +30,8 @@ import type { PopupRender, } from './interface'; import MenuItem from './MenuItem'; -import SubMenu, { SemanticName } from './SubMenu'; +import type { SemanticName } from './SubMenu'; +import SubMenu from './SubMenu'; import { parseItems } from './utils/nodeUtil'; import { warnItemProp } from './utils/warnUtil'; @@ -61,6 +62,8 @@ export interface MenuProps /** @deprecated Please use `items` instead */ children?: React.ReactNode; + itemsRender?: (originalNode: React.ReactNode, item: NonNullable) => React.ReactNode; + disabled?: boolean; /** @private Disable auto overflow. Pls note the prop name may refactor since we do not final decided. */ disabledOverflow?: boolean; @@ -242,6 +245,8 @@ const Menu = React.forwardRef((props, ref) => { _internalComponents, popupRender, + + itemsRender, ...restProps } = props as LegacyMenuProps; @@ -250,10 +255,10 @@ const Menu = React.forwardRef((props, ref) => { measureChildList: React.ReactElement[], ] = React.useMemo( () => [ - parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls), - parseItems(children, items, EMPTY_LIST, {}, prefixCls), + parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls, itemsRender), + parseItems(children, items, EMPTY_LIST, {}, prefixCls, itemsRender), ], - [children, items, _internalComponents], + [children, items, _internalComponents, prefixCls, itemsRender], ); const [mounted, setMounted] = React.useState(false); diff --git a/src/utils/nodeUtil.tsx b/src/utils/nodeUtil.tsx index c0b81106..65b07f47 100644 --- a/src/utils/nodeUtil.tsx +++ b/src/utils/nodeUtil.tsx @@ -10,6 +10,7 @@ function convertItemsToNodes( list: ItemType[], components: Required, prefixCls?: string, + itemsRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactNode, ) { const { item: MergedMenuItem, @@ -24,38 +25,44 @@ function convertItemsToNodes( const { label, children, key, type, extra, ...restProps } = opt as any; const mergedKey = key ?? `tmp-${index}`; - // MenuItemGroup & SubMenuItem + let originNode: React.ReactNode = null; + + // MenuItemGroup & SubMenu if (children || type === 'group') { if (type === 'group') { - // Group - return ( + originNode = ( - {convertItemsToNodes(children, components, prefixCls)} + {convertItemsToNodes(children, components, prefixCls, itemsRender)} ); + } else { + originNode = ( + + {convertItemsToNodes(children, components, prefixCls, itemsRender)} + + ); } - - // Sub Menu - return ( - - {convertItemsToNodes(children, components, prefixCls)} - + } + // Divider + else if (type === 'divider') { + originNode = ; + } + // MenuItem + else { + originNode = ( + + {label} + {(!!extra || extra === 0) && ( + {extra} + )} + ); } - // MenuItem & Divider - if (type === 'divider') { - return ; + if (typeof itemsRender === 'function') { + return itemsRender(originNode, opt); } - - return ( - - {label} - {(!!extra || extra === 0) && ( - {extra} - )} - - ); + return originNode; } return null; @@ -69,6 +76,7 @@ export function parseItems( keyPath: string[], components: Components, prefixCls?: string, + itemsRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactNode, ) { let childNodes = children; @@ -81,7 +89,7 @@ export function parseItems( }; if (items) { - childNodes = convertItemsToNodes(items, mergedComponents, prefixCls); + childNodes = convertItemsToNodes(items, mergedComponents, prefixCls, itemsRender); } return parseChildren(childNodes, keyPath); diff --git a/tests/MenuItem.spec.tsx b/tests/MenuItem.spec.tsx index 7e7e2177..af69f882 100644 --- a/tests/MenuItem.spec.tsx +++ b/tests/MenuItem.spec.tsx @@ -228,5 +228,40 @@ describe('MenuItem', () => { expect(container.querySelector('li')).toMatchSnapshot(); }); + + it('should wrap originNode with custom render', () => { + const { container } = render( + { + if (item.type === 'item') { + return ( + + {originNode} + + ); + } + return originNode; + }} + items={[ + { + key: 'mail', + type: 'item', + label: 'Navigation One', + }, + { + key: 'app', + label: 'Navigation Two', + }, + { + key: 'upload', + label: 'Upload File', + }, + ]} + />, + ); + + const link = container.querySelector('a'); + expect(link).toHaveAttribute('href', 'https://ant.design'); + }); }); }); From b0829a466b75d87d94a5f663b369bd04a78006c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Sun, 17 Aug 2025 22:25:03 +0800 Subject: [PATCH 2/9] feat: update readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 1f21241e..e0d409de 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,12 @@ ReactDOM.render( () => document.body Where to render the DOM node of popup menu when the mode is horizontal or vertical + + itemRender + Function(originNode:React.ReactNode, item:ItemType) => React.ReactNode + () => originNode + Customize the rendering of menu item + builtinPlacements Object of alignConfigs for dom-align From e9f46a00ad37c0891bae545fc2030b7469602c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Tue, 19 Aug 2025 16:56:25 +0800 Subject: [PATCH 3/9] update key --- docs/examples/items.tsx | 2 +- src/Menu.tsx | 10 +++++----- src/utils/commonUtil.ts | 10 ++++++++-- src/utils/nodeUtil.tsx | 16 ++++++++-------- tests/MenuItem.spec.tsx | 2 +- tests/Responsive.spec.tsx | 4 ++-- 6 files changed, 25 insertions(+), 19 deletions(-) diff --git a/docs/examples/items.tsx b/docs/examples/items.tsx index 7dbfb07d..cb9c30c6 100644 --- a/docs/examples/items.tsx +++ b/docs/examples/items.tsx @@ -6,7 +6,7 @@ import '../../assets/index.less'; export default () => ( { + itemRender={(originNode, item) => { if (item.type === 'item') { return ( diff --git a/src/Menu.tsx b/src/Menu.tsx index b7dd9ae4..81be2dac 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -62,7 +62,7 @@ export interface MenuProps /** @deprecated Please use `items` instead */ children?: React.ReactNode; - itemsRender?: (originalNode: React.ReactNode, item: NonNullable) => React.ReactNode; + itemRender?: (originalNode: React.ReactNode, item?: NonNullable) => React.ReactNode; disabled?: boolean; /** @private Disable auto overflow. Pls note the prop name may refactor since we do not final decided. */ @@ -246,7 +246,7 @@ const Menu = React.forwardRef((props, ref) => { popupRender, - itemsRender, + itemRender, ...restProps } = props as LegacyMenuProps; @@ -255,10 +255,10 @@ const Menu = React.forwardRef((props, ref) => { measureChildList: React.ReactElement[], ] = React.useMemo( () => [ - parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls, itemsRender), - parseItems(children, items, EMPTY_LIST, {}, prefixCls, itemsRender), + parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls, itemRender), + parseItems(children, items, EMPTY_LIST, {}, prefixCls, itemRender), ], - [children, items, _internalComponents, prefixCls, itemsRender], + [children, items, _internalComponents, prefixCls, itemRender], ); const [mounted, setMounted] = React.useState(false); diff --git a/src/utils/commonUtil.ts b/src/utils/commonUtil.ts index fc09b9f4..0265848c 100644 --- a/src/utils/commonUtil.ts +++ b/src/utils/commonUtil.ts @@ -1,7 +1,11 @@ import toArray from '@rc-component/util/lib/Children/toArray'; import * as React from 'react'; -export function parseChildren(children: React.ReactNode | undefined, keyPath: string[]) { +export function parseChildren( + children: React.ReactNode | undefined, + keyPath: string[], + itemRender?: (originNode: React.ReactNode) => React.ReactNode, +) { return toArray(children).map((child, index) => { if (React.isValidElement(child)) { const { key } = child; @@ -18,7 +22,9 @@ export function parseChildren(children: React.ReactNode | undefined, keyPath: st if (process.env.NODE_ENV !== 'production' && emptyKey) { cloneProps.warnKey = true; } - + if (typeof itemRender === 'function') { + return itemRender(React.cloneElement(child, cloneProps)); + } return React.cloneElement(child, cloneProps); } diff --git a/src/utils/nodeUtil.tsx b/src/utils/nodeUtil.tsx index 65b07f47..e43d2282 100644 --- a/src/utils/nodeUtil.tsx +++ b/src/utils/nodeUtil.tsx @@ -10,7 +10,7 @@ function convertItemsToNodes( list: ItemType[], components: Required, prefixCls?: string, - itemsRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactNode, + itemRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactNode, ) { const { item: MergedMenuItem, @@ -32,13 +32,13 @@ function convertItemsToNodes( if (type === 'group') { originNode = ( - {convertItemsToNodes(children, components, prefixCls, itemsRender)} + {convertItemsToNodes(children, components, prefixCls, itemRender)} ); } else { originNode = ( - {convertItemsToNodes(children, components, prefixCls, itemsRender)} + {convertItemsToNodes(children, components, prefixCls, itemRender)} ); } @@ -59,8 +59,8 @@ function convertItemsToNodes( ); } - if (typeof itemsRender === 'function') { - return itemsRender(originNode, opt); + if (typeof itemRender === 'function') { + return itemRender(originNode, opt); } return originNode; } @@ -76,7 +76,7 @@ export function parseItems( keyPath: string[], components: Components, prefixCls?: string, - itemsRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactNode, + itemRender?: (originNode: React.ReactNode, item?: NonNullable) => React.ReactNode, ) { let childNodes = children; @@ -89,8 +89,8 @@ export function parseItems( }; if (items) { - childNodes = convertItemsToNodes(items, mergedComponents, prefixCls, itemsRender); + childNodes = convertItemsToNodes(items, mergedComponents, prefixCls, itemRender); } - return parseChildren(childNodes, keyPath); + return parseChildren(childNodes, keyPath, itemRender); } diff --git a/tests/MenuItem.spec.tsx b/tests/MenuItem.spec.tsx index af69f882..48a51419 100644 --- a/tests/MenuItem.spec.tsx +++ b/tests/MenuItem.spec.tsx @@ -232,7 +232,7 @@ describe('MenuItem', () => { it('should wrap originNode with custom render', () => { const { container } = render( { + itemRender={(originNode, item) => { if (item.type === 'item') { return ( diff --git a/tests/Responsive.spec.tsx b/tests/Responsive.spec.tsx index 064dfaf9..32987530 100644 --- a/tests/Responsive.spec.tsx +++ b/tests/Responsive.spec.tsx @@ -30,7 +30,7 @@ jest.mock('rc-resize-observer', () => { describe('Menu.Responsive', () => { beforeEach(() => { - global.resizeProps = null; + global.resizeProps = new Map(); jest.useFakeTimers(); }); @@ -122,7 +122,7 @@ describe('Menu.Responsive', () => { })); // Set container width act(() => { - getResizeProps()[0].onResize({}, document.createElement('div')); + getResizeProps()?.[0]?.onResize?.({}, document.createElement('div')); jest.runAllTimers(); }); spy.mockRestore(); From 743803fedd42e5910d9520fda710e4f0c4422da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Tue, 19 Aug 2025 17:02:47 +0800 Subject: [PATCH 4/9] update type and key --- src/Menu.tsx | 4 ++-- src/utils/commonUtil.ts | 2 +- src/utils/nodeUtil.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Menu.tsx b/src/Menu.tsx index 81be2dac..9872d603 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -62,7 +62,7 @@ export interface MenuProps /** @deprecated Please use `items` instead */ children?: React.ReactNode; - itemRender?: (originalNode: React.ReactNode, item?: NonNullable) => React.ReactNode; + itemRender?: (originalNode: React.ReactNode, item?: NonNullable) => React.ReactElement; disabled?: boolean; /** @private Disable auto overflow. Pls note the prop name may refactor since we do not final decided. */ @@ -258,7 +258,7 @@ const Menu = React.forwardRef((props, ref) => { parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls, itemRender), parseItems(children, items, EMPTY_LIST, {}, prefixCls, itemRender), ], - [children, items, _internalComponents, prefixCls, itemRender], + [children, items, _internalComponents, prefixCls], ); const [mounted, setMounted] = React.useState(false); diff --git a/src/utils/commonUtil.ts b/src/utils/commonUtil.ts index 0265848c..0c746fdd 100644 --- a/src/utils/commonUtil.ts +++ b/src/utils/commonUtil.ts @@ -4,7 +4,7 @@ import * as React from 'react'; export function parseChildren( children: React.ReactNode | undefined, keyPath: string[], - itemRender?: (originNode: React.ReactNode) => React.ReactNode, + itemRender?: (originNode: React.ReactNode) => React.ReactElement, ) { return toArray(children).map((child, index) => { if (React.isValidElement(child)) { diff --git a/src/utils/nodeUtil.tsx b/src/utils/nodeUtil.tsx index e43d2282..6af44c39 100644 --- a/src/utils/nodeUtil.tsx +++ b/src/utils/nodeUtil.tsx @@ -10,7 +10,7 @@ function convertItemsToNodes( list: ItemType[], components: Required, prefixCls?: string, - itemRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactNode, + itemRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactElement, ) { const { item: MergedMenuItem, @@ -76,7 +76,7 @@ export function parseItems( keyPath: string[], components: Components, prefixCls?: string, - itemRender?: (originNode: React.ReactNode, item?: NonNullable) => React.ReactNode, + itemRender?: (originNode: React.ReactNode, item?: NonNullable) => React.ReactElement, ) { let childNodes = children; From 281afbb2a45e62fee06d98410a1d02a5062ee8a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Tue, 19 Aug 2025 22:02:51 +0800 Subject: [PATCH 5/9] Support item custom rendering --- src/Divider.tsx | 10 ++++-- src/Menu.tsx | 4 +-- src/MenuItem.tsx | 6 ++++ src/MenuItemGroup.tsx | 4 +-- src/SubMenu/index.tsx | 9 ++++-- src/interface.ts | 1 + src/utils/commonUtil.ts | 15 ++++----- src/utils/nodeUtil.tsx | 70 +++++++++++++++++++++++------------------ 8 files changed, 71 insertions(+), 48 deletions(-) diff --git a/src/Divider.tsx b/src/Divider.tsx index 616af405..b39b8aeb 100644 --- a/src/Divider.tsx +++ b/src/Divider.tsx @@ -6,7 +6,7 @@ import type { MenuDividerType } from './interface'; export type DividerProps = Omit; -export default function Divider({ className, style }: DividerProps) { +export default function Divider({ className, style, itemRender }: DividerProps) { const { prefixCls } = React.useContext(MenuContext); const measure = useMeasure(); @@ -14,11 +14,17 @@ export default function Divider({ className, style }: DividerProps) { return null; } - return ( + const renderNode = (
  • ); + + if (typeof itemRender === 'function') { + return itemRender(renderNode); + } + + return renderNode; } diff --git a/src/Menu.tsx b/src/Menu.tsx index 9872d603..469f5912 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -62,8 +62,6 @@ export interface MenuProps /** @deprecated Please use `items` instead */ children?: React.ReactNode; - itemRender?: (originalNode: React.ReactNode, item?: NonNullable) => React.ReactElement; - disabled?: boolean; /** @private Disable auto overflow. Pls note the prop name may refactor since we do not final decided. */ disabledOverflow?: boolean; @@ -160,6 +158,8 @@ export interface MenuProps _internalComponents?: Components; popupRender?: PopupRender; + + itemRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactNode; } interface LegacyMenuProps extends MenuProps { diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 6b179e73..bac98f7d 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -89,6 +89,8 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< onFocus, + itemRender, + ...restProps } = props; @@ -238,6 +240,10 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< ); + if (typeof itemRender === 'function') { + renderNode = itemRender(renderNode); + } + if (_internalRenderMenuItem) { renderNode = _internalRenderMenuItem(renderNode, props, { selected }); } diff --git a/src/MenuItemGroup.tsx b/src/MenuItemGroup.tsx index 7bc04740..d9646897 100644 --- a/src/MenuItemGroup.tsx +++ b/src/MenuItemGroup.tsx @@ -52,7 +52,7 @@ const InternalMenuItemGroup = React.forwardRef((props, ref) => { - const { eventKey, children } = props; + const { eventKey, children, itemRender } = props; const connectedKeyPath = useFullPath(eventKey); const childList: React.ReactElement[] = parseChildren(children, connectedKeyPath); @@ -63,7 +63,7 @@ const MenuItemGroup = React.forwardRef((props return ( - {childList} + {typeof itemRender === 'function' ? itemRender(childList) : childList} ); }); diff --git a/src/SubMenu/index.tsx b/src/SubMenu/index.tsx index b419cd5c..9d32c4cc 100644 --- a/src/SubMenu/index.tsx +++ b/src/SubMenu/index.tsx @@ -384,7 +384,7 @@ const InternalSubMenu = React.forwardRef((props, re }); const SubMenu = React.forwardRef((props, ref) => { - const { eventKey, children } = props; + const { eventKey, children, itemRender } = props; const connectedKeyPath = useFullPath(eventKey); const childList: React.ReactElement[] = parseChildren(children, connectedKeyPath); @@ -406,12 +406,15 @@ const SubMenu = React.forwardRef((props, ref) => { let renderNode: React.ReactNode; // ======================== Render ======================== + + const childListNode = typeof itemRender === 'function' ? itemRender(childList) : childList; + if (measure) { - renderNode = childList; + renderNode = childListNode; } else { renderNode = ( - {childList} + {childListNode} ); } diff --git a/src/interface.ts b/src/interface.ts index e845058b..f8702de6 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -6,6 +6,7 @@ interface ItemSharedProps { ref?: React.Ref; style?: React.CSSProperties; className?: string; + itemRender?: (originNode: React.ReactNode) => React.ReactNode; } export interface SubMenuType extends ItemSharedProps { diff --git a/src/utils/commonUtil.ts b/src/utils/commonUtil.ts index 0c746fdd..ef2258d4 100644 --- a/src/utils/commonUtil.ts +++ b/src/utils/commonUtil.ts @@ -1,11 +1,7 @@ import toArray from '@rc-component/util/lib/Children/toArray'; import * as React from 'react'; -export function parseChildren( - children: React.ReactNode | undefined, - keyPath: string[], - itemRender?: (originNode: React.ReactNode) => React.ReactElement, -) { +export function parseChildren(children: React.ReactNode | undefined, keyPath: string[]) { return toArray(children).map((child, index) => { if (React.isValidElement(child)) { const { key } = child; @@ -17,14 +13,15 @@ export function parseChildren( eventKey = `tmp_key-${[...keyPath, index].join('-')}`; } - const cloneProps = { key: eventKey, eventKey } as any; + const cloneProps = { + key: eventKey, + eventKey, + } as any; if (process.env.NODE_ENV !== 'production' && emptyKey) { cloneProps.warnKey = true; } - if (typeof itemRender === 'function') { - return itemRender(React.cloneElement(child, cloneProps)); - } + return React.cloneElement(child, cloneProps); } diff --git a/src/utils/nodeUtil.tsx b/src/utils/nodeUtil.tsx index 6af44c39..6c01c9a2 100644 --- a/src/utils/nodeUtil.tsx +++ b/src/utils/nodeUtil.tsx @@ -21,48 +21,58 @@ function convertItemsToNodes( return (list || []) .map((opt, index) => { + const renderNodeWrapper = node => { + return typeof itemRender === 'function' ? itemRender(node, opt as any) : node; + }; if (opt && typeof opt === 'object') { const { label, children, key, type, extra, ...restProps } = opt as any; const mergedKey = key ?? `tmp-${index}`; - let originNode: React.ReactNode = null; - - // MenuItemGroup & SubMenu + // MenuItemGroup & SubMenuItem if (children || type === 'group') { if (type === 'group') { - originNode = ( - - {convertItemsToNodes(children, components, prefixCls, itemRender)} + // Group + return ( + + {convertItemsToNodes(children, components, prefixCls)} ); - } else { - originNode = ( - - {convertItemsToNodes(children, components, prefixCls, itemRender)} - - ); } - } - // Divider - else if (type === 'divider') { - originNode = ; - } - // MenuItem - else { - originNode = ( - - {label} - {(!!extra || extra === 0) && ( - {extra} - )} - + + // Sub Menu + return ( + + {convertItemsToNodes(children, components, prefixCls)} + ); } - if (typeof itemRender === 'function') { - return itemRender(originNode, opt); + // MenuItem & Divider + if (type === 'divider') { + return ; } - return originNode; + + return ( + + {label} + {(!!extra || extra === 0) && {extra}} + + ); } return null; @@ -92,5 +102,5 @@ export function parseItems( childNodes = convertItemsToNodes(items, mergedComponents, prefixCls, itemRender); } - return parseChildren(childNodes, keyPath, itemRender); + return parseChildren(childNodes, keyPath); } From 9ad16df5cf19b6c1772308df1b29934beb95637b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Tue, 19 Aug 2025 22:12:24 +0800 Subject: [PATCH 6/9] revert: test --- tests/Responsive.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Responsive.spec.tsx b/tests/Responsive.spec.tsx index 32987530..064dfaf9 100644 --- a/tests/Responsive.spec.tsx +++ b/tests/Responsive.spec.tsx @@ -30,7 +30,7 @@ jest.mock('rc-resize-observer', () => { describe('Menu.Responsive', () => { beforeEach(() => { - global.resizeProps = new Map(); + global.resizeProps = null; jest.useFakeTimers(); }); @@ -122,7 +122,7 @@ describe('Menu.Responsive', () => { })); // Set container width act(() => { - getResizeProps()?.[0]?.onResize?.({}, document.createElement('div')); + getResizeProps()[0].onResize({}, document.createElement('div')); jest.runAllTimers(); }); spy.mockRestore(); From a438a2ea368783370663aa630162f7be9448aad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Tue, 19 Aug 2025 22:16:50 +0800 Subject: [PATCH 7/9] feat: update code --- src/utils/nodeUtil.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/nodeUtil.tsx b/src/utils/nodeUtil.tsx index 6c01c9a2..dc2beec8 100644 --- a/src/utils/nodeUtil.tsx +++ b/src/utils/nodeUtil.tsx @@ -39,7 +39,7 @@ function convertItemsToNodes( itemRender={renderNodeWrapper} title={label} > - {convertItemsToNodes(children, components, prefixCls)} + {convertItemsToNodes(children, components, prefixCls, itemRender)} ); } @@ -52,7 +52,7 @@ function convertItemsToNodes( itemRender={renderNodeWrapper} title={label} > - {convertItemsToNodes(children, components, prefixCls)} + {convertItemsToNodes(children, components, prefixCls, itemRender)} ); } From 89930615b8f02bb5c9707c314e36a90139b71047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Wed, 20 Aug 2025 22:30:48 +0800 Subject: [PATCH 8/9] feat: update code --- docs/examples/items.tsx | 2 +- src/Divider.tsx | 16 ++++++++++++---- src/Menu.tsx | 8 +++++--- src/MenuItem.tsx | 20 +++++++++++++++----- src/MenuItemGroup.tsx | 16 +++++++++++++--- src/SubMenu/index.tsx | 13 +++++++++++-- src/context/MenuContext.tsx | 3 +++ src/interface.ts | 10 +++++++++- src/utils/nodeUtil.tsx | 34 +++++++--------------------------- tests/MenuItem.spec.tsx | 2 +- 10 files changed, 77 insertions(+), 47 deletions(-) diff --git a/docs/examples/items.tsx b/docs/examples/items.tsx index cb9c30c6..5bc57466 100644 --- a/docs/examples/items.tsx +++ b/docs/examples/items.tsx @@ -6,7 +6,7 @@ import '../../assets/index.less'; export default () => ( { + itemRender={(originNode, { item }) => { if (item.type === 'item') { return ( diff --git a/src/Divider.tsx b/src/Divider.tsx index b39b8aeb..2f68b149 100644 --- a/src/Divider.tsx +++ b/src/Divider.tsx @@ -3,12 +3,15 @@ import classNames from 'classnames'; import { MenuContext } from './context/MenuContext'; import { useMeasure } from './context/PathContext'; import type { MenuDividerType } from './interface'; +import { useFullPath } from './context/PathContext'; export type DividerProps = Omit; -export default function Divider({ className, style, itemRender }: DividerProps) { - const { prefixCls } = React.useContext(MenuContext); +export default function Divider(props: DividerProps) { + const { className, style, itemRender: propItemRender } = props; + const { prefixCls, itemRender: contextItemRender } = React.useContext(MenuContext); const measure = useMeasure(); + const connectedKeyPath = useFullPath(); if (measure) { return null; @@ -22,8 +25,13 @@ export default function Divider({ className, style, itemRender }: DividerProps) /> ); - if (typeof itemRender === 'function') { - return itemRender(renderNode); + const mergedItemRender = propItemRender || contextItemRender; + + if (typeof mergedItemRender === 'function') { + return mergedItemRender(renderNode, { + item: { type: 'divider', ...props }, + keys: connectedKeyPath, + }); } return renderNode; diff --git a/src/Menu.tsx b/src/Menu.tsx index 469f5912..dd57dc52 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -28,6 +28,7 @@ import type { SelectInfo, TriggerSubMenuAction, PopupRender, + ItemRenderType, } from './interface'; import MenuItem from './MenuItem'; import type { SemanticName } from './SubMenu'; @@ -159,7 +160,7 @@ export interface MenuProps popupRender?: PopupRender; - itemRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactNode; + itemRender?: ItemRenderType; } interface LegacyMenuProps extends MenuProps { @@ -255,8 +256,8 @@ const Menu = React.forwardRef((props, ref) => { measureChildList: React.ReactElement[], ] = React.useMemo( () => [ - parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls, itemRender), - parseItems(children, items, EMPTY_LIST, {}, prefixCls, itemRender), + parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls), + parseItems(children, items, EMPTY_LIST, {}, prefixCls), ], [children, items, _internalComponents, prefixCls], ); @@ -660,6 +661,7 @@ const Menu = React.forwardRef((props, ref) => { onItemClick={onInternalClick} onOpenChange={onInternalOpenChange} popupRender={popupRender} + itemRender={itemRender} > {container} diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index bac98f7d..61fb5064 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -12,7 +12,7 @@ import PrivateContext from './context/PrivateContext'; import useActive from './hooks/useActive'; import useDirectionStyle from './hooks/useDirectionStyle'; import Icon from './Icon'; -import type { MenuInfo, MenuItemType } from './interface'; +import type { MenuInfo, MenuItemType, ItemType } from './interface'; import { warnItemProp } from './utils/warnUtil'; export interface MenuItemProps @@ -89,7 +89,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< onFocus, - itemRender, + itemRender: propItemRender, ...restProps } = props; @@ -111,8 +111,12 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< // Active onActive, + + itemRender: contextItemRender, } = React.useContext(MenuContext); + const mergedItemRender = propItemRender || contextItemRender; + const { _internalRenderMenuItem } = React.useContext(PrivateContext); const itemCls = `${prefixCls}-item`; @@ -200,7 +204,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< optionRoleProps['aria-selected'] = selected; } - let renderNode = ( + let renderNode: React.ReactElement = ( ); - if (typeof itemRender === 'function') { - renderNode = itemRender(renderNode); + if (typeof mergedItemRender === 'function') { + renderNode = mergedItemRender(renderNode, { + item: { + type: 'item', + ...props, + } as ItemType, + keys: connectedKeys, + }) as React.ReactElement; } if (_internalRenderMenuItem) { diff --git a/src/MenuItemGroup.tsx b/src/MenuItemGroup.tsx index d9646897..ff01a941 100644 --- a/src/MenuItemGroup.tsx +++ b/src/MenuItemGroup.tsx @@ -3,7 +3,7 @@ import omit from '@rc-component/util/lib/omit'; import * as React from 'react'; import { MenuContext } from './context/MenuContext'; import { useFullPath, useMeasure } from './context/PathContext'; -import type { MenuItemGroupType } from './interface'; +import type { MenuItemGroupType, ItemType } from './interface'; import { parseChildren } from './utils/commonUtil'; export interface MenuItemGroupProps extends Omit { @@ -52,18 +52,28 @@ const InternalMenuItemGroup = React.forwardRef((props, ref) => { - const { eventKey, children, itemRender } = props; + const { eventKey, children, itemRender: propItemRender } = props; const connectedKeyPath = useFullPath(eventKey); const childList: React.ReactElement[] = parseChildren(children, connectedKeyPath); + const { itemRender: contextItemRender } = React.useContext(MenuContext); const measure = useMeasure(); if (measure) { return childList as any as React.ReactElement; } + const mergedItemRender = propItemRender || contextItemRender; return ( - {typeof itemRender === 'function' ? itemRender(childList) : childList} + {typeof mergedItemRender === 'function' + ? mergedItemRender(childList, { + item: { + type: 'group', + ...props, + } as ItemType, + keys: connectedKeyPath, + }) + : childList} ); }); diff --git a/src/SubMenu/index.tsx b/src/SubMenu/index.tsx index 9d32c4cc..75cf90b3 100644 --- a/src/SubMenu/index.tsx +++ b/src/SubMenu/index.tsx @@ -4,7 +4,7 @@ import Overflow from 'rc-overflow'; import warning from '@rc-component/util/lib/warning'; import SubMenuList from './SubMenuList'; import { parseChildren } from '../utils/commonUtil'; -import type { MenuInfo, SubMenuType, PopupRender } from '../interface'; +import type { MenuInfo, SubMenuType, PopupRender, ItemType } from '../interface'; import MenuContextProvider, { MenuContext } from '../context/MenuContext'; import useMemoCallback from '../hooks/useMemoCallback'; import PopupTrigger from './PopupTrigger'; @@ -407,7 +407,16 @@ const SubMenu = React.forwardRef((props, ref) => { // ======================== Render ======================== - const childListNode = typeof itemRender === 'function' ? itemRender(childList) : childList; + const childListNode = + typeof itemRender === 'function' + ? itemRender(childList, { + item: { + type: 'submenu', + ...props, + } as ItemType, + keys: connectedKeyPath, + }) + : childList; if (measure) { renderNode = childListNode; diff --git a/src/context/MenuContext.tsx b/src/context/MenuContext.tsx index 1de6a8ff..3c8ecb60 100644 --- a/src/context/MenuContext.tsx +++ b/src/context/MenuContext.tsx @@ -9,6 +9,7 @@ import type { RenderIconType, TriggerSubMenuAction, PopupRender, + ItemRenderType, } from '../interface'; import { SubMenuProps } from '..'; @@ -53,6 +54,8 @@ export interface MenuContextProps { popupRender?: PopupRender; + itemRender?: ItemRenderType; + // Icon itemIcon?: RenderIconType; expandIcon?: RenderIconType; diff --git a/src/interface.ts b/src/interface.ts index f8702de6..d34f1ae1 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,12 +1,15 @@ import type * as React from 'react'; import type { SubMenuProps } from './SubMenu'; +import type { MenuItemProps } from './MenuItem'; +import type { MenuItemGroupProps } from './MenuItemGroup'; +import type { DividerProps } from './Divider'; // ========================= Options ========================= interface ItemSharedProps { ref?: React.Ref; style?: React.CSSProperties; className?: string; - itemRender?: (originNode: React.ReactNode) => React.ReactNode; + itemRender?: ItemRenderType; } export interface SubMenuType extends ItemSharedProps { @@ -141,3 +144,8 @@ export type PopupRender = ( node: React.ReactElement, info: { item: SubMenuProps; keys: string[] }, ) => React.ReactNode; + +export type ItemRenderType = ( + node: React.ReactElement | React.ReactElement>[], + info: { item: ItemType; keys: string[] }, +) => React.ReactNode | React.ReactElement; diff --git a/src/utils/nodeUtil.tsx b/src/utils/nodeUtil.tsx index dc2beec8..587efc38 100644 --- a/src/utils/nodeUtil.tsx +++ b/src/utils/nodeUtil.tsx @@ -10,7 +10,6 @@ function convertItemsToNodes( list: ItemType[], components: Required, prefixCls?: string, - itemRender?: (originNode: React.ReactNode, item: NonNullable) => React.ReactElement, ) { const { item: MergedMenuItem, @@ -21,9 +20,6 @@ function convertItemsToNodes( return (list || []) .map((opt, index) => { - const renderNodeWrapper = node => { - return typeof itemRender === 'function' ? itemRender(node, opt as any) : node; - }; if (opt && typeof opt === 'object') { const { label, children, key, type, extra, ...restProps } = opt as any; const mergedKey = key ?? `tmp-${index}`; @@ -33,42 +29,27 @@ function convertItemsToNodes( if (type === 'group') { // Group return ( - - {convertItemsToNodes(children, components, prefixCls, itemRender)} + + {convertItemsToNodes(children, components, prefixCls)} ); } // Sub Menu return ( - - {convertItemsToNodes(children, components, prefixCls, itemRender)} + + {convertItemsToNodes(children, components, prefixCls)} ); } // MenuItem & Divider if (type === 'divider') { - return ; + return ; } return ( - + {label} {(!!extra || extra === 0) && {extra}} @@ -86,7 +67,6 @@ export function parseItems( keyPath: string[], components: Components, prefixCls?: string, - itemRender?: (originNode: React.ReactNode, item?: NonNullable) => React.ReactElement, ) { let childNodes = children; @@ -99,7 +79,7 @@ export function parseItems( }; if (items) { - childNodes = convertItemsToNodes(items, mergedComponents, prefixCls, itemRender); + childNodes = convertItemsToNodes(items, mergedComponents, prefixCls); } return parseChildren(childNodes, keyPath); diff --git a/tests/MenuItem.spec.tsx b/tests/MenuItem.spec.tsx index 48a51419..657ab26d 100644 --- a/tests/MenuItem.spec.tsx +++ b/tests/MenuItem.spec.tsx @@ -232,7 +232,7 @@ describe('MenuItem', () => { it('should wrap originNode with custom render', () => { const { container } = render( { + itemRender={(originNode, { item }) => { if (item.type === 'item') { return ( From 6a22b419be2752554e6b688a2cfa42cb267bd9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=81=87=E8=A7=81=E5=90=8C=E5=AD=A6?= <1875694521@qq.com> Date: Tue, 2 Sep 2025 13:58:41 +0800 Subject: [PATCH 9/9] update type --- src/interface.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/interface.ts b/src/interface.ts index d34f1ae1..bfde58c1 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,8 +1,5 @@ import type * as React from 'react'; import type { SubMenuProps } from './SubMenu'; -import type { MenuItemProps } from './MenuItem'; -import type { MenuItemGroupProps } from './MenuItemGroup'; -import type { DividerProps } from './Divider'; // ========================= Options ========================= interface ItemSharedProps {