Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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
itemsRender={(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
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 @@ -61,6 +62,8 @@ export interface MenuProps
/** @deprecated Please use `items` instead */
children?: React.ReactNode;

itemsRender?: (originalNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactNode;

disabled?: boolean;
/** @private Disable auto overflow. Pls note the prop name may refactor since we do not final decided. */
disabledOverflow?: boolean;
Expand Down Expand Up @@ -242,6 +245,8 @@ const Menu = React.forwardRef<MenuRef, MenuProps>((props, ref) => {
_internalComponents,

popupRender,

itemsRender,
...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, itemsRender),
parseItems(children, items, EMPTY_LIST, {}, prefixCls, itemsRender),
],
[children, items, _internalComponents],
[children, items, _internalComponents, prefixCls, itemsRender],
);

const [mounted, setMounted] = React.useState(false);
Expand Down
54 changes: 31 additions & 23 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,
itemsRender?: (originNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactNode,
) {
const {
item: MergedMenuItem,
Expand All @@ -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 = (
<MergedMenuItemGroup key={mergedKey} {...restProps} title={label}>
{convertItemsToNodes(children, components, prefixCls)}
{convertItemsToNodes(children, components, prefixCls, itemsRender)}
</MergedMenuItemGroup>
);
} else {
originNode = (
<MergedSubMenu key={mergedKey} {...restProps} title={label}>
{convertItemsToNodes(children, components, prefixCls, itemsRender)}
</MergedSubMenu>
);
}

// Sub Menu
return (
<MergedSubMenu key={mergedKey} {...restProps} title={label}>
{convertItemsToNodes(children, components, prefixCls)}
</MergedSubMenu>
}
// Divider
else if (type === 'divider') {
originNode = <MergedDivider key={mergedKey} {...restProps} />;
}
// MenuItem
else {
originNode = (
<MergedMenuItem key={mergedKey} {...restProps} extra={extra}>
{label}
{(!!extra || extra === 0) && (
<span className={`${prefixCls}-item-extra`}>{extra}</span>
)}
</MergedMenuItem>
);
}

// MenuItem & Divider
if (type === 'divider') {
return <MergedDivider key={mergedKey} {...restProps} />;
if (typeof itemsRender === 'function') {
return itemsRender(originNode, opt);
}

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

return null;
Expand All @@ -69,6 +76,7 @@ export function parseItems(
keyPath: string[],
components: Components,
prefixCls?: string,
itemsRender?: (originNode: React.ReactNode, item: NonNullable<ItemType>) => React.ReactNode,
) {
let childNodes = children;

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

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

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
itemsRender={(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');
});
});
});