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
97 changes: 64 additions & 33 deletions src/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import PrivateContext from './context/PrivateContext';
import useActive from './hooks/useActive';
import useDirectionStyle from './hooks/useDirectionStyle';
import Icon from './Icon';
import type { MenuInfo, ItemData, MenuItemType } from './interface';
import type { LegacyMenuItemInfo, MenuInfo, ItemData, MenuItemType } from './interface';
import { warnItemProp } from './utils/warnUtil';

export interface MenuItemProps
Expand All @@ -34,40 +34,70 @@ export interface MenuItemProps
itemData?: ItemData;
}

// Since Menu event provide the `info.item` which point to the MenuItem node instance.
// We have to use class component here.
// This should be removed from doc & api in future.
class LegacyMenuItem extends React.Component<any> {
render() {
const { title, attribute, elementRef, ...restProps } = this.props;

// Here the props are eventually passed to the DOM element.
// React does not recognize non-standard attributes.
// Therefore, remove the props that is not used here.
// ref: https://github.com/ant-design/ant-design/issues/41395
const passedProps = omit(restProps, [
'eventKey',
'popupClassName',
'popupOffset',
'onTitleClick',
]);
warning(!attribute, '`attribute` of Menu.Item is deprecated. Please pass attribute directly.');

return (
<Overflow.Item
{...attribute}
title={typeof title === 'string' ? title : undefined}
{...passedProps}
ref={elementRef}
/>
);
}
}
type LegacyMenuItemProps = Omit<React.ComponentProps<typeof Overflow.Item>, 'title' | 'ref'> & {
title?: React.ReactNode;
attribute?: Record<string, string>;
elementRef?: React.Ref<HTMLLIElement>;
eventKey?: string;
popupClassName?: string;
popupOffset?: number[];
onTitleClick?: () => void;
};

type LegacyMenuItemHandle = Omit<LegacyMenuItemInfo, 'props'> & {
props: LegacyMenuItemProps;
};

// Keep exposing a legacy-compatible handle for deprecated `info.item`.
// This should be removed together with the deprecated API in future.
const LegacyMenuItem = React.forwardRef<LegacyMenuItemHandle, LegacyMenuItemProps>((props, ref) => {
const { title, attribute, elementRef, ...restProps } = props;
const propsRef = React.useRef(props);
const domRef = React.useRef<HTMLLIElement>(null);

propsRef.current = props;

React.useImperativeHandle(
ref,
() => ({
get props() {
return propsRef.current;
},
get element() {
return domRef.current;
},
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
[],
);

// Here the props are eventually passed to the DOM element.
// React does not recognize non-standard attributes.
// Therefore, remove the props that is not used here.
// ref: https://github.com/ant-design/ant-design/issues/41395
const passedProps = omit(restProps, [
'eventKey',
'popupClassName',
'popupOffset',
'onTitleClick',
]);
const mergedRef = useComposeRef(elementRef, domRef);

warning(!attribute, '`attribute` of Menu.Item is deprecated. Please pass attribute directly.');

return (
<Overflow.Item
{...attribute}
title={typeof title === 'string' ? title : undefined}
{...passedProps}
ref={mergedRef}
/>
);
});

/**
* Real Menu Item component
*/
const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<HTMLElement>) => {
const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<HTMLLIElement>) => {
const {
style,
className,
Expand Down Expand Up @@ -117,7 +147,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<

const itemCls = `${prefixCls}-item`;

const legacyMenuItemRef = React.useRef<any>();
const legacyMenuItemRef = React.useRef<LegacyMenuItemHandle | null>(null);
const elementRef = React.useRef<HTMLLIElement>();
const mergedDisabled = contextDisabled || disabled;

Expand Down Expand Up @@ -214,6 +244,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<
<LegacyMenuItem
ref={legacyMenuItemRef}
elementRef={mergedEleRef}
eventKey={eventKey}
role={role === null ? 'none' : role || 'menuitem'}
tabIndex={disabled ? null : -1}
data-menu-id={overflowDisabled && domDataId ? null : domDataId}
Expand Down Expand Up @@ -257,7 +288,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref<
return renderNode;
});

function MenuItem(props: MenuItemProps, ref: React.Ref<HTMLElement>): React.ReactElement {
function MenuItem(props: MenuItemProps, ref: React.Ref<HTMLLIElement>): React.ReactElement {
const { eventKey } = props;

// ==================== Record KeyPath ====================
Expand Down
14 changes: 13 additions & 1 deletion src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,23 @@ export interface RenderIconInfo {

export type RenderIconType = React.ReactNode | ((props: RenderIconInfo) => React.ReactNode);

/**
* @deprecated Compatibility handle for deprecated `info.item`.
* Avoid relying on this shape since it will be removed in a future major version.
*/
export interface LegacyMenuItemInfo {
props: {
eventKey?: string;
[key: string]: unknown;
};
element: HTMLLIElement | null;
}

export interface MenuInfo {
key: string;
keyPath: string[];
/** @deprecated This will not support in future. You should avoid to use this */
item: React.ReactInstance;
item: LegacyMenuItemInfo;
domEvent: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>;
itemData: ItemData;
}
Expand Down
7 changes: 4 additions & 3 deletions src/utils/warnUtil.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { warning } from '@rc-component/util';
import type { LegacyMenuItemInfo } from '../interface';

/**
* `onClick` event return `info.item` which point to react node directly.
* We should warning this since it will not work on FC.
* `onClick` still exposes deprecated `info.item` for backward compatibility.
* Keep warning since function components no longer provide a React node instance.
*/
export function warnItemProp<T extends { item: React.ReactInstance }>({ item, ...restInfo }: T): T {
export function warnItemProp<T extends { item: LegacyMenuItemInfo }>({ item, ...restInfo }: T): T {
Object.defineProperty(restInfo, 'item', {
get: () => {
warning(
Expand Down
5 changes: 4 additions & 1 deletion tests/Menu.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -442,10 +442,13 @@ describe('Menu', () => {
jest.runAllTimers();
});

fireEvent.click(container.querySelector('.rc-menu-item'));
const firstItem = container.querySelector('.rc-menu-item') as HTMLLIElement;
fireEvent.click(firstItem);
const info = handleClick.mock.calls[0][0];
expect(info.key).toBe('1');
expect(info.item).toBeTruthy();
expect(info.item.props.eventKey).toBe('1');
expect(info.item.element).toBe(firstItem);

expect(errorSpy).toHaveBeenCalledWith(
'Warning: `info.item` is deprecated since we will move to function component that not provides React Node instance in future.',
Expand Down
Loading