Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
19 changes: 19 additions & 0 deletions docs/examples/items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,32 @@ 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',
},
{
key: 'ToOriginNode',
type: 'item',
label: 'Navigation Two',
},
{
key: 'ToOriginNode1',
label: 'SubMenu',
},
{
// MenuGroup
type: 'group',
Expand Down
10 changes: 8 additions & 2 deletions src/Divider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@ import type { MenuDividerType } from './interface';

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

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

if (measure) {
return null;
}

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

if (typeof itemRender === 'function') {
return itemRender(renderNode);
}

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

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

popupRender?: PopupRender;

itemRender?: (originNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactNode;
}

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

popupRender,

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

Expand All @@ -250,10 +255,10 @@ const Menu = React.forwardRef<MenuRef, MenuProps>((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, itemRender),
parseItems(children, items, EMPTY_LIST, {}, prefixCls, itemRender),
],
[children, items, _internalComponents],
[children, items, _internalComponents, prefixCls],
);

const [mounted, setMounted] = React.useState(false);
Expand Down
6 changes: 6 additions & 0 deletions src/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<

onFocus,

itemRender,

...restProps
} = props;

Expand Down Expand Up @@ -238,6 +240,10 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<
</LegacyMenuItem>
);

if (typeof itemRender === 'function') {
renderNode = itemRender(renderNode);
}

if (_internalRenderMenuItem) {
renderNode = _internalRenderMenuItem(renderNode, props, { selected });
}
Expand Down
4 changes: 2 additions & 2 deletions src/MenuItemGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const InternalMenuItemGroup = React.forwardRef<HTMLLIElement, MenuItemGroupProps
});

const MenuItemGroup = React.forwardRef<HTMLLIElement, MenuItemGroupProps>((props, ref) => {
const { eventKey, children } = props;
const { eventKey, children, itemRender } = props;
const connectedKeyPath = useFullPath(eventKey);
const childList: React.ReactElement[] = parseChildren(children, connectedKeyPath);

Expand All @@ -63,7 +63,7 @@ const MenuItemGroup = React.forwardRef<HTMLLIElement, MenuItemGroupProps>((props

return (
<InternalMenuItemGroup ref={ref} {...omit(props, ['warnKey'])}>
{childList}
{typeof itemRender === 'function' ? itemRender(childList) : childList}
</InternalMenuItemGroup>
);
});
Expand Down
9 changes: 6 additions & 3 deletions src/SubMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ const InternalSubMenu = React.forwardRef<HTMLLIElement, SubMenuProps>((props, re
});

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

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

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

const childListNode = typeof itemRender === 'function' ? itemRender(childList) : childList;

if (measure) {
renderNode = childList;
renderNode = childListNode;
} else {
renderNode = (
<InternalSubMenu ref={ref} {...props}>
{childList}
{childListNode}
</InternalSubMenu>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface ItemSharedProps {
ref?: React.Ref<HTMLLIElement | null>;
style?: React.CSSProperties;
className?: string;
itemRender?: (originNode: React.ReactNode) => React.ReactNode;
}

export interface SubMenuType extends ItemSharedProps {
Expand Down
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
38 changes: 28 additions & 10 deletions src/utils/nodeUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function convertItemsToNodes(
list: ItemType[],
components: Required<Components>,
prefixCls?: string,
itemRender?: (originNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactElement,
) {
const {
item: MergedMenuItem,
Expand All @@ -20,6 +21,9 @@ 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}`;
Expand All @@ -29,31 +33,44 @@ function convertItemsToNodes(
if (type === 'group') {
// Group
return (
<MergedMenuItemGroup key={mergedKey} {...restProps} title={label}>
{convertItemsToNodes(children, components, prefixCls)}
<MergedMenuItemGroup
key={mergedKey}
{...restProps}
itemRender={renderNodeWrapper}
title={label}
>
{convertItemsToNodes(children, components, prefixCls, itemRender)}
</MergedMenuItemGroup>
);
}

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

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

return (
<MergedMenuItem key={mergedKey} {...restProps} extra={extra}>
<MergedMenuItem
key={mergedKey}
{...restProps}
extra={extra}
itemRender={renderNodeWrapper}
>
{label}
{(!!extra || extra === 0) && (
<span className={`${prefixCls}-item-extra`}>{extra}</span>
)}
{(!!extra || extra === 0) && <span className={`${prefixCls}-item-extra`}>{extra}</span>}
</MergedMenuItem>
);
}
Expand All @@ -69,6 +86,7 @@ export function parseItems(
keyPath: string[],
components: Components,
prefixCls?: string,
itemRender?: (originNode: React.ReactNode, item?: NonNullable<ItemType>) => React.ReactElement,
) {
let childNodes = children;

Expand All @@ -81,7 +99,7 @@ export function parseItems(
};

if (items) {
childNodes = convertItemsToNodes(items, mergedComponents, prefixCls);
childNodes = convertItemsToNodes(items, mergedComponents, prefixCls, itemRender);
}

return parseChildren(childNodes, keyPath);
Expand Down
35 changes: 35 additions & 0 deletions tests/MenuItem.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,5 +228,40 @@ describe('MenuItem', () => {

expect(container.querySelector('li')).toMatchSnapshot();
});

it('should wrap originNode with custom render', () => {
const { container } = render(
<Menu
itemRender={(originNode, item) => {
if (item.type === 'item') {
return (
<a href="https://ant.design" target="_blank" rel="noopener noreferrer">
{originNode}
</a>
);
}
return originNode;
}}
items={[
{
key: 'mail',
type: 'item',
label: 'Navigation One',
},
{
key: 'app',
label: 'Navigation Two',
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

render 放 item 里意义不大,和直接写 label 没区别。用户期望的是可以在顶层统一配置 itemRender

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Menu itemRender={...} />

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{
key: 'upload',
label: 'Upload File',
},
]}
/>,
);

const link = container.querySelector('a');
expect(link).toHaveAttribute('href', 'https://ant.design');
});
});
});