diff --git a/components/menu/demo/horizontal.vue b/components/menu/demo/horizontal.vue index 9038f81d9..19cf9af9b 100644 --- a/components/menu/demo/horizontal.vue +++ b/components/menu/demo/horizontal.vue @@ -22,8 +22,9 @@ Horizontal top navigation menu. <script lang="ts" setup> import { h, ref } from 'vue'; import { MailOutlined, AppstoreOutlined, SettingOutlined } from '@ant-design/icons-vue'; +import { MenuProps } from 'ant-design-vue'; const current = ref<string[]>(['mail']); -const items = ref([ +const items = ref<MenuProps['items']>([ { key: 'mail', icon: () => h(MailOutlined), diff --git a/components/menu/index.en-US.md b/components/menu/index.en-US.md index b497abab0..69d319671 100644 --- a/components/menu/index.en-US.md +++ b/components/menu/index.en-US.md @@ -66,14 +66,14 @@ More layouts with navigation: [Layout](/components/layout). #### MenuItemType -| Param | Description | Type | Default value | Version | -| -------- | ------------------------------------ | ------- | ------------- | ------- | -| danger | Display the danger style | boolean | false | | -| disabled | Whether menu item is disabled | boolean | false | | -| icon | The icon of the menu item | VueNode | - | | -| key | Unique ID of the menu item | string | - | | -| label | Menu label | VueNode | - | | -| title | Set display title for collapsed item | string | - | | +| Param | Description | Type | Default value | Version | +| --- | --- | --- | --- | --- | +| danger | Display the danger style | boolean | false | | +| disabled | Whether menu item is disabled | boolean | false | | +| icon | The icon of the menu item | VueNode \| (item: MenuItemType) => VNode | - | | +| key | Unique ID of the menu item | string | - | | +| label | Menu label | VueNode | - | | +| title | Set display title for collapsed item | string | - | | #### SubMenuType @@ -82,7 +82,7 @@ More layouts with navigation: [Layout](/components/layout). | --- | --- | --- | --- | --- | | children | Sub-menus or sub-menu items | [ItemType\[\]](#ItemType) | - | | | disabled | Whether sub-menu is disabled | boolean | false | | -| icon | Icon of sub menu | VueNode\|()=>VueNode | - | | +| icon | Icon of sub menu | VueNode \| (item: SubMenuType) => VueNode | - | | | key | Unique ID of the sub-menu | string | - | | | label | Menu label | VueNode | - | | | popupClassName | Sub-menu class name, not working when `mode="inline"` | string | - | | diff --git a/components/menu/index.zh-CN.md b/components/menu/index.zh-CN.md index ecc0f3908..08202d2ba 100644 --- a/components/menu/index.zh-CN.md +++ b/components/menu/index.zh-CN.md @@ -67,14 +67,14 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*Vn4XSqJFAxcAAA ### MenuItemType -| 参数 | 说明 | 类型 | 默认值 | 版本 | -| -------- | ------------------------ | -------------------- | ------ | ---- | -| danger | 展示错误状态样式 | boolean | false | | -| disabled | 是否禁用 | boolean | false | | -| icon | 菜单图标 | VueNode\|()=>VueNode | - | | -| key | item 的唯一标志 | string | - | | -| label | 菜单项标题 | VueNode | - | | -| title | 设置收缩时展示的悬浮标题 | string | - | | +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| -------- | ------------------------ | -------------------------------------- | ------ | ---- | +| danger | 展示错误状态样式 | boolean | false | | +| disabled | 是否禁用 | boolean | false | | +| icon | 菜单图标 | VueNode\|(item: MenuItemType)=>VueNode | - | | +| key | item 的唯一标志 | string | - | | +| label | 菜单项标题 | VueNode | - | | +| title | 设置收缩时展示的悬浮标题 | string | - | | #### SubMenuType @@ -82,7 +82,7 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*Vn4XSqJFAxcAAA | --- | --- | --- | --- | --- | | children | 子菜单的菜单项 | [ItemType\[\]](#ItemType) | - | | | disabled | 是否禁用 | boolean | false | | -| icon | 菜单图标 | VueNode\|()=>VueNode | - | | +| icon | 菜单图标 | VueNode\|(item: SubMenuType)=>VueNode | - | | | key | 唯一标志 | string | - | | | label | 菜单项标题 | VueNode | - | | | popupClassName | 子菜单样式,`mode="inline"` 时无效 | string | - | | diff --git a/components/menu/src/ItemGroup.tsx b/components/menu/src/ItemGroup.tsx index 875decd58..970ccfe8d 100644 --- a/components/menu/src/ItemGroup.tsx +++ b/components/menu/src/ItemGroup.tsx @@ -4,9 +4,13 @@ import { computed, defineComponent } from 'vue'; import PropTypes from '../../_util/vue-types'; import { useInjectMenu } from './hooks/useMenuContext'; import { useMeasure } from './hooks/useKeyPath'; +import type { ItemType } from './interface'; +import { objectType } from '../../_util/type'; export const menuItemGroupProps = () => ({ title: PropTypes.any, + // Internal user prop + originItemValue: objectType<ItemType>(), }); export type MenuItemGroupProps = Partial<ExtractPropTypes<ReturnType<typeof menuItemGroupProps>>>; diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx index 1c459683a..ce741a433 100644 --- a/components/menu/src/MenuItem.tsx +++ b/components/menu/src/MenuItem.tsx @@ -1,4 +1,4 @@ -import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util'; +import { flattenChildren, isValidElement } from '../../_util/props-util'; import PropTypes from '../../_util/vue-types'; import type { ExtractPropTypes, PropType } from 'vue'; import { @@ -13,12 +13,13 @@ import { useInjectKeyPath, useMeasure } from './hooks/useKeyPath'; import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext'; import { cloneElement } from '../../_util/vnode'; import Tooltip from '../../tooltip'; -import type { MenuInfo } from './interface'; +import type { ItemType, MenuInfo } from './interface'; import KeyCode from '../../_util/KeyCode'; import useDirectionStyle from './hooks/useDirectionStyle'; import Overflow from '../../vc-overflow'; import devWarning from '../../vc-util/devWarning'; import type { MouseEventHandler } from '../../_util/EventInterface'; +import { objectType } from '../../_util/type'; let indexGuid = 0; export const menuItemProps = () => ({ @@ -33,6 +34,8 @@ export const menuItemProps = () => ({ onClick: Function as PropType<MouseEventHandler>, onKeydown: Function as PropType<MouseEventHandler>, onFocus: Function as PropType<MouseEventHandler>, + // Internal user prop + originItemValue: objectType<ItemType>(), }); export type MenuItemProps = Partial<ExtractPropTypes<ReturnType<typeof menuItemProps>>>; @@ -215,7 +218,7 @@ export default defineComponent({ optionRoleProps['aria-selected'] = selected.value; } - const icon = getPropsSlot(slots, props, 'icon'); + const icon = props.icon ?? slots.icon?.(props); return ( <Tooltip {...tooltipProps} @@ -248,7 +251,7 @@ export default defineComponent({ title={typeof title === 'string' ? title : undefined} > {cloneElement( - typeof icon === 'function' ? icon() : icon, + typeof icon === 'function' ? icon(props.originItemValue) : icon, { class: `${prefixCls.value}-item-icon`, }, diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index b9bae5b87..54c94fc1a 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -28,7 +28,8 @@ import devWarning from '../../vc-util/devWarning'; import isValid from '../../_util/isValid'; import type { MouseEventHandler } from '../../_util/EventInterface'; import type { Key } from '../../_util/type'; -import type { MenuTheme } from './interface'; +import { objectType } from '../../_util/type'; +import type { ItemType, MenuTheme } from './interface'; let indexGuid = 0; @@ -46,6 +47,9 @@ export const subMenuProps = () => ({ onMouseenter: Function as PropType<MouseEventHandler>, onMouseleave: Function as PropType<MouseEventHandler>, onTitleClick: Function as PropType<(e: MouseEvent, key: Key) => void>, + + // Internal user prop + originItemValue: objectType<ItemType>(), }); export type SubMenuProps = Partial<ExtractPropTypes<ReturnType<typeof subMenuProps>>>; @@ -224,7 +228,7 @@ export default defineComponent({ return ( <> {cloneElement( - typeof icon === 'function' ? icon() : icon, + typeof icon === 'function' ? icon(props.originItemValue) : icon, { class: `${prefixCls.value}-item-icon`, }, @@ -247,7 +251,7 @@ export default defineComponent({ ); const baseTitleNode = () => { const subMenuPrefixClsValue = subMenuPrefixCls.value; - const icon = getPropsSlot(slots, props, 'icon'); + const icon = props.icon ?? slots.icon?.(props); const expandIcon = props.expandIcon || slots.expandIcon || menuExpandIcon.value; const title = renderTitle(getPropsSlot(slots, props, 'title'), icon); return ( diff --git a/components/menu/src/hooks/useItems.tsx b/components/menu/src/hooks/useItems.tsx index b49127f9d..322493b66 100644 --- a/components/menu/src/hooks/useItems.tsx +++ b/components/menu/src/hooks/useItems.tsx @@ -9,19 +9,19 @@ import ItemGroup from '../ItemGroup'; import MenuDivider from '../Divider'; import MenuItem from '../MenuItem'; import type { Key } from '../../../_util/type'; +import type { VNode } from 'vue'; import { ref, shallowRef, watch } from 'vue'; import type { MenuProps } from '../Menu'; import type { StoreMenuInfo } from './useMenuContext'; export interface MenuItemType extends VcMenuItemType { danger?: boolean; - icon?: any; + icon?: VNode | ((item: MenuItemType) => VNode); title?: string; } export interface SubMenuType extends Omit<VcSubMenuType, 'children'> { - icon?: any; - theme?: 'dark' | 'light'; + icon?: VNode | ((item: SubMenuType) => VNode); children: ItemType[]; } @@ -69,7 +69,7 @@ function convertItemsToNodes( const childrenNodes = convertItemsToNodes(children, store, parentMenuInfo); // Group return ( - <ItemGroup key={mergedKey} {...restProps} title={label}> + <ItemGroup key={mergedKey} {...restProps} title={label} originItemValue={opt}> {childrenNodes} </ItemGroup> ); @@ -84,7 +84,7 @@ function convertItemsToNodes( parentKeys: [].concat(parentKeys, mergedKey), }); return ( - <SubMenu key={mergedKey} {...restProps} title={label}> + <SubMenu key={mergedKey} {...restProps} title={label} originItemValue={opt}> {childrenNodes} </SubMenu> ); @@ -97,7 +97,7 @@ function convertItemsToNodes( menuInfo.isLeaf = true; store.set(mergedKey, menuInfo); return ( - <MenuItem key={mergedKey} {...restProps}> + <MenuItem key={mergedKey} {...restProps} originItemValue={opt}> {label} </MenuItem> );