diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 0ed4d8f3e..9e412969f 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -7,8 +7,11 @@ import { PropType, inject, watchEffect, + watch, + reactive, } from 'vue'; -import useProvideMenu, { useProvideFirstLevel } from './hooks/useMenuContext'; +import shallowEqual from '../../_util/shallowequal'; +import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext'; import useConfigInject from '../../_util/hooks/useConfigInject'; import { MenuTheme, MenuMode, BuiltinPlacements, TriggerSubMenuAction } from './interface'; import devWarning from 'ant-design-vue/es/vc-util/devWarning'; @@ -19,6 +22,7 @@ export const menuProps = { disabled: Boolean, inlineCollapsed: Boolean, overflowDisabled: Boolean, + openKeys: Array, theme: { type: String as PropType, default: 'light' }, mode: { type: String as PropType, default: 'vertical' }, @@ -42,7 +46,7 @@ export default defineComponent({ emits: ['update:openKeys', 'openChange'], setup(props, { slots, emit }) { const { prefixCls, direction } = useConfigInject('menu', props); - + const store = reactive>({}); const siderCollapsed = inject( 'layoutSiderCollapsed', computed(() => undefined), @@ -70,8 +74,19 @@ export default defineComponent({ }); const activeKeys = ref([]); - const openKeys = ref([]); const selectedKeys = ref([]); + + const mergedOpenKeys = ref([]); + + watch( + () => props.openKeys, + (openKeys = mergedOpenKeys.value) => { + console.log('mergedOpenKeys', openKeys); + mergedOpenKeys.value = openKeys; + }, + { immediate: true }, + ); + const changeActiveKeys = (keys: Key[]) => { activeKeys.value = keys; }; @@ -107,15 +122,46 @@ export default defineComponent({ useProvideFirstLevel(true); - const onOpenChange = (key: Key, open: boolean) => { - // emit('update:openKeys', openKeys); - emit('openChange', open); + const getChildrenKeys = (eventKeys: string[]): Key[] => { + const keys = []; + eventKeys.forEach(eventKey => { + const { key, childrenEventKeys } = store[eventKey] as any; + keys.push(key, ...getChildrenKeys(childrenEventKeys.value)); + }); + return keys; + }; + + const onInternalOpenChange = (eventKey: Key, open: boolean) => { + const { key, childrenEventKeys } = store[eventKey] as any; + let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key); + + if (open) { + newOpenKeys.push(key); + } else if (mergedMode.value !== 'inline') { + // We need find all related popup to close + const subPathKeys = getChildrenKeys(childrenEventKeys.value); + newOpenKeys = newOpenKeys.filter(k => !subPathKeys.includes(k)); + } + + if (!shallowEqual(mergedOpenKeys, newOpenKeys)) { + mergedOpenKeys.value = newOpenKeys; + emit('update:openKeys', newOpenKeys); + emit('openChange', key, open); + } + }; + + const registerMenuInfo = (key: string, info: StoreMenuInfo) => { + store[key] = info as any; + }; + const unRegisterMenuInfo = (key: string) => { + delete store[key]; }; useProvideMenu({ + store, prefixCls, activeKeys, - openKeys, + openKeys: mergedOpenKeys, selectedKeys, changeActiveKeys, disabled, @@ -132,7 +178,9 @@ export default defineComponent({ siderCollapsed, defaultMotions, overflowDisabled: computed(() => props.overflowDisabled), - onOpenChange, + onOpenChange: onInternalOpenChange, + registerMenuInfo, + unRegisterMenuInfo, }); return () => { return
    {slots.default?.()}
; diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx index 3c1bf23c1..5fbaeef49 100644 --- a/components/menu/src/MenuItem.tsx +++ b/components/menu/src/MenuItem.tsx @@ -23,9 +23,9 @@ export default defineComponent({ setup(props, { slots, emit, attrs }) { const instance = getCurrentInstance(); const key = instance.vnode.key; - const uniKey = `menu_item_${++indexGuid}`; - const parentKeys = useInjectKeyPath(); - console.log(parentKeys.value); + const eventKey = `menu_item_${++indexGuid}_$$_${key}`; + const { parentEventKeys } = useInjectKeyPath(); + console.log(parentEventKeys.value); const { prefixCls, activeKeys, @@ -58,7 +58,7 @@ export default defineComponent({ }); const onMouseEnter = (event: MouseEvent) => { if (!mergedDisabled.value) { - changeActiveKeys([...parentKeys.value, key]); + changeActiveKeys([...parentEventKeys.value, key]); emit('mouseenter', event); } }; diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index 98627c5bd..ea5ed3763 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -1,5 +1,13 @@ import PropTypes from '../../_util/vue-types'; -import { computed, defineComponent, getCurrentInstance, ref, watch, PropType } from 'vue'; +import { + computed, + defineComponent, + getCurrentInstance, + ref, + watch, + PropType, + onBeforeUnmount, +} from 'vue'; import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath'; import { useInjectMenu, useProvideFirstLevel, MenuContextProvider } from './hooks/useMenuContext'; import { getPropsSlot, isValidElement } from 'ant-design-vue/es/_util/props-util'; @@ -9,6 +17,7 @@ import PopupTrigger from './PopupTrigger'; import SubMenuList from './SubMenuList'; import InlineSubMenuList from './InlineSubMenuList'; +let indexGuid = 0; export default defineComponent({ name: 'ASubMenu', props: { @@ -24,11 +33,34 @@ export default defineComponent({ emits: ['titleClick', 'titleMouseenter', 'titleMouseleave'], inheritAttrs: false, setup(props, { slots, attrs, emit }) { - useProvideKeyPath(); useProvideFirstLevel(false); + const instance = getCurrentInstance(); const key = instance.vnode.key; - const parentKeys = useInjectKeyPath(); + + const eventKey = `sub_menu_${++indexGuid}_$$_${key}`; + const { parentEventKeys, parentInfo } = useInjectKeyPath(); + const keysPath = computed(() => [...parentEventKeys.value, eventKey]); + + const childrenEventKeys = ref([]); + const menuInfo = { + eventKey, + key, + parentEventKeys, + childrenEventKeys, + }; + + parentInfo.childrenEventKeys?.value.push(eventKey); + onBeforeUnmount(() => { + if (parentInfo.childrenEventKeys) { + parentInfo.childrenEventKeys.value = parentInfo.childrenEventKeys?.value.filter( + k => k != eventKey, + ); + } + }); + + useProvideKeyPath(eventKey, menuInfo); + const { prefixCls, activeKeys, @@ -40,8 +72,16 @@ export default defineComponent({ openKeys, overflowDisabled, onOpenChange, + registerMenuInfo, + unRegisterMenuInfo, } = useInjectMenu(); + registerMenuInfo(eventKey, menuInfo); + + onBeforeUnmount(() => { + unRegisterMenuInfo(eventKey); + }); + const subMenuPrefixCls = computed(() => `${prefixCls.value}-submenu`); const mergedDisabled = computed(() => contextDisabled.value || props.disabled); const elementRef = ref(); @@ -71,20 +111,20 @@ export default defineComponent({ // >>>> Title click const onInternalTitleClick = (e: Event) => { // Skip if disabled - if (mergedDisabled) { + if (mergedDisabled.value) { return; } emit('titleClick', e, key); // Trigger open by click when mode is `inline` if (mode.value === 'inline') { - onOpenChange(key, !originOpen); + onOpenChange(eventKey, !originOpen.value); } }; const onMouseEnter = (event: MouseEvent) => { if (!mergedDisabled.value) { - changeActiveKeys([...parentKeys.value, key]); + changeActiveKeys(keysPath.value); emit('titleMouseenter', event); } }; @@ -96,12 +136,12 @@ export default defineComponent({ }; // ========================== DirectionStyle ========================== - const directionStyle = useDirectionStyle(computed(() => parentKeys.value.length)); + const directionStyle = useDirectionStyle(computed(() => keysPath.value.length)); // >>>>> Visible change const onPopupVisibleChange = (newVisible: boolean) => { if (mode.value !== 'inline') { - onOpenChange(key, newVisible); + onOpenChange(eventKey, newVisible); } }; @@ -110,11 +150,11 @@ export default defineComponent({ * We should manually trigger an active */ const onInternalFocus = () => { - changeActiveKeys([...parentKeys.value, key]); + changeActiveKeys(keysPath.value); }; // =============================== Render =============================== - const popupId = key && `${key}-popup`; + const popupId = eventKey && `${eventKey}-popup`; const popupClassName = computed(() => classNames( @@ -152,7 +192,7 @@ export default defineComponent({ // Cache mode if it change to `inline` which do not have popup motion const triggerModeRef = computed(() => { - return mode.value !== 'inline' && parentKeys.value.length > 1 ? 'vertical' : mode.value; + return mode.value !== 'inline' && keysPath.value.length > 1 ? 'vertical' : mode.value; }); const renderMode = computed(() => (mode.value === 'horizontal' ? 'vertical' : mode.value)); @@ -240,7 +280,7 @@ export default defineComponent({ {/* Inline mode */} {!overflowDisabled.value && ( - + {slots.default?.()} )} diff --git a/components/menu/src/hooks/useKeyPath.ts b/components/menu/src/hooks/useKeyPath.ts index a9ecfcee7..35f30a1ec 100644 --- a/components/menu/src/hooks/useKeyPath.ts +++ b/components/menu/src/hooks/useKeyPath.ts @@ -1,20 +1,23 @@ import { Key } from '../../../_util/type'; -import { computed, ComputedRef, getCurrentInstance, inject, InjectionKey, provide } from 'vue'; +import { computed, ComputedRef, inject, InjectionKey, provide } from 'vue'; +import { StoreMenuInfo } from './useMenuContext'; -const KeyPathContext: InjectionKey> = Symbol('KeyPathContext'); +const KeyPathContext: InjectionKey<{ + parentEventKeys: ComputedRef; + parentInfo: StoreMenuInfo; +}> = Symbol('KeyPathContext'); const useInjectKeyPath = () => { - return inject( - KeyPathContext, - computed(() => []), - ); + return inject(KeyPathContext, { + parentEventKeys: computed(() => []), + parentInfo: {} as StoreMenuInfo, + }); }; -const useProvideKeyPath = () => { - const parentKeys = useInjectKeyPath(); - const key = getCurrentInstance().vnode.key; - const keys = computed(() => [...parentKeys.value, key]); - provide(KeyPathContext, keys); +const useProvideKeyPath = (eventKey: string, menuInfo: StoreMenuInfo) => { + const { parentEventKeys } = useInjectKeyPath(); + const keys = computed(() => [...parentEventKeys.value, eventKey]); + provide(KeyPathContext, { parentEventKeys: keys, parentInfo: menuInfo }); return keys; }; diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 097aea7a5..0b4fea4f5 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -1,8 +1,18 @@ import { Key } from '../../../_util/type'; -import { ComputedRef, defineComponent, inject, InjectionKey, provide, Ref } from 'vue'; +import { ComputedRef, defineComponent, inject, InjectionKey, provide, Ref, UnwrapRef } from 'vue'; import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface'; +export interface StoreMenuInfo { + eventKey: string; + key: Key; + parentEventKeys: ComputedRef; + childrenEventKeys: Ref; + isLeaf?: boolean; +} export interface MenuContextProps { + store: UnwrapRef>; + registerMenuInfo: (key: string, info: StoreMenuInfo) => void; + unRegisterMenuInfo: (key: string) => void; prefixCls: ComputedRef; openKeys: Ref; selectedKeys: Ref; 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