Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
9 changes: 9 additions & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type * as React from 'react';
import type { SubMenuProps } from './SubMenu';
import type { MenuItemProps } from './MenuItem';
import type { MenuItemGroupProps } from './MenuItemGroup';
import type { DividerProps } from './Divider';

// ========================= Options =========================
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 +144,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