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
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
20 changes: 17 additions & 3 deletions src/Divider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 } = 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={classNames(`${prefixCls}-item-divider`, className)}
style={style}
/>
);

const mergedItemRender = propItemRender || contextItemRender;

if (typeof mergedItemRender === 'function') {
return mergedItemRender(renderNode, {
item: { type: 'divider', ...props },
Copy link
Member

Choose a reason for hiding this comment

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

不能用 props,这个和 Item 的数据不等价。应该直接拿 item 的数据传入。

Copy link
Member

Choose a reason for hiding this comment

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

其他几个也看看类似的问题~

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 @@ -655,6 +661,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
20 changes: 18 additions & 2 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,8 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<

onFocus,

itemRender: propItemRender,

...restProps
} = props;

Expand All @@ -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`;
Expand Down Expand Up @@ -198,7 +204,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 @@ -238,6 +244,16 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<
</LegacyMenuItem>
);

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 });
}
Expand Down
16 changes: 13 additions & 3 deletions src/MenuItemGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<MenuItemGroupType, 'type' | 'children' | 'label'> {
Expand Down Expand Up @@ -52,18 +52,28 @@ const InternalMenuItemGroup = React.forwardRef<HTMLLIElement, MenuItemGroupProps
});

const MenuItemGroup = React.forwardRef<HTMLLIElement, MenuItemGroupProps>((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 (
<InternalMenuItemGroup ref={ref} {...omit(props, ['warnKey'])}>
{childList}
{typeof mergedItemRender === 'function'
? mergedItemRender(childList, {
item: {
type: 'group',
...props,
} as ItemType,
keys: connectedKeyPath,
})
: childList}
</InternalMenuItemGroup>
);
});
Expand Down
20 changes: 16 additions & 4 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,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,24 @@ const SubMenu = React.forwardRef<HTMLLIElement, SubMenuProps>((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 = (
<InternalSubMenu ref={ref} {...props}>
{childList}
{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
6 changes: 6 additions & 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?: ItemRenderType;
}

export interface SubMenuType extends ItemSharedProps {
Expand Down Expand Up @@ -140,3 +141,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
4 changes: 1 addition & 3 deletions src/utils/nodeUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ function convertItemsToNodes(
return (
<MergedMenuItem key={mergedKey} {...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
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');
});
});
});
Loading