diff --git a/components/menu/index.tsx b/components/menu/index.tsx index fa839d6b1..d61ffcdd6 100644 --- a/components/menu/index.tsx +++ b/components/menu/index.tsx @@ -1,7 +1,7 @@ -import Menu from './src/Menu'; -import MenuItem from './src/MenuItem'; -import SubMenu from './src/SubMenu'; -import ItemGroup from './src/ItemGroup'; +import Menu, { MenuProps } from './src/Menu'; +import MenuItem, { MenuItemProps } from './src/MenuItem'; +import SubMenu, { SubMenuProps } from './src/SubMenu'; +import ItemGroup, { MenuItemGroupProps } from './src/ItemGroup'; import Divider from './src/Divider'; import { App } from 'vue'; /* istanbul ignore next */ @@ -19,6 +19,18 @@ Menu.Divider = Divider; Menu.SubMenu = SubMenu; Menu.ItemGroup = ItemGroup; +export { + SubMenu, + MenuItem as Item, + MenuItem, + ItemGroup, + Divider, + MenuProps, + SubMenuProps, + MenuItemProps, + MenuItemGroupProps, +}; + export default Menu as typeof Menu & Plugin & { readonly Item: typeof MenuItem; diff --git a/components/menu/src/ItemGroup.tsx b/components/menu/src/ItemGroup.tsx index 993f0cb4d..953594f20 100644 --- a/components/menu/src/ItemGroup.tsx +++ b/components/menu/src/ItemGroup.tsx @@ -1,13 +1,17 @@ import { getPropsSlot } from '../../_util/props-util'; -import { computed, defineComponent } from 'vue'; +import { computed, defineComponent, ExtractPropTypes } from 'vue'; import PropTypes from '../../_util/vue-types'; import { useInjectMenu } from './hooks/useMenuContext'; +const menuItemGroupProps = { + title: PropTypes.VNodeChild, +}; + +export type MenuItemGroupProps = Partial>; + export default defineComponent({ name: 'AMenuItemGroup', - props: { - title: PropTypes.VNodeChild, - }, + props: menuItemGroupProps, inheritAttrs: false, slots: ['title'], setup(props, { slots, attrs }) { diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 4f619ad38..36f2e2556 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -10,8 +10,8 @@ import { watch, reactive, onMounted, - toRaw, unref, + UnwrapRef, } from 'vue'; import shallowEqual from '../../_util/shallowequal'; import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext'; @@ -35,6 +35,7 @@ export const menuProps = { overflowDisabled: Boolean, openKeys: Array, selectedKeys: Array, + activeKey: String, // 内部组件使用 selectable: { type: Boolean, default: true }, multiple: { type: Boolean, default: false }, @@ -59,7 +60,15 @@ export type MenuProps = Partial>; export default defineComponent({ name: 'AMenu', props: menuProps, - emits: ['update:openKeys', 'openChange', 'select', 'deselect', 'update:selectedKeys', 'click'], + emits: [ + 'update:openKeys', + 'openChange', + 'select', + 'deselect', + 'update:selectedKeys', + 'click', + 'update:activeKey', + ], setup(props, { slots, emit }) { const { prefixCls, direction } = useConfigInject('menu', props); const store = reactive>({}); @@ -94,6 +103,34 @@ export default defineComponent({ const activeKeys = ref([]); const mergedSelectedKeys = ref([]); + const keyMapStore = ref({}); + watch( + store, + () => { + const newKeyMapStore = {}; + for (let [_eventKey, menuInfo] of Object.entries(store)) { + newKeyMapStore[menuInfo.key] = menuInfo; + } + keyMapStore.value = newKeyMapStore; + }, + { immediate: true }, + ); + watchEffect(() => { + if ('activeKey' in props) { + let keys = []; + const menuInfo = props.activeKey + ? (keyMapStore.value[props.activeKey] as UnwrapRef) + : undefined; + if (menuInfo && props.activeKey !== undefined) { + keys = [...menuInfo.parentKeys, props.activeKey]; + } else { + keys = []; + } + if (!shallowEqual(activeKeys.value, keys)) { + activeKeys.value = keys; + } + } + }); watch( () => props.selectedKeys, @@ -106,11 +143,12 @@ export default defineComponent({ const selectedSubMenuEventKeys = ref([]); watch( - [store, mergedSelectedKeys], + [keyMapStore, mergedSelectedKeys], () => { let subMenuParentEventKeys = []; - (Object.values(toRaw(store)) as any).forEach((menuInfo: StoreMenuInfo) => { - if (mergedSelectedKeys.value.includes(menuInfo.key)) { + mergedSelectedKeys.value.forEach(key => { + const menuInfo = keyMapStore.value[key]; + if (menuInfo) { subMenuParentEventKeys.push(...unref(menuInfo.parentEventKeys)); } }); @@ -172,7 +210,11 @@ export default defineComponent({ ); const changeActiveKeys = (keys: Key[]) => { - activeKeys.value = keys; + if ('activeKey' in props) { + emit('update:activeKey', keys[keys.length - 1]); + } else { + activeKeys.value = keys; + } }; const disabled = computed(() => !!props.disabled); const isRtl = computed(() => direction.value === 'rtl'); diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx index fb0368eea..21bffc608 100644 --- a/components/menu/src/MenuItem.tsx +++ b/components/menu/src/MenuItem.tsx @@ -1,24 +1,36 @@ import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util'; import PropTypes from '../../_util/vue-types'; -import { computed, defineComponent, getCurrentInstance, onBeforeUnmount, ref, watch } from 'vue'; +import { + computed, + defineComponent, + ExtractPropTypes, + getCurrentInstance, + onBeforeUnmount, + ref, + watch, +} from 'vue'; import { useInjectKeyPath } from './hooks/useKeyPath'; import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext'; import { cloneElement } from '../../_util/vnode'; import Tooltip from '../../tooltip'; import { MenuInfo } from './interface'; import KeyCode from 'ant-design-vue/es/_util/KeyCode'; +import useDirectionStyle from './hooks/useDirectionStyle'; let indexGuid = 0; +const menuItemProps = { + role: String, + disabled: Boolean, + danger: Boolean, + title: { type: [String, Boolean], default: undefined }, + icon: PropTypes.VNodeChild, +}; + +export type MenuItemProps = Partial>; export default defineComponent({ name: 'AMenuItem', - props: { - role: String, - disabled: Boolean, - danger: Boolean, - title: { type: [String, Boolean], default: undefined }, - icon: PropTypes.VNodeChild, - }, + props: menuItemProps, emits: ['mouseenter', 'mouseleave', 'click', 'keydown', 'focus'], slots: ['icon'], inheritAttrs: false, @@ -26,7 +38,7 @@ export default defineComponent({ const instance = getCurrentInstance(); const key = instance.vnode.key; const eventKey = `menu_item_${++indexGuid}_$$_${key}`; - const { parentEventKeys } = useInjectKeyPath(); + const { parentEventKeys, parentKeys } = useInjectKeyPath(); const { prefixCls, activeKeys, @@ -37,21 +49,21 @@ export default defineComponent({ siderCollapsed, onItemClick, selectedKeys, - store, registerMenuInfo, unRegisterMenuInfo, } = useInjectMenu(); const firstLevel = useInjectFirstLevel(); const isActive = ref(false); - const keyPath = computed(() => { - return [...parentEventKeys.value.map(eK => store[eK].key), key]; + const keysPath = computed(() => { + return [...parentKeys.value, key]; }); - const keysPath = computed(() => [...parentEventKeys.value, eventKey]); + // const keysPath = computed(() => [...parentEventKeys.value, eventKey]); const menuInfo = { eventKey, key, parentEventKeys, + parentKeys, isLeaf: true, }; @@ -85,7 +97,7 @@ export default defineComponent({ return { key: key, eventKey: eventKey, - keyPath: keyPath.value, + keyPath: keysPath.value, eventKeyPath: [...parentEventKeys.value, eventKey], domEvent: e, }; @@ -150,6 +162,9 @@ export default defineComponent({ return {children}; }; + // ========================== DirectionStyle ========================== + const directionStyle = useDirectionStyle(computed(() => keysPath.value.length)); + return () => { const { title } = props; const children = flattenChildren(slots.default?.()); @@ -187,6 +202,7 @@ export default defineComponent({ >
  • , + internalPopupClose: Boolean, +}; + +export type SubMenuProps = Partial>; + export default defineComponent({ name: 'ASubMenu', - props: { - icon: PropTypes.VNodeChild, - title: PropTypes.VNodeChild, - disabled: Boolean, - level: Number, - popupClassName: String, - popupOffset: Array as PropType, - internalPopupClose: Boolean, - }, + props: subMenuProps, slots: ['icon', 'title'], emits: ['titleClick', 'mouseenter', 'mouseleave'], inheritAttrs: false, @@ -40,15 +46,16 @@ export default defineComponent({ const key = instance.vnode.key; const eventKey = `sub_menu_${++indexGuid}_$$_${key}`; - const { parentEventKeys, parentInfo } = useInjectKeyPath(); - const keysPath = computed(() => [...parentEventKeys.value, eventKey]); - + const { parentEventKeys, parentInfo, parentKeys } = useInjectKeyPath(); + const keysPath = computed(() => [...parentKeys.value, key]); + const eventKeysPath = computed(() => [...parentEventKeys.value, eventKey]); const childrenEventKeys = ref([]); const menuInfo = { eventKey, key, parentEventKeys, childrenEventKeys, + parentKeys, }; parentInfo.childrenEventKeys?.value.push(eventKey); onBeforeUnmount(() => { @@ -59,7 +66,7 @@ export default defineComponent({ } }); - useProvideKeyPath(eventKey, menuInfo); + useProvideKeyPath(eventKey, key, menuInfo); const { prefixCls, @@ -141,7 +148,7 @@ export default defineComponent({ }; // ========================== DirectionStyle ========================== - const directionStyle = useDirectionStyle(computed(() => keysPath.value.length)); + const directionStyle = useDirectionStyle(computed(() => eventKeysPath.value.length)); // >>>>> Visible change const onPopupVisibleChange = (newVisible: boolean) => { @@ -189,7 +196,7 @@ export default defineComponent({ // Cache mode if it change to `inline` which do not have popup motion const triggerModeRef = computed(() => { - return mode.value !== 'inline' && keysPath.value.length > 1 ? 'vertical' : mode.value; + return mode.value !== 'inline' && eventKeysPath.value.length > 1 ? 'vertical' : mode.value; }); const renderMode = computed(() => (mode.value === 'horizontal' ? 'vertical' : mode.value)); diff --git a/components/menu/src/hooks/useDirectionStyle.ts b/components/menu/src/hooks/useDirectionStyle.ts index 9721fc16d..358cb13d9 100644 --- a/components/menu/src/hooks/useDirectionStyle.ts +++ b/components/menu/src/hooks/useDirectionStyle.ts @@ -8,7 +8,7 @@ export default function useDirectionStyle(level: ComputedRef): ComputedR mode.value !== 'inline' ? null : rtl.value - ? { paddingRight: level.value * inlineIndent.value } - : { paddingLeft: level.value * inlineIndent.value }, + ? { paddingRight: `${level.value * inlineIndent.value}px` } + : { paddingLeft: `${level.value * inlineIndent.value}px` }, ); } diff --git a/components/menu/src/hooks/useKeyPath.ts b/components/menu/src/hooks/useKeyPath.ts index 35f30a1ec..45f6230c9 100644 --- a/components/menu/src/hooks/useKeyPath.ts +++ b/components/menu/src/hooks/useKeyPath.ts @@ -3,21 +3,24 @@ import { computed, ComputedRef, inject, InjectionKey, provide } from 'vue'; import { StoreMenuInfo } from './useMenuContext'; const KeyPathContext: InjectionKey<{ - parentEventKeys: ComputedRef; + parentEventKeys: ComputedRef; + parentKeys: ComputedRef; parentInfo: StoreMenuInfo; }> = Symbol('KeyPathContext'); const useInjectKeyPath = () => { return inject(KeyPathContext, { parentEventKeys: computed(() => []), + parentKeys: computed(() => []), parentInfo: {} as StoreMenuInfo, }); }; -const useProvideKeyPath = (eventKey: string, menuInfo: StoreMenuInfo) => { - const { parentEventKeys } = useInjectKeyPath(); - const keys = computed(() => [...parentEventKeys.value, eventKey]); - provide(KeyPathContext, { parentEventKeys: keys, parentInfo: menuInfo }); +const useProvideKeyPath = (eventKey: string, key: Key, menuInfo: StoreMenuInfo) => { + const { parentEventKeys, parentKeys } = useInjectKeyPath(); + const eventKeys = computed(() => [...parentEventKeys.value, eventKey]); + const keys = computed(() => [...parentKeys.value, key]); + provide(KeyPathContext, { parentEventKeys: eventKeys, parentKeys: keys, parentInfo: menuInfo }); return keys; }; diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 7ee25502c..5ada41065 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -24,6 +24,7 @@ export interface StoreMenuInfo { parentEventKeys: ComputedRef; childrenEventKeys?: Ref; isLeaf?: boolean; + parentKeys: ComputedRef; } export interface MenuContextProps { isRootMenu: boolean; diff --git a/components/menu/src/interface.ts b/components/menu/src/interface.ts index 14a153c18..a752316f7 100644 --- a/components/menu/src/interface.ts +++ b/components/menu/src/interface.ts @@ -22,7 +22,7 @@ export interface MenuInfo { key: Key; eventKey: string; keyPath?: Key[]; - eventKeyPath: Key[]; + eventKeyPath: string[]; domEvent: MouseEvent | KeyboardEvent; } diff --git a/v2-doc b/v2-doc index d19705328..a7013ae87 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2 +Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557