From 16485d7311dc4d89161ee311c6454617fc619bb7 Mon Sep 17 00:00:00 2001 From: Sparkle <2990918167@qq.com> Date: Fri, 26 Jun 2026 08:55:32 +0000 Subject: [PATCH 1/5] chore: add draft pr placeholder --- docs/wip-pr-placeholder.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/wip-pr-placeholder.md diff --git a/docs/wip-pr-placeholder.md b/docs/wip-pr-placeholder.md new file mode 100644 index 00000000..e927d433 --- /dev/null +++ b/docs/wip-pr-placeholder.md @@ -0,0 +1,7 @@ +This file is a temporary placeholder for creating a Draft WIP PR for the `LegacyMenuItem` function-component migration work. + +It exists only so maintainers can associate the PR with `ant-design/ant-design#58404`. + +Planned cleanup: + +- Remove this file in the first real implementation commit, or before merge. From 5efe1bceaa7883fa197bdabdd8deebc96816930e Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Sat, 27 Jun 2026 17:00:11 +0800 Subject: [PATCH 2/5] refactor: migrate legacy menu item to fc --- src/MenuItem.tsx | 98 ++++++++++++++++++++++++++++--------------- src/utils/warnUtil.ts | 4 +- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 74882518..5fc5c0a1 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -34,40 +34,71 @@ 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 = { + props: LegacyMenuItemProps; + element: HTMLLIElement | null; +}; + +// 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 +148,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; @@ -147,7 +178,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< key: eventKey, // Note: For legacy code is reversed which not like other antd component keyPath: [...connectedKeys].reverse(), - item: legacyMenuItemRef.current, + item: legacyMenuItemRef.current as unknown as React.ReactInstance, domEvent: e, itemData: propsItemData || itemData, }; @@ -214,6 +245,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/utils/warnUtil.ts b/src/utils/warnUtil.ts index c295e1a1..b66abe2c 100644 --- a/src/utils/warnUtil.ts +++ b/src/utils/warnUtil.ts @@ -1,8 +1,8 @@ import { warning } from '@rc-component/util'; /** - * `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 { Object.defineProperty(restInfo, 'item', { From 6af2feaaa7e0882ccf78c315bd84cce720c4c98e Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Sat, 27 Jun 2026 17:00:36 +0800 Subject: [PATCH 3/5] test: cover deprecated menu item compat --- docs/wip-pr-placeholder.md | 7 ------- tests/Menu.spec.tsx | 4 +++- 2 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 docs/wip-pr-placeholder.md diff --git a/docs/wip-pr-placeholder.md b/docs/wip-pr-placeholder.md deleted file mode 100644 index e927d433..00000000 --- a/docs/wip-pr-placeholder.md +++ /dev/null @@ -1,7 +0,0 @@ -This file is a temporary placeholder for creating a Draft WIP PR for the `LegacyMenuItem` function-component migration work. - -It exists only so maintainers can associate the PR with `ant-design/ant-design#58404`. - -Planned cleanup: - -- Remove this file in the first real implementation commit, or before merge. diff --git a/tests/Menu.spec.tsx b/tests/Menu.spec.tsx index 3f629082..ec47e34d 100644 --- a/tests/Menu.spec.tsx +++ b/tests/Menu.spec.tsx @@ -444,8 +444,10 @@ describe('Menu', () => { fireEvent.click(container.querySelector('.rc-menu-item')); const info = handleClick.mock.calls[0][0]; + const legacyItem = info.item as unknown as { props: { eventKey?: string } }; expect(info.key).toBe('1'); - expect(info.item).toBeTruthy(); + expect(legacyItem).toBeTruthy(); + expect(legacyItem.props.eventKey).toBe('1'); expect(errorSpy).toHaveBeenCalledWith( 'Warning: `info.item` is deprecated since we will move to function component that not provides React Node instance in future.', From 31ea25a7a6f5ed2a237d5c73855891c813344bb8 Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Sat, 27 Jun 2026 17:26:22 +0800 Subject: [PATCH 4/5] types: align legacy menu item contract --- src/MenuItem.tsx | 7 +++---- src/interface.ts | 14 +++++++++++++- src/utils/warnUtil.ts | 3 ++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 5fc5c0a1..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 @@ -44,9 +44,8 @@ type LegacyMenuItemProps = Omit, 'tit onTitleClick?: () => void; }; -type LegacyMenuItemHandle = { +type LegacyMenuItemHandle = Omit & { props: LegacyMenuItemProps; - element: HTMLLIElement | null; }; // Keep exposing a legacy-compatible handle for deprecated `info.item`. @@ -178,7 +177,7 @@ const InternalMenuItem = React.forwardRef((props: MenuItemProps, ref: React.Ref< key: eventKey, // Note: For legacy code is reversed which not like other antd component keyPath: [...connectedKeys].reverse(), - item: legacyMenuItemRef.current as unknown as React.ReactInstance, + item: legacyMenuItemRef.current, domEvent: e, itemData: propsItemData || itemData, }; 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 b66abe2c..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` 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( From 5e50486399ab16ac57605ef29ca8468f40a2f66d Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Sat, 27 Jun 2026 17:26:28 +0800 Subject: [PATCH 5/5] test: cover legacy menu item element handle --- tests/Menu.spec.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/Menu.spec.tsx b/tests/Menu.spec.tsx index ec47e34d..66433f75 100644 --- a/tests/Menu.spec.tsx +++ b/tests/Menu.spec.tsx @@ -442,12 +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]; - const legacyItem = info.item as unknown as { props: { eventKey?: string } }; expect(info.key).toBe('1'); - expect(legacyItem).toBeTruthy(); - expect(legacyItem.props.eventKey).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.',