refactor: menu

This commit is contained in:
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 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;

View File

@ -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<ExtractPropTypes<typeof menuItemGroupProps>>;
export default defineComponent({
name: 'AMenuItemGroup',
props: {
title: PropTypes.VNodeChild,
},
props: menuItemGroupProps,
inheritAttrs: false,
slots: ['title'],
setup(props, { slots, attrs }) {

View File

@ -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<ExtractPropTypes<typeof menuProps>>;
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<Record<string, StoreMenuInfo>>({});
@ -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<StoreMenuInfo>)
: 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');

View File

@ -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<ExtractPropTypes<typeof menuItemProps>>;
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 <span class={`${prefixCls.value}-title-content`}>{children}</span>;
};
// ========================== DirectionStyle ==========================
const directionStyle = useDirectionStyle(computed(() => keysPath.value.length));
return () => {
const { title } = props;
const children = flattenChildren(slots.default?.());
@ -187,6 +202,7 @@ export default defineComponent({
>
<li
{...attrs}
style={{ ...((attrs.style as any) || {}), ...directionStyle.value }}
class={[
classNames.value,
{

View File

@ -7,6 +7,7 @@ import {
watch,
PropType,
onBeforeUnmount,
ExtractPropTypes,
} from 'vue';
import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath';
import { useInjectMenu, useProvideFirstLevel, MenuContextProvider } from './hooks/useMenuContext';
@ -19,17 +20,22 @@ import InlineSubMenuList from './InlineSubMenuList';
import Transition, { getTransitionProps } from '../../_util/transition';
let indexGuid = 0;
const subMenuProps = {
icon: PropTypes.VNodeChild,
title: PropTypes.VNodeChild,
disabled: Boolean,
level: Number,
popupClassName: String,
popupOffset: Array as PropType<number[]>,
internalPopupClose: Boolean,
};
export type SubMenuProps = Partial<ExtractPropTypes<typeof subMenuProps>>;
export default defineComponent({
name: 'ASubMenu',
props: {
icon: PropTypes.VNodeChild,
title: PropTypes.VNodeChild,
disabled: Boolean,
level: Number,
popupClassName: String,
popupOffset: Array as PropType<number[]>,
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));

View File

@ -8,7 +8,7 @@ export default function useDirectionStyle(level: ComputedRef<number>): 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` },
);
}

View File

@ -3,21 +3,24 @@ import { computed, ComputedRef, inject, InjectionKey, provide } from 'vue';
import { StoreMenuInfo } from './useMenuContext';
const KeyPathContext: InjectionKey<{
parentEventKeys: ComputedRef<Key[]>;
parentEventKeys: ComputedRef<string[]>;
parentKeys: ComputedRef<Key[]>;
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;
};

View File

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

View File

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

2
v2-doc

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