diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 74882518..6c21d955 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -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 @@ -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 { - 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 ( - - ); - } -} +type LegacyMenuItemProps = Omit, 'title' | 'ref'> & { + title?: React.ReactNode; + attribute?: Record; + elementRef?: React.Ref; + eventKey?: string; + popupClassName?: string; + popupOffset?: number[]; + onTitleClick?: () => void; +}; + +type LegacyMenuItemHandle = Omit & { + 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((props, ref) => { + const { title, attribute, elementRef, ...restProps } = props; + const propsRef = React.useRef(props); + const domRef = React.useRef(null); + + propsRef.current = props; + + React.useImperativeHandle( + ref, + () => ({ + get props() { + return propsRef.current; + }, + get element() { + return domRef.current; + }, + }), + [], + ); + + // 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 ( + + ); +}); /** * Real Menu Item component */ -const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref) => { +const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref) => { const { style, className, @@ -117,7 +147,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< const itemCls = `${prefixCls}-item`; - const legacyMenuItemRef = React.useRef(); + const legacyMenuItemRef = React.useRef(null); const elementRef = React.useRef(); const mergedDisabled = contextDisabled || disabled; @@ -214,6 +244,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< ): React.ReactElement { +function MenuItem(props: MenuItemProps, ref: React.Ref): React.ReactElement { const { eventKey } = props; // ==================== Record KeyPath ==================== diff --git a/src/interface.ts b/src/interface.ts index 20d8885e..dce03bf0 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -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 | React.KeyboardEvent; itemData: ItemData; } diff --git a/src/utils/warnUtil.ts b/src/utils/warnUtil.ts index c295e1a1..bbd1f226 100644 --- a/src/utils/warnUtil.ts +++ b/src/utils/warnUtil.ts @@ -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({ item, ...restInfo }: T): T { +export function warnItemProp({ item, ...restInfo }: T): T { Object.defineProperty(restInfo, 'item', { get: () => { warning( diff --git a/tests/Menu.spec.tsx b/tests/Menu.spec.tsx index 3f629082..66433f75 100644 --- a/tests/Menu.spec.tsx +++ b/tests/Menu.spec.tsx @@ -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.',