refactor: menu

feat-new-menu
tangjinzhou 2021-05-19 23:38:56 +08:00
parent f3db7548b5
commit 24efefe16d
6 changed files with 101 additions and 15 deletions

View File

@ -1,4 +1,4 @@
import { computed, defineComponent, reactive, ref, watch } from '@vue/runtime-core'; import { computed, defineComponent, ref, watch } from '@vue/runtime-core';
import Transition from 'ant-design-vue/es/_util/transition'; import Transition from 'ant-design-vue/es/_util/transition';
import { useInjectMenu, MenuContextProvider } from './hooks/useMenuContext'; import { useInjectMenu, MenuContextProvider } from './hooks/useMenuContext';
import { MenuMode } from './interface'; import { MenuMode } from './interface';
@ -14,7 +14,7 @@ export default defineComponent({
}, },
setup(props, { slots }) { setup(props, { slots }) {
const fixedMode: MenuMode = 'inline'; const fixedMode: MenuMode = 'inline';
const { prefixCls, forceSubMenuRender, motion, mode, defaultMotions } = useInjectMenu(); const { forceSubMenuRender, motion, mode, defaultMotions } = useInjectMenu();
const sameModeRef = computed(() => mode.value === fixedMode); const sameModeRef = computed(() => mode.value === fixedMode);
const destroy = ref(!sameModeRef.value); const destroy = ref(!sameModeRef.value);

View File

@ -14,7 +14,14 @@ import {
import shallowEqual from '../../_util/shallowequal'; import shallowEqual from '../../_util/shallowequal';
import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext'; import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext';
import useConfigInject from '../../_util/hooks/useConfigInject'; import useConfigInject from '../../_util/hooks/useConfigInject';
import { MenuTheme, MenuMode, BuiltinPlacements, TriggerSubMenuAction } from './interface'; import {
MenuTheme,
MenuMode,
BuiltinPlacements,
TriggerSubMenuAction,
MenuInfo,
SelectInfo,
} from './interface';
import devWarning from 'ant-design-vue/es/vc-util/devWarning'; import devWarning from 'ant-design-vue/es/vc-util/devWarning';
import { collapseMotion, CSSMotionProps } from 'ant-design-vue/es/_util/transition'; import { collapseMotion, CSSMotionProps } from 'ant-design-vue/es/_util/transition';
@ -24,6 +31,9 @@ export const menuProps = {
inlineCollapsed: Boolean, inlineCollapsed: Boolean,
overflowDisabled: Boolean, overflowDisabled: Boolean,
openKeys: Array, openKeys: Array,
selectedKeys: Array,
selectable: Boolean,
multiple: Boolean,
motion: Object as PropType<CSSMotionProps>, motion: Object as PropType<CSSMotionProps>,
@ -46,7 +56,7 @@ 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'], emits: ['update:openKeys', 'openChange', 'select', 'deselect', 'update:selectedKeys'],
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>>({});
@ -81,7 +91,48 @@ export default defineComponent({
}); });
const activeKeys = ref([]); const activeKeys = ref([]);
const selectedKeys = ref([]); const mergedSelectedKeys = ref([]);
watch(
() => props.selectedKeys,
(selectedKeys = mergedSelectedKeys.value) => {
mergedSelectedKeys.value = selectedKeys;
},
{ immediate: true },
);
// >>>>> Trigger select
const triggerSelection = (info: MenuInfo) => {
if (!props.selectable) {
return;
}
// Insert or Remove
const { key: targetKey } = info;
const exist = mergedSelectedKeys.value.includes(targetKey);
let newSelectedKeys: Key[];
if (exist) {
newSelectedKeys = mergedSelectedKeys.value.filter(key => key !== targetKey);
} else if (props.multiple) {
newSelectedKeys = [...mergedSelectedKeys.value, targetKey];
} else {
newSelectedKeys = [targetKey];
}
mergedSelectedKeys.value = newSelectedKeys;
// Trigger event
const selectInfo: SelectInfo = {
...info,
selectedKeys: newSelectedKeys,
};
if (exist) {
emit('deselect', selectInfo);
} else {
emit('select', selectInfo);
}
};
const mergedOpenKeys = ref([]); const mergedOpenKeys = ref([]);
@ -201,7 +252,7 @@ export default defineComponent({
prefixCls, prefixCls,
activeKeys, activeKeys,
openKeys: mergedOpenKeys, openKeys: mergedOpenKeys,
selectedKeys, selectedKeys: mergedSelectedKeys,
changeActiveKeys, changeActiveKeys,
disabled, disabled,
rtl: isRtl, rtl: isRtl,
@ -219,6 +270,7 @@ export default defineComponent({
motion: computed(() => (isMounted.value ? props.motion : null)), motion: computed(() => (isMounted.value ? props.motion : null)),
overflowDisabled: computed(() => props.overflowDisabled), overflowDisabled: computed(() => props.overflowDisabled),
onOpenChange: onInternalOpenChange, onOpenChange: onInternalOpenChange,
onItemClick: triggerSelection,
registerMenuInfo, registerMenuInfo,
unRegisterMenuInfo, unRegisterMenuInfo,
}); });

View File

@ -5,6 +5,7 @@ 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';
let indexGuid = 0; let indexGuid = 0;
@ -17,7 +18,7 @@ export default defineComponent({
title: { type: [String, Boolean] }, title: { type: [String, Boolean] },
icon: PropTypes.VNodeChild, icon: PropTypes.VNodeChild,
}, },
emits: ['mouseenter', 'mouseleave'], emits: ['mouseenter', 'mouseleave', 'click'],
slots: ['icon'], slots: ['icon'],
inheritAttrs: false, inheritAttrs: false,
setup(props, { slots, emit, attrs }) { setup(props, { slots, emit, attrs }) {
@ -34,6 +35,7 @@ export default defineComponent({
rtl, rtl,
inlineCollapsed, inlineCollapsed,
siderCollapsed, siderCollapsed,
onItemClick,
} = useInjectMenu(); } = useInjectMenu();
const firstLevel = useInjectFirstLevel(); const firstLevel = useInjectFirstLevel();
const isActive = ref(false); const isActive = ref(false);
@ -56,6 +58,27 @@ export default defineComponent({
[`${itemCls}-disabled`]: mergedDisabled.value, [`${itemCls}-disabled`]: mergedDisabled.value,
}; };
}); });
const getEventInfo = (e: MouseEvent): MenuInfo => {
return {
key: key,
eventKey: eventKey,
eventKeyPath: [...parentEventKeys.value, key],
domEvent: e,
};
};
// ============================ Events ============================
const onInternalClick = (e: MouseEvent) => {
if (mergedDisabled.value) {
return;
}
const info = getEventInfo(e);
emit('click', e);
onItemClick(info);
};
const onMouseEnter = (event: MouseEvent) => { const onMouseEnter = (event: MouseEvent) => {
if (!mergedDisabled.value) { if (!mergedDisabled.value) {
changeActiveKeys([...parentEventKeys.value, key]); changeActiveKeys([...parentEventKeys.value, key]);
@ -135,6 +158,7 @@ export default defineComponent({
{...optionRoleProps} {...optionRoleProps}
onMouseenter={onMouseEnter} onMouseenter={onMouseEnter}
onMouseleave={onMouseLeave} onMouseleave={onMouseLeave}
onClick={onInternalClick}
title={typeof title === 'string' ? title : undefined} title={typeof title === 'string' ? title : undefined}
> >
{cloneElement(icon, { {cloneElement(icon, {

View File

@ -9,7 +9,13 @@ import {
Ref, Ref,
UnwrapRef, UnwrapRef,
} from 'vue'; } from 'vue';
import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface'; import {
BuiltinPlacements,
MenuClickEventHandler,
MenuMode,
MenuTheme,
TriggerSubMenuAction,
} from '../interface';
import { CSSMotionProps } from '../../../_util/transition'; import { CSSMotionProps } from '../../../_util/transition';
export interface StoreMenuInfo { export interface StoreMenuInfo {
@ -77,7 +83,7 @@ export interface MenuContextProps {
// expandIcon?: RenderIconType; // expandIcon?: RenderIconType;
// // Function // // Function
// onItemClick: MenuClickEventHandler; onItemClick: MenuClickEventHandler;
onOpenChange: (key: Key, open: boolean) => void; onOpenChange: (key: Key, open: boolean) => void;
getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>; getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>;
} }

View File

@ -1,3 +1,5 @@
import { Key } from 'ant-design-vue/es/_util/type';
export type MenuTheme = 'light' | 'dark'; export type MenuTheme = 'light' | 'dark';
// ========================== Basic ========================== // ========================== Basic ==========================
@ -17,22 +19,24 @@ export interface RenderIconInfo {
export type RenderIconType = (props: RenderIconInfo) => any; export type RenderIconType = (props: RenderIconInfo) => any;
export interface MenuInfo { export interface MenuInfo {
key: string; key: Key;
keyPath: string[]; eventKey: string;
keyPath?: string[];
eventKeyPath: Key[];
domEvent: MouseEvent | KeyboardEvent; domEvent: MouseEvent | KeyboardEvent;
} }
export interface MenuTitleInfo { export interface MenuTitleInfo {
key: string; key: Key;
domEvent: MouseEvent | KeyboardEvent; domEvent: MouseEvent | KeyboardEvent;
} }
// ========================== Hover ========================== // ========================== Hover ==========================
export type MenuHoverEventHandler = (info: { key: string; domEvent: MouseEvent }) => void; export type MenuHoverEventHandler = (info: { key: Key; domEvent: MouseEvent }) => void;
// ======================== Selection ======================== // ======================== Selection ========================
export interface SelectInfo extends MenuInfo { export interface SelectInfo extends MenuInfo {
selectedKeys: string[]; selectedKeys: Key[];
} }
export type SelectEventHandler = (info: SelectInfo) => void; export type SelectEventHandler = (info: SelectInfo) => void;

2
v2-doc

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