refactor: menu

pull/4134/head
tanjinzhou 2021-05-24 11:51:32 +08:00
parent 7b494fd445
commit 8179361803
10 changed files with 137 additions and 52 deletions

View File

@ -1,7 +1,7 @@
import Menu from './src/Menu'; import Menu, { MenuProps } from './src/Menu';
import MenuItem from './src/MenuItem'; import MenuItem, { MenuItemProps } from './src/MenuItem';
import SubMenu from './src/SubMenu'; import SubMenu, { SubMenuProps } from './src/SubMenu';
import ItemGroup from './src/ItemGroup'; import ItemGroup, { MenuItemGroupProps } from './src/ItemGroup';
import Divider from './src/Divider'; import Divider from './src/Divider';
import { App } from 'vue'; import { App } from 'vue';
/* istanbul ignore next */ /* istanbul ignore next */
@ -19,6 +19,18 @@ Menu.Divider = Divider;
Menu.SubMenu = SubMenu; Menu.SubMenu = SubMenu;
Menu.ItemGroup = ItemGroup; Menu.ItemGroup = ItemGroup;
export {
SubMenu,
MenuItem as Item,
MenuItem,
ItemGroup,
Divider,
MenuProps,
SubMenuProps,
MenuItemProps,
MenuItemGroupProps,
};
export default Menu as typeof Menu & export default Menu as typeof Menu &
Plugin & { Plugin & {
readonly Item: typeof MenuItem; readonly Item: typeof MenuItem;

View File

@ -1,13 +1,17 @@
import { getPropsSlot } from '../../_util/props-util'; import { getPropsSlot } from '../../_util/props-util';
import { computed, defineComponent } from 'vue'; import { computed, defineComponent, ExtractPropTypes } from 'vue';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import { useInjectMenu } from './hooks/useMenuContext'; import { useInjectMenu } from './hooks/useMenuContext';
const menuItemGroupProps = {
title: PropTypes.VNodeChild,
};
export type MenuItemGroupProps = Partial<ExtractPropTypes<typeof menuItemGroupProps>>;
export default defineComponent({ export default defineComponent({
name: 'AMenuItemGroup', name: 'AMenuItemGroup',
props: { props: menuItemGroupProps,
title: PropTypes.VNodeChild,
},
inheritAttrs: false, inheritAttrs: false,
slots: ['title'], slots: ['title'],
setup(props, { slots, attrs }) { setup(props, { slots, attrs }) {

View File

@ -10,8 +10,8 @@ import {
watch, watch,
reactive, reactive,
onMounted, onMounted,
toRaw,
unref, unref,
UnwrapRef,
} from 'vue'; } from 'vue';
import shallowEqual from '../../_util/shallowequal'; import shallowEqual from '../../_util/shallowequal';
import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext'; import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext';
@ -35,6 +35,7 @@ export const menuProps = {
overflowDisabled: Boolean, overflowDisabled: Boolean,
openKeys: Array, openKeys: Array,
selectedKeys: Array, selectedKeys: Array,
activeKey: String, // 使
selectable: { type: Boolean, default: true }, selectable: { type: Boolean, default: true },
multiple: { type: Boolean, default: false }, multiple: { type: Boolean, default: false },
@ -59,7 +60,15 @@ export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
export default defineComponent({ export default defineComponent({
name: 'AMenu', name: 'AMenu',
props: menuProps, 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 }) { setup(props, { slots, emit }) {
const { prefixCls, direction } = useConfigInject('menu', props); const { prefixCls, direction } = useConfigInject('menu', props);
const store = reactive<Record<string, StoreMenuInfo>>({}); const store = reactive<Record<string, StoreMenuInfo>>({});
@ -94,6 +103,34 @@ export default defineComponent({
const activeKeys = ref([]); const activeKeys = ref([]);
const mergedSelectedKeys = 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<StoreMenuInfo>)
: undefined;
if (menuInfo && props.activeKey !== undefined) {
keys = [...menuInfo.parentKeys, props.activeKey];
} else {
keys = [];
}
if (!shallowEqual(activeKeys.value, keys)) {
activeKeys.value = keys;
}
}
});
watch( watch(
() => props.selectedKeys, () => props.selectedKeys,
@ -106,11 +143,12 @@ export default defineComponent({
const selectedSubMenuEventKeys = ref([]); const selectedSubMenuEventKeys = ref([]);
watch( watch(
[store, mergedSelectedKeys], [keyMapStore, mergedSelectedKeys],
() => { () => {
let subMenuParentEventKeys = []; let subMenuParentEventKeys = [];
(Object.values(toRaw(store)) as any).forEach((menuInfo: StoreMenuInfo) => { mergedSelectedKeys.value.forEach(key => {
if (mergedSelectedKeys.value.includes(menuInfo.key)) { const menuInfo = keyMapStore.value[key];
if (menuInfo) {
subMenuParentEventKeys.push(...unref(menuInfo.parentEventKeys)); subMenuParentEventKeys.push(...unref(menuInfo.parentEventKeys));
} }
}); });
@ -172,7 +210,11 @@ export default defineComponent({
); );
const changeActiveKeys = (keys: Key[]) => { const changeActiveKeys = (keys: Key[]) => {
if ('activeKey' in props) {
emit('update:activeKey', keys[keys.length - 1]);
} else {
activeKeys.value = keys; activeKeys.value = keys;
}
}; };
const disabled = computed(() => !!props.disabled); const disabled = computed(() => !!props.disabled);
const isRtl = computed(() => direction.value === 'rtl'); const isRtl = computed(() => direction.value === 'rtl');

View File

@ -1,24 +1,36 @@
import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util'; import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util';
import PropTypes from '../../_util/vue-types'; 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 { useInjectKeyPath } from './hooks/useKeyPath';
import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext'; import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext';
import { cloneElement } from '../../_util/vnode'; import { cloneElement } from '../../_util/vnode';
import Tooltip from '../../tooltip'; import Tooltip from '../../tooltip';
import { MenuInfo } from './interface'; import { MenuInfo } from './interface';
import KeyCode from 'ant-design-vue/es/_util/KeyCode'; import KeyCode from 'ant-design-vue/es/_util/KeyCode';
import useDirectionStyle from './hooks/useDirectionStyle';
let indexGuid = 0; let indexGuid = 0;
const menuItemProps = {
export default defineComponent({
name: 'AMenuItem',
props: {
role: String, role: String,
disabled: Boolean, disabled: Boolean,
danger: Boolean, danger: Boolean,
title: { type: [String, Boolean], default: undefined }, title: { type: [String, Boolean], default: undefined },
icon: PropTypes.VNodeChild, icon: PropTypes.VNodeChild,
}, };
export type MenuItemProps = Partial<ExtractPropTypes<typeof menuItemProps>>;
export default defineComponent({
name: 'AMenuItem',
props: menuItemProps,
emits: ['mouseenter', 'mouseleave', 'click', 'keydown', 'focus'], emits: ['mouseenter', 'mouseleave', 'click', 'keydown', 'focus'],
slots: ['icon'], slots: ['icon'],
inheritAttrs: false, inheritAttrs: false,
@ -26,7 +38,7 @@ export default defineComponent({
const instance = getCurrentInstance(); const instance = getCurrentInstance();
const key = instance.vnode.key; const key = instance.vnode.key;
const eventKey = `menu_item_${++indexGuid}_$$_${key}`; const eventKey = `menu_item_${++indexGuid}_$$_${key}`;
const { parentEventKeys } = useInjectKeyPath(); const { parentEventKeys, parentKeys } = useInjectKeyPath();
const { const {
prefixCls, prefixCls,
activeKeys, activeKeys,
@ -37,21 +49,21 @@ export default defineComponent({
siderCollapsed, siderCollapsed,
onItemClick, onItemClick,
selectedKeys, selectedKeys,
store,
registerMenuInfo, registerMenuInfo,
unRegisterMenuInfo, unRegisterMenuInfo,
} = useInjectMenu(); } = useInjectMenu();
const firstLevel = useInjectFirstLevel(); const firstLevel = useInjectFirstLevel();
const isActive = ref(false); const isActive = ref(false);
const keyPath = computed(() => { const keysPath = computed(() => {
return [...parentEventKeys.value.map(eK => store[eK].key), key]; return [...parentKeys.value, key];
}); });
const keysPath = computed(() => [...parentEventKeys.value, eventKey]); // const keysPath = computed(() => [...parentEventKeys.value, eventKey]);
const menuInfo = { const menuInfo = {
eventKey, eventKey,
key, key,
parentEventKeys, parentEventKeys,
parentKeys,
isLeaf: true, isLeaf: true,
}; };
@ -85,7 +97,7 @@ export default defineComponent({
return { return {
key: key, key: key,
eventKey: eventKey, eventKey: eventKey,
keyPath: keyPath.value, keyPath: keysPath.value,
eventKeyPath: [...parentEventKeys.value, eventKey], eventKeyPath: [...parentEventKeys.value, eventKey],
domEvent: e, domEvent: e,
}; };
@ -150,6 +162,9 @@ export default defineComponent({
return <span class={`${prefixCls.value}-title-content`}>{children}</span>; return <span class={`${prefixCls.value}-title-content`}>{children}</span>;
}; };
// ========================== DirectionStyle ==========================
const directionStyle = useDirectionStyle(computed(() => keysPath.value.length));
return () => { return () => {
const { title } = props; const { title } = props;
const children = flattenChildren(slots.default?.()); const children = flattenChildren(slots.default?.());
@ -187,6 +202,7 @@ export default defineComponent({
> >
<li <li
{...attrs} {...attrs}
style={{ ...((attrs.style as any) || {}), ...directionStyle.value }}
class={[ class={[
classNames.value, classNames.value,
{ {

View File

@ -7,6 +7,7 @@ import {
watch, watch,
PropType, PropType,
onBeforeUnmount, onBeforeUnmount,
ExtractPropTypes,
} from 'vue'; } from 'vue';
import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath'; import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath';
import { useInjectMenu, useProvideFirstLevel, MenuContextProvider } from './hooks/useMenuContext'; import { useInjectMenu, useProvideFirstLevel, MenuContextProvider } from './hooks/useMenuContext';
@ -19,9 +20,8 @@ import InlineSubMenuList from './InlineSubMenuList';
import Transition, { getTransitionProps } from '../../_util/transition'; import Transition, { getTransitionProps } from '../../_util/transition';
let indexGuid = 0; let indexGuid = 0;
export default defineComponent({
name: 'ASubMenu', const subMenuProps = {
props: {
icon: PropTypes.VNodeChild, icon: PropTypes.VNodeChild,
title: PropTypes.VNodeChild, title: PropTypes.VNodeChild,
disabled: Boolean, disabled: Boolean,
@ -29,7 +29,13 @@ export default defineComponent({
popupClassName: String, popupClassName: String,
popupOffset: Array as PropType<number[]>, popupOffset: Array as PropType<number[]>,
internalPopupClose: Boolean, internalPopupClose: Boolean,
}, };
export type SubMenuProps = Partial<ExtractPropTypes<typeof subMenuProps>>;
export default defineComponent({
name: 'ASubMenu',
props: subMenuProps,
slots: ['icon', 'title'], slots: ['icon', 'title'],
emits: ['titleClick', 'mouseenter', 'mouseleave'], emits: ['titleClick', 'mouseenter', 'mouseleave'],
inheritAttrs: false, inheritAttrs: false,
@ -40,15 +46,16 @@ export default defineComponent({
const key = instance.vnode.key; const key = instance.vnode.key;
const eventKey = `sub_menu_${++indexGuid}_$$_${key}`; const eventKey = `sub_menu_${++indexGuid}_$$_${key}`;
const { parentEventKeys, parentInfo } = useInjectKeyPath(); const { parentEventKeys, parentInfo, parentKeys } = useInjectKeyPath();
const keysPath = computed(() => [...parentEventKeys.value, eventKey]); const keysPath = computed(() => [...parentKeys.value, key]);
const eventKeysPath = computed(() => [...parentEventKeys.value, eventKey]);
const childrenEventKeys = ref([]); const childrenEventKeys = ref([]);
const menuInfo = { const menuInfo = {
eventKey, eventKey,
key, key,
parentEventKeys, parentEventKeys,
childrenEventKeys, childrenEventKeys,
parentKeys,
}; };
parentInfo.childrenEventKeys?.value.push(eventKey); parentInfo.childrenEventKeys?.value.push(eventKey);
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -59,7 +66,7 @@ export default defineComponent({
} }
}); });
useProvideKeyPath(eventKey, menuInfo); useProvideKeyPath(eventKey, key, menuInfo);
const { const {
prefixCls, prefixCls,
@ -141,7 +148,7 @@ export default defineComponent({
}; };
// ========================== DirectionStyle ========================== // ========================== DirectionStyle ==========================
const directionStyle = useDirectionStyle(computed(() => keysPath.value.length)); const directionStyle = useDirectionStyle(computed(() => eventKeysPath.value.length));
// >>>>> Visible change // >>>>> Visible change
const onPopupVisibleChange = (newVisible: boolean) => { const onPopupVisibleChange = (newVisible: boolean) => {
@ -189,7 +196,7 @@ export default defineComponent({
// Cache mode if it change to `inline` which do not have popup motion // Cache mode if it change to `inline` which do not have popup motion
const triggerModeRef = computed(() => { 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)); const renderMode = computed(() => (mode.value === 'horizontal' ? 'vertical' : mode.value));

View File

@ -8,7 +8,7 @@ export default function useDirectionStyle(level: ComputedRef<number>): ComputedR
mode.value !== 'inline' mode.value !== 'inline'
? null ? null
: rtl.value : rtl.value
? { paddingRight: level.value * inlineIndent.value } ? { paddingRight: `${level.value * inlineIndent.value}px` }
: { paddingLeft: level.value * inlineIndent.value }, : { paddingLeft: `${level.value * inlineIndent.value}px` },
); );
} }

View File

@ -3,21 +3,24 @@ import { computed, ComputedRef, inject, InjectionKey, provide } from 'vue';
import { StoreMenuInfo } from './useMenuContext'; import { StoreMenuInfo } from './useMenuContext';
const KeyPathContext: InjectionKey<{ const KeyPathContext: InjectionKey<{
parentEventKeys: ComputedRef<Key[]>; parentEventKeys: ComputedRef<string[]>;
parentKeys: ComputedRef<Key[]>;
parentInfo: StoreMenuInfo; parentInfo: StoreMenuInfo;
}> = Symbol('KeyPathContext'); }> = Symbol('KeyPathContext');
const useInjectKeyPath = () => { const useInjectKeyPath = () => {
return inject(KeyPathContext, { return inject(KeyPathContext, {
parentEventKeys: computed(() => []), parentEventKeys: computed(() => []),
parentKeys: computed(() => []),
parentInfo: {} as StoreMenuInfo, parentInfo: {} as StoreMenuInfo,
}); });
}; };
const useProvideKeyPath = (eventKey: string, menuInfo: StoreMenuInfo) => { const useProvideKeyPath = (eventKey: string, key: Key, menuInfo: StoreMenuInfo) => {
const { parentEventKeys } = useInjectKeyPath(); const { parentEventKeys, parentKeys } = useInjectKeyPath();
const keys = computed(() => [...parentEventKeys.value, eventKey]); const eventKeys = computed(() => [...parentEventKeys.value, eventKey]);
provide(KeyPathContext, { parentEventKeys: keys, parentInfo: menuInfo }); const keys = computed(() => [...parentKeys.value, key]);
provide(KeyPathContext, { parentEventKeys: eventKeys, parentKeys: keys, parentInfo: menuInfo });
return keys; return keys;
}; };

View File

@ -24,6 +24,7 @@ export interface StoreMenuInfo {
parentEventKeys: ComputedRef<string[]>; parentEventKeys: ComputedRef<string[]>;
childrenEventKeys?: Ref<string[]>; childrenEventKeys?: Ref<string[]>;
isLeaf?: boolean; isLeaf?: boolean;
parentKeys: ComputedRef<Key[]>;
} }
export interface MenuContextProps { export interface MenuContextProps {
isRootMenu: boolean; isRootMenu: boolean;

View File

@ -22,7 +22,7 @@ export interface MenuInfo {
key: Key; key: Key;
eventKey: string; eventKey: string;
keyPath?: Key[]; keyPath?: Key[];
eventKeyPath: Key[]; eventKeyPath: string[];
domEvent: MouseEvent | KeyboardEvent; domEvent: MouseEvent | KeyboardEvent;
} }

2
v2-doc

@ -1 +1 @@
Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2 Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557