diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index dbcc976b4..8e8be9fa3 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -1,6 +1,7 @@ import type { Key } from '../../_util/type'; import type { ExtractPropTypes, PropType, VNode } from 'vue'; import { + shallowRef, Teleport, computed, defineComponent, @@ -38,10 +39,14 @@ import { cloneElement } from '../../_util/vnode'; import { OVERFLOW_KEY, PathContext } from './hooks/useKeyPath'; import type { FocusEventHandler, MouseEventHandler } from '../../_util/EventInterface'; import collapseMotion from '../../_util/collapseMotion'; +import type { ItemType } from './hooks/useItems'; +import useItems from './hooks/useItems'; export const menuProps = () => ({ id: String, prefixCls: String, + // donot use items, now only support inner use + items: Array as PropType, disabled: Boolean, inlineCollapsed: Boolean, disabledOverflow: Boolean, @@ -90,7 +95,7 @@ export default defineComponent({ slots: ['expandIcon', 'overflowedIndicator'], setup(props, { slots, emit, attrs }) { const { prefixCls, direction, getPrefixCls } = useConfigInject('menu', props); - const store = ref>({}); + const store = shallowRef>(new Map()); const siderCollapsed = inject(SiderCollapsedKey, ref(undefined)); const inlineCollapsed = computed(() => { if (siderCollapsed.value !== undefined) { @@ -98,7 +103,7 @@ export default defineComponent({ } return props.inlineCollapsed; }); - + const { itemsNodes } = useItems(props); const isMounted = ref(false); onMounted(() => { isMounted.value = true; @@ -115,6 +120,11 @@ export default defineComponent({ 'Menu', '`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.', ); + // devWarning( + // !!props.items && !slots.default, + // 'Menu', + // '`children` will be removed in next major version. Please use `items` instead.', + // ); }); const activeKeys = ref([]); @@ -124,7 +134,7 @@ export default defineComponent({ store, () => { const newKeyMapStore = {}; - for (const menuInfo of Object.values(store.value)) { + for (const menuInfo of store.value.values()) { newKeyMapStore[menuInfo.key] = menuInfo; } keyMapStore.value = newKeyMapStore; @@ -322,8 +332,8 @@ export default defineComponent({ const keys = []; const storeValue = store.value; eventKeys.forEach(eventKey => { - const { key, childrenEventKeys } = storeValue[eventKey]; - keys.push(key, ...getChildrenKeys(childrenEventKeys)); + const { key, childrenEventKeys } = storeValue.get(eventKey); + keys.push(key, ...getChildrenKeys(unref(childrenEventKeys))); }); return keys; }; @@ -355,11 +365,12 @@ export default defineComponent({ }; const registerMenuInfo = (key: string, info: StoreMenuInfo) => { - store.value = { ...store.value, [key]: info as any }; + store.value.set(key, info); + store.value = new Map(store.value); }; const unRegisterMenuInfo = (key: string) => { - delete store.value[key]; - store.value = { ...store.value }; + store.value.delete(key); + store.value = new Map(store.value); }; const lastVisibleIndex = ref(0); @@ -379,7 +390,6 @@ export default defineComponent({ : null, ); useProvideMenu({ - store, prefixCls, activeKeys, openKeys: mergedOpenKeys, @@ -408,9 +418,10 @@ export default defineComponent({ isRootMenu: ref(true), expandIcon, forceSubMenuRender: computed(() => props.forceSubMenuRender), + rootClassName: computed(() => ''), }); return () => { - const childList = flattenChildren(slots.default?.()); + const childList = itemsNodes.value || flattenChildren(slots.default?.()); const allVisible = lastVisibleIndex.value >= childList.length - 1 || mergedMode.value !== 'horizontal' || diff --git a/components/menu/src/PopupTrigger.tsx b/components/menu/src/PopupTrigger.tsx index 09996d427..661e9f11d 100644 --- a/components/menu/src/PopupTrigger.tsx +++ b/components/menu/src/PopupTrigger.tsx @@ -42,6 +42,7 @@ export default defineComponent({ forceSubMenuRender, motion, defaultMotions, + rootClassName, } = useInjectMenu(); const forceRender = useInjectForceRender(); const placement = computed(() => @@ -86,6 +87,7 @@ export default defineComponent({ [`${prefixCls}-rtl`]: rtl.value, }, popupClassName, + rootClassName.value, )} stretch={mode === 'horizontal' ? 'minWidth' : null} getPopupContainer={ diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index 3eb4269f8..e0e332614 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -21,6 +21,7 @@ import devWarning from '../../vc-util/devWarning'; import isValid from '../../_util/isValid'; import type { MouseEventHandler } from '../../_util/EventInterface'; import type { Key } from 'ant-design-vue/es/_util/type'; +import type { MenuTheme } from './interface'; let indexGuid = 0; @@ -34,6 +35,7 @@ export const subMenuProps = () => ({ internalPopupClose: Boolean, eventKey: String, expandIcon: Function as PropType<(p?: { isOpen: boolean; [key: string]: any }) => any>, + theme: String as PropType, onMouseenter: Function as PropType, onMouseleave: Function as PropType, onTitleClick: Function as PropType<(e: MouseEvent, key: Key) => void>, @@ -193,7 +195,7 @@ export default defineComponent({ const popupClassName = computed(() => classNames( prefixCls.value, - `${prefixCls.value}-${antdMenuTheme.value}`, + `${prefixCls.value}-${props.theme || antdMenuTheme.value}`, props.popupClassName, ), ); diff --git a/components/menu/src/hooks/useItems.tsx b/components/menu/src/hooks/useItems.tsx new file mode 100644 index 000000000..a140f1aff --- /dev/null +++ b/components/menu/src/hooks/useItems.tsx @@ -0,0 +1,136 @@ +import type { + MenuItemType as VcMenuItemType, + MenuDividerType as VcMenuDividerType, + SubMenuType as VcSubMenuType, + MenuItemGroupType as VcMenuItemGroupType, +} from '../interface'; +import SubMenu from '../SubMenu'; +import ItemGroup from '../ItemGroup'; +import MenuDivider from '../Divider'; +import MenuItem from '../MenuItem'; +import type { Key } from '../../../_util/type'; +import { ref, shallowRef, watch } from 'vue'; +import type { MenuProps } from '../Menu'; +import type { StoreMenuInfo } from './useMenuContext'; + +interface MenuItemType extends VcMenuItemType { + danger?: boolean; + icon?: any; + title?: string; +} + +interface SubMenuType extends Omit { + icon?: any; + theme?: 'dark' | 'light'; + children: ItemType[]; +} + +interface MenuItemGroupType extends Omit { + children?: MenuItemType[]; + key?: Key; +} + +interface MenuDividerType extends VcMenuDividerType { + dashed?: boolean; + key?: Key; +} + +export type ItemType = MenuItemType | SubMenuType | MenuItemGroupType | MenuDividerType | null; + +function convertItemsToNodes( + list: ItemType[], + store: Map, + parentMenuInfo?: { + childrenEventKeys: string[]; + parentKeys: string[]; + }, +) { + return (list || []) + .map((opt, index) => { + if (opt && typeof opt === 'object') { + const { label, children, key, type, ...restProps } = opt as any; + const mergedKey = key ?? `tmp-${index}`; + // 此处 eventKey === key, 移除 children 后可以移除 eventKey + const parentKeys = parentMenuInfo ? parentMenuInfo.parentKeys.slice() : []; + const childrenEventKeys = []; + // if + const menuInfo = { + eventKey: mergedKey, + key: mergedKey, + parentEventKeys: ref(parentKeys), + parentKeys: ref(parentKeys), + childrenEventKeys: ref(childrenEventKeys), + isLeaf: false, + }; + + // MenuItemGroup & SubMenuItem + if (children || type === 'group') { + if (type === 'group') { + const childrenNodes = convertItemsToNodes(children, store, parentMenuInfo); + // Group + return ( + + {childrenNodes} + + ); + } + store.set(mergedKey, menuInfo); + if (parentMenuInfo) { + parentMenuInfo.childrenEventKeys.push(mergedKey); + } + // Sub Menu + const childrenNodes = convertItemsToNodes(children, store, { + childrenEventKeys, + parentKeys: [].concat(parentKeys, mergedKey), + }); + return ( + + {childrenNodes} + + ); + } + + // MenuItem & Divider + if (type === 'divider') { + return ; + } + menuInfo.isLeaf = true; + store.set(mergedKey, menuInfo); + return ( + + {label} + + ); + } + + return null; + }) + .filter(opt => opt); +} + +// FIXME: Move logic here in v4 +/** + * We simply convert `items` to VueNode for reuse origin component logic. But we need move all the + * logic from component into this hooks when in v4 + */ +export default function useItems(props: MenuProps) { + const itemsNodes = shallowRef([]); + const hasItmes = ref(false); + const store = shallowRef>(new Map()); + watch( + () => props.items, + () => { + const newStore = new Map(); + hasItmes.value = false; + if (props.items) { + hasItmes.value = true; + itemsNodes.value = convertItemsToNodes(props.items as ItemType[], newStore); + } else { + itemsNodes.value = undefined; + } + store.value = newStore; + }, + { immediate: true, deep: true }, + ); + return { itemsNodes, store, hasItmes }; +} diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 81bd8c76d..fc4cfdd37 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -1,5 +1,5 @@ import type { Key } from '../../../_util/type'; -import type { ComputedRef, InjectionKey, PropType, Ref, UnwrapRef } from 'vue'; +import type { ComputedRef, InjectionKey, PropType, Ref } from 'vue'; import { defineComponent, inject, provide, toRef } from 'vue'; import type { BuiltinPlacements, @@ -13,15 +13,14 @@ import type { CSSMotionProps } from '../../../_util/transition'; export interface StoreMenuInfo { eventKey: string; key: Key; - parentEventKeys: ComputedRef; + parentEventKeys: Ref; childrenEventKeys?: Ref; isLeaf?: boolean; - parentKeys: ComputedRef; + parentKeys: Ref; } export interface MenuContextProps { isRootMenu: Ref; - - store: Ref>>; + rootClassName: Ref; registerMenuInfo: (key: string, info: StoreMenuInfo) => void; unRegisterMenuInfo: (key: string) => void; prefixCls: ComputedRef; diff --git a/components/menu/src/interface.ts b/components/menu/src/interface.ts index 19afc7116..137b67039 100644 --- a/components/menu/src/interface.ts +++ b/components/menu/src/interface.ts @@ -1,6 +1,74 @@ +import type { CSSProperties } from 'vue'; import type { Key } from '../../_util/type'; import type { MenuItemProps } from './MenuItem'; +// ========================= Options ========================= +interface ItemSharedProps { + style?: CSSProperties; + class?: string; +} + +export interface SubMenuType extends ItemSharedProps { + label?: any; + + children: ItemType[]; + + disabled?: boolean; + + key: string; + + rootClassName?: string; + + // >>>>> Icon + itemIcon?: RenderIconType; + expandIcon?: RenderIconType; + + // >>>>> Active + onMouseenter?: MenuHoverEventHandler; + onMouseleave?: MenuHoverEventHandler; + + // >>>>> Popup + popupClassName?: string; + popupOffset?: number[]; + + // >>>>> Events + onClick?: MenuClickEventHandler; + onTitleClick?: (info: MenuTitleInfo) => void; + onTitleMouseenter?: MenuHoverEventHandler; + onTitleMouseleave?: MenuHoverEventHandler; +} + +export interface MenuItemType extends ItemSharedProps { + label?: any; + + disabled?: boolean; + + itemIcon?: RenderIconType; + + key: Key; + + // >>>>> Active + onMouseenter?: MenuHoverEventHandler; + onMouseleave?: MenuHoverEventHandler; + + // >>>>> Events + onClick?: MenuClickEventHandler; +} + +export interface MenuItemGroupType extends ItemSharedProps { + type: 'group'; + + label?: any; + + children?: ItemType[]; +} + +export interface MenuDividerType extends ItemSharedProps { + type: 'divider'; +} + +export type ItemType = SubMenuType | MenuItemType | MenuItemGroupType | MenuDividerType | null; + export type MenuTheme = 'light' | 'dark'; // ========================== Basic ==========================