From a60d55ef10be46d79c0e9b32c0ed1fe3ca797176 Mon Sep 17 00:00:00 2001 From: yangyuanxx <114125354+yangyuanxx@users.noreply.github.com> Date: Wed, 10 May 2023 10:20:17 +0800 Subject: [PATCH] chore: improved support for generics MenuItemType and MenuItemProps in the Menu component (#42240) * chore: improved support for generics MenuItemType and MenuItemProps in the Menu component * docs: update codesandbox link (#42238) * chore: improved support for generics MenuItemType and MenuItemProps in the Menu component * chore: add typing test to Menu --------- Co-authored-by: yangyuan Co-authored-by: afc163 --- components/menu/MenuItem.tsx | 19 ++++++- components/menu/__tests__/type.test.tsx | 70 ++++++++++++++++++++++++- components/menu/hooks/useItems.tsx | 17 ++++-- components/menu/index.tsx | 23 ++++++-- 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/components/menu/MenuItem.tsx b/components/menu/MenuItem.tsx index aa1273e2fa..46240759a8 100644 --- a/components/menu/MenuItem.tsx +++ b/components/menu/MenuItem.tsx @@ -18,7 +18,24 @@ export interface MenuItemProps extends Omit { title?: React.ReactNode; } -const MenuItem: React.FC = (props) => { +type MenuItemComponent = React.FC; + +type RestArgs = T extends (arg: any, ...args: infer P) => any ? P : never; + +type GenericProps = T extends infer U extends MenuItemProps + ? unknown extends U + ? MenuItemProps + : U + : MenuItemProps; + +interface GenericComponent extends Omit { + ( + props: GenericProps, + ...args: RestArgs + ): ReturnType; +} + +const MenuItem: GenericComponent = (props) => { const { className, children, icon, title, danger } = props; const { prefixCls, diff --git a/components/menu/__tests__/type.test.tsx b/components/menu/__tests__/type.test.tsx index 0d5b0b494f..631e99d00e 100644 --- a/components/menu/__tests__/type.test.tsx +++ b/components/menu/__tests__/type.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import Menu from '..'; +import Menu, { type MenuItemProps } from '..'; +import type { MenuItemType } from '../hooks/useItems'; describe('Menu.typescript', () => { it('Menu.items', () => { @@ -37,4 +38,71 @@ describe('Menu.typescript', () => { expect(menu).toBeTruthy(); }); + + it('Menu.items should accept custom item type', () => { + interface CustomItemType extends MenuItemType { + 'data-x': number; + } + + const menu = ( + + items={[ + { key: 'item', title: 'Item', 'data-x': 0 }, + { + key: 'submenu', + theme: 'light', + children: [ + { key: 'submenu-item', title: 'SubmenuItem', 'data-x': 0 }, + { key: 'submenu-submenu', theme: 'light', children: [], 'data-x': 0 }, + { key: 'submenu-divider', type: 'divider' }, + { key: 'submenu-group', type: 'group' }, + null, + ], + }, + { + key: 'group', + type: 'group', + children: [ + { key: 'group-item', label: 'GroupItem', 'data-x': 0 }, + { key: 'group-submenu', theme: 'light', children: [], 'data-x': 0 }, + { key: 'group-divider', type: 'divider' }, + { key: 'group-group', type: 'group' }, + null, + ], + }, + { key: 'divider', type: 'divider' }, + null, + ]} + /> + ); + + expect(menu).toBeTruthy(); + }); + + it('MenuItem.props should accept custom props', () => { + interface CustomItemProps extends MenuItemProps { + 'data-x': number; + } + + const menu = ( + + key="item" title="Item" data-x={0} /> + + key="submenu-item" title="SubmenuItem" data-x={0} /> + + + + + + key="group-item" title="GroupItem" data-x={0} /> + + + + + + + ); + + expect(menu).toBeTruthy(); + }); }); diff --git a/components/menu/hooks/useItems.tsx b/components/menu/hooks/useItems.tsx index dbffab2467..33ab721d8c 100644 --- a/components/menu/hooks/useItems.tsx +++ b/components/menu/hooks/useItems.tsx @@ -16,14 +16,16 @@ export interface MenuItemType extends RcMenuItemType { title?: string; } -export interface SubMenuType extends Omit { +export interface SubMenuType + extends Omit { icon?: React.ReactNode; theme?: 'dark' | 'light'; - children: ItemType[]; + children: ItemType[]; } -export interface MenuItemGroupType extends Omit { - children?: ItemType[]; +export interface MenuItemGroupType + extends Omit { + children?: ItemType[]; key?: React.Key; } @@ -32,7 +34,12 @@ export interface MenuDividerType extends RcMenuDividerType { key?: React.Key; } -export type ItemType = MenuItemType | SubMenuType | MenuItemGroupType | MenuDividerType | null; +export type ItemType = + | T + | SubMenuType + | MenuItemGroupType + | MenuDividerType + | null; function convertItemsToNodes(list: ItemType[]) { return (list || []) diff --git a/components/menu/index.tsx b/components/menu/index.tsx index 4c30d2edc6..f6e80ddc57 100644 --- a/components/menu/index.tsx +++ b/components/menu/index.tsx @@ -9,6 +9,7 @@ import type { MenuTheme } from './MenuContext'; import MenuDivider from './MenuDivider'; import Item, { type MenuItemProps } from './MenuItem'; import SubMenu, { type SubMenuProps } from './SubMenu'; +import type { ItemType, MenuItemType } from './hooks/useItems'; export type { MenuItemGroupProps } from 'rc-menu'; export type { MenuDividerProps } from './MenuDivider'; @@ -19,15 +20,29 @@ export type MenuRef = { focus: (options?: FocusOptions) => void; }; -type CompoundedComponent = React.ForwardRefExoticComponent< - MenuProps & React.RefAttributes -> & { +type ComponentProps = MenuProps & React.RefAttributes; + +type GenericItemType = T extends infer U extends MenuItemType + ? unknown extends U + ? ItemType + : ItemType + : ItemType; + +type GenericComponentProps = Omit & { + items?: GenericItemType[]; +}; + +type CompoundedComponent = React.ForwardRefExoticComponent & { Item: typeof Item; SubMenu: typeof SubMenu; Divider: typeof MenuDivider; ItemGroup: typeof ItemGroup; }; +interface GenericComponent extends Omit { + (props: GenericComponentProps): ReturnType; +} + const Menu = forwardRef((props, ref) => { const menuRef = useRef(null); const context = React.useContext(SiderContext); @@ -39,7 +54,7 @@ const Menu = forwardRef((props, ref) => { }, })); return ; -}) as CompoundedComponent; +}) as GenericComponent; Menu.Item = Item; Menu.SubMenu = SubMenu;