Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ ReactDOM.render(
<th>() => document.body</th>
<td>Where to render the DOM node of popup menu when the mode is horizontal or vertical</td>
</tr>
<tr>
<td>itemRender</td>
<td>Function(originNode:React.ReactNode, item:ItemType) => React.ReactNode</td>
<th>() => originNode</th>
<td>Customize the rendering of menu item</td>
</tr>
<tr>
<td>builtinPlacements</td>
<td>Object of alignConfigs for <a href="https://github.com/yiminghe/dom-align">dom-align</a></td>
Expand Down
27 changes: 18 additions & 9 deletions docs/examples/items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@ import '../../assets/index.less';

export default () => (
<Menu
itemRender={(originNode, { item }) => {
if (item.type === 'item') {
return (
<a href="https://ant.design" target="_blank" rel="noopener noreferrer">
{originNode}
</a>
);
}
return originNode;
}}
items={[
{
// MenuItem
label: 'Top Menu Item',
key: 'top',
extra: '⌘B',
},
{
// MenuGroup
type: 'group',
label: 'Top Menu Group without children',
key: 'ToOriginNode',
type: 'item',
label: 'Navigation Two',
disabled: false,
extra: '111',
},
{
// MenuGroup
Expand Down Expand Up @@ -64,6 +70,9 @@ export default () => (
},
],
},
{
type: 'divider', // Must have
},
]}
/>
);
23 changes: 20 additions & 3 deletions src/Divider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,35 @@ import { clsx } from 'clsx';
import { MenuContext } from './context/MenuContext';
import { useMeasure } from './context/PathContext';
import type { MenuDividerType } from './interface';
import { useFullPath } from './context/PathContext';

export type DividerProps = Omit<MenuDividerType, 'type'>;

export default function Divider({ className, style }: DividerProps) {
const { prefixCls } = React.useContext(MenuContext);
export default function Divider(props: DividerProps) {
const { className, style, itemRender: propItemRender, eventOpt } = props;
const { prefixCls, itemRender: contextItemRender } = React.useContext(MenuContext);
const measure = useMeasure();
const connectedKeyPath = useFullPath();

if (measure) {
return null;
}

return (
const renderNode = (
<li role="separator" className={clsx(`${prefixCls}-item-divider`, className)} style={style} />
);

const mergedItemRender = propItemRender || contextItemRender;

if (typeof mergedItemRender === 'function') {
return mergedItemRender(renderNode, {
item: {
type: 'divider',
...eventOpt,
},
keys: connectedKeyPath,
});
}

return renderNode;
}
11 changes: 9 additions & 2 deletions src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -157,6 +159,8 @@ export interface MenuProps
_internalComponents?: Components;

popupRender?: PopupRender;

itemRender?: ItemRenderType;
}

interface LegacyMenuProps extends MenuProps {
Expand Down Expand Up @@ -242,6 +246,8 @@ const Menu = React.forwardRef<MenuRef, MenuProps>((props, ref) => {
_internalComponents,

popupRender,

itemRender,
...restProps
} = props as LegacyMenuProps;

Expand All @@ -253,7 +259,7 @@ const Menu = React.forwardRef<MenuRef, MenuProps>((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);
Expand Down Expand Up @@ -652,6 +658,7 @@ const Menu = React.forwardRef<MenuRef, MenuProps>((props, ref) => {
onItemClick={onInternalClick}
onOpenChange={onInternalOpenChange}
popupRender={popupRender}
itemRender={itemRender}
>
<PathUserContext.Provider value={pathUserContext}>{container}</PathUserContext.Provider>

Expand Down
30 changes: 26 additions & 4 deletions src/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,6 +89,10 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<

onFocus,

itemRender: propItemRender,

eventOpt,

...restProps
} = props;

Expand All @@ -109,8 +113,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`;
Expand Down Expand Up @@ -198,7 +206,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<
optionRoleProps['aria-selected'] = selected;
}

let renderNode = (
let renderNode: React.ReactElement = (
<LegacyMenuItem
ref={legacyMenuItemRef}
elementRef={mergedEleRef}
Expand Down Expand Up @@ -230,16 +238,30 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<
{children}
<Icon
props={{
...props,
...omit(props, ['extra', 'eventOpt', 'itemRender']),
isSelected: selected,
}}
icon={mergedItemIcon}
/>
</LegacyMenuItem>
);

if (typeof mergedItemRender === 'function') {
renderNode = mergedItemRender(renderNode, {
item: {
type: 'item',
...eventOpt,
},
keys: connectedKeys,
}) as React.ReactElement;
}

if (_internalRenderMenuItem) {
renderNode = _internalRenderMenuItem(renderNode, props, { selected });
renderNode = _internalRenderMenuItem(
renderNode,
omit(props, ['extra', 'eventOpt', 'itemRender']),
{ selected },
);
}

return renderNode;
Expand Down
17 changes: 14 additions & 3 deletions src/MenuItemGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,29 @@ const InternalMenuItemGroup = React.forwardRef<HTMLLIElement, MenuItemGroupProps
});

const MenuItemGroup = React.forwardRef<HTMLLIElement, MenuItemGroupProps>((props, ref) => {
const { eventKey, children } = props;
const { eventKey, children, itemRender: propItemRender, eventOpt } = 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 (
<InternalMenuItemGroup ref={ref} {...omit(props, ['warnKey'])}>
{childList}
<InternalMenuItemGroup ref={ref} {...omit(props, ['warnKey', 'eventOpt', 'itemRender'])}>
{typeof mergedItemRender === 'function'
? mergedItemRender(childList, {
item: {
type: 'group',
...eventOpt,
},
keys: connectedKeyPath,
})
: childList}
</InternalMenuItemGroup>
);
});
Expand Down
28 changes: 23 additions & 5 deletions src/SubMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -384,7 +384,13 @@ const InternalSubMenu = React.forwardRef<HTMLLIElement, SubMenuProps>((props, re
});

const SubMenu = React.forwardRef<HTMLLIElement, SubMenuProps>((props, ref) => {
const { eventKey, children } = props;
const { eventKey, children, itemRender, eventOpt, ...restProps } = props;

const mergedProps = {
eventKey,
children,
...restProps,
};

const connectedKeyPath = useFullPath(eventKey);
const childList: React.ReactElement[] = parseChildren(children, connectedKeyPath);
Expand All @@ -406,12 +412,24 @@ const SubMenu = React.forwardRef<HTMLLIElement, SubMenuProps>((props, ref) => {
let renderNode: React.ReactNode;

// ======================== Render ========================

const childListNode =
typeof itemRender === 'function'
? itemRender(childList, {
item: {
type: 'submenu',
...eventOpt,
} as ItemType,
keys: connectedKeyPath,
})
: childList;

if (measure) {
renderNode = childList;
renderNode = childListNode;
} else {
renderNode = (
<InternalSubMenu ref={ref} {...props}>
{childList}
<InternalSubMenu ref={ref} {...mergedProps}>
{childListNode}
</InternalSubMenu>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/context/MenuContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
RenderIconType,
TriggerSubMenuAction,
PopupRender,
ItemRenderType,
} from '../interface';
import { SubMenuProps } from '..';

Expand Down Expand Up @@ -53,6 +54,8 @@ export interface MenuContextProps {

popupRender?: PopupRender;

itemRender?: ItemRenderType;

// Icon
itemIcon?: RenderIconType;
expandIcon?: RenderIconType;
Expand Down
7 changes: 7 additions & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ interface ItemSharedProps {
ref?: React.Ref<HTMLLIElement | null>;
style?: React.CSSProperties;
className?: string;
itemRender?: ItemRenderType;
eventOpt?: ItemType;
}

export interface SubMenuType extends ItemSharedProps {
Expand Down Expand Up @@ -140,3 +142,8 @@ export type PopupRender = (
node: React.ReactElement,
info: { item: SubMenuProps; keys: string[] },
) => React.ReactNode;

export type ItemRenderType = (
node: React.ReactElement | React.ReactElement<any, string | React.JSXElementConstructor<any>>[],
info: { item: ItemType; keys: string[] },
) => React.ReactNode | React.ReactElement;
5 changes: 4 additions & 1 deletion src/utils/commonUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 6 additions & 7 deletions src/utils/nodeUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,37 @@ function convertItemsToNodes(
.map((opt, index) => {
if (opt && typeof opt === 'object') {
const { label, children, key, type, extra, ...restProps } = opt as any;

const mergedKey = key ?? `tmp-${index}`;

// MenuItemGroup & SubMenuItem
if (children || type === 'group') {
if (type === 'group') {
// Group
return (
<MergedMenuItemGroup key={mergedKey} {...restProps} title={label}>
<MergedMenuItemGroup key={mergedKey} eventOpt={opt} {...restProps} title={label}>
{convertItemsToNodes(children, components, prefixCls)}
</MergedMenuItemGroup>
);
}

// Sub Menu
return (
<MergedSubMenu key={mergedKey} {...restProps} title={label}>
<MergedSubMenu key={mergedKey} eventOpt={opt} {...restProps} title={label}>
{convertItemsToNodes(children, components, prefixCls)}
</MergedSubMenu>
);
}

// MenuItem & Divider
if (type === 'divider') {
return <MergedDivider key={mergedKey} {...restProps} />;
return <MergedDivider key={mergedKey} eventOpt={opt} {...restProps} />;
}

return (
<MergedMenuItem key={mergedKey} {...restProps} extra={extra}>
<MergedMenuItem key={mergedKey} eventOpt={opt} {...restProps} extra={extra}>
{label}
{(!!extra || extra === 0) && (
<span className={`${prefixCls}-item-extra`}>{extra}</span>
)}
{(!!extra || extra === 0) && <span className={`${prefixCls}-item-extra`}>{extra}</span>}
</MergedMenuItem>
);
}
Expand Down
Loading