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 diff --git a/docs/examples/items.tsx b/docs/examples/items.tsx index a0044731..5bc57466 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/Divider.tsx b/src/Divider.tsx index 616af405..2f68b149 100644 --- a/src/Divider.tsx +++ b/src/Divider.tsx @@ -3,22 +3,36 @@ 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 }: 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; } - return ( + const 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 6d1cdf36..dd57dc52 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -28,9 +28,11 @@ import type { SelectInfo, TriggerSubMenuAction, PopupRender, + ItemRenderType, } 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'; @@ -157,6 +159,8 @@ export interface MenuProps _internalComponents?: Components; popupRender?: PopupRender; + + itemRender?: ItemRenderType; } interface LegacyMenuProps extends MenuProps { @@ -242,6 +246,8 @@ const Menu = React.forwardRef((props, ref) => { _internalComponents, popupRender, + + itemRender, ...restProps } = props as LegacyMenuProps; @@ -253,7 +259,7 @@ const Menu = React.forwardRef((props, ref) => { parseItems(children, items, EMPTY_LIST, _internalComponents, prefixCls), parseItems(children, items, EMPTY_LIST, {}, prefixCls), ], - [children, items, _internalComponents], + [children, items, _internalComponents, prefixCls], ); const [mounted, setMounted] = React.useState(false); @@ -655,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 6b179e73..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,6 +89,8 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< onFocus, + itemRender: propItemRender, + ...restProps } = props; @@ -109,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`; @@ -198,7 +204,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< optionRoleProps['aria-selected'] = selected; } - let renderNode = ( + let renderNode: React.ReactElement = ( ); + if (typeof mergedItemRender === 'function') { + renderNode = mergedItemRender(renderNode, { + item: { + type: 'item', + ...props, + } as ItemType, + keys: connectedKeys, + }) as React.ReactElement; + } + if (_internalRenderMenuItem) { renderNode = _internalRenderMenuItem(renderNode, props, { selected }); } diff --git a/src/MenuItemGroup.tsx b/src/MenuItemGroup.tsx index 7bc04740..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 } = 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 ( - {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 b419cd5c..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'; @@ -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,24 @@ const SubMenu = React.forwardRef((props, ref) => { let renderNode: React.ReactNode; // ======================== Render ======================== + + const childListNode = + typeof itemRender === 'function' + ? itemRender(childList, { + item: { + type: 'submenu', + ...props, + } as ItemType, + keys: connectedKeyPath, + }) + : childList; + if (measure) { - renderNode = childList; + renderNode = childListNode; } else { renderNode = ( - {childList} + {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 e845058b..d34f1ae1 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,11 +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?: ItemRenderType; } export interface SubMenuType extends ItemSharedProps { @@ -140,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/commonUtil.ts b/src/utils/commonUtil.ts index fc09b9f4..ef2258d4 100644 --- a/src/utils/commonUtil.ts +++ b/src/utils/commonUtil.ts @@ -13,7 +13,10 @@ export function parseChildren(children: React.ReactNode | undefined, keyPath: st 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; diff --git a/src/utils/nodeUtil.tsx b/src/utils/nodeUtil.tsx index c0b81106..587efc38 100644 --- a/src/utils/nodeUtil.tsx +++ b/src/utils/nodeUtil.tsx @@ -51,9 +51,7 @@ function convertItemsToNodes( return ( {label} - {(!!extra || extra === 0) && ( - {extra} - )} + {(!!extra || extra === 0) && {extra}} ); } diff --git a/tests/MenuItem.spec.tsx b/tests/MenuItem.spec.tsx index 7e7e2177..657ab26d 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'); + }); }); });