Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
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;

itemRender?: (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,

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, itemRender],
);

const [mounted, setMounted] = React.useState(false);
Expand Down
10 changes: 8 additions & 2 deletions src/utils/commonUtil.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}

Expand Down
56 changes: 32 additions & 24 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.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, itemRender)}
</MergedMenuItemGroup>
);
} else {
originNode = (
<MergedSubMenu key={mergedKey} {...restProps} title={label}>
{convertItemsToNodes(children, components, prefixCls, itemRender)}
</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 itemRender === 'function') {
return itemRender(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,
itemRender?: (originNode: React.ReactNode, item?: NonNullable<ItemType>) => React.ReactNode,
) {
let childNodes = children;

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

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

return parseChildren(childNodes, keyPath);
return parseChildren(childNodes, keyPath, itemRender);
}
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');
});
});
});
4 changes: 2 additions & 2 deletions tests/Responsive.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jest.mock('rc-resize-observer', () => {

describe('Menu.Responsive', () => {
beforeEach(() => {
global.resizeProps = null;
global.resizeProps = new Map<number, any>();
jest.useFakeTimers();
});

Expand Down Expand Up @@ -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();
Expand Down