feat: menu add itemsType

pull/5820/head
tangjinzhou 2022-05-11 09:58:23 +08:00
parent 0244e7f5b4
commit c4a61f210f
6 changed files with 234 additions and 16 deletions

View File

@ -1,6 +1,7 @@
import type { Key } from '../../_util/type';
import type { ExtractPropTypes, PropType, VNode } from 'vue';
import {
shallowRef,
Teleport,
computed,
defineComponent,
@ -38,10 +39,14 @@ import { cloneElement } from '../../_util/vnode';
import { OVERFLOW_KEY, PathContext } from './hooks/useKeyPath';
import type { FocusEventHandler, MouseEventHandler } from '../../_util/EventInterface';
import collapseMotion from '../../_util/collapseMotion';
import type { ItemType } from './hooks/useItems';
import useItems from './hooks/useItems';
export const menuProps = () => ({
id: String,
prefixCls: String,
// donot use items, now only support inner use
items: Array as PropType<ItemType[]>,
disabled: Boolean,
inlineCollapsed: Boolean,
disabledOverflow: Boolean,
@ -90,7 +95,7 @@ export default defineComponent({
slots: ['expandIcon', 'overflowedIndicator'],
setup(props, { slots, emit, attrs }) {
const { prefixCls, direction, getPrefixCls } = useConfigInject('menu', props);
const store = ref<Record<string, StoreMenuInfo>>({});
const store = shallowRef<Map<string, StoreMenuInfo>>(new Map());
const siderCollapsed = inject(SiderCollapsedKey, ref(undefined));
const inlineCollapsed = computed(() => {
if (siderCollapsed.value !== undefined) {
@ -98,7 +103,7 @@ export default defineComponent({
}
return props.inlineCollapsed;
});
const { itemsNodes } = useItems(props);
const isMounted = ref(false);
onMounted(() => {
isMounted.value = true;
@ -115,6 +120,11 @@ export default defineComponent({
'Menu',
'`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.',
);
// devWarning(
// !!props.items && !slots.default,
// 'Menu',
// '`children` will be removed in next major version. Please use `items` instead.',
// );
});
const activeKeys = ref([]);
@ -124,7 +134,7 @@ export default defineComponent({
store,
() => {
const newKeyMapStore = {};
for (const menuInfo of Object.values(store.value)) {
for (const menuInfo of store.value.values()) {
newKeyMapStore[menuInfo.key] = menuInfo;
}
keyMapStore.value = newKeyMapStore;
@ -322,8 +332,8 @@ export default defineComponent({
const keys = [];
const storeValue = store.value;
eventKeys.forEach(eventKey => {
const { key, childrenEventKeys } = storeValue[eventKey];
keys.push(key, ...getChildrenKeys(childrenEventKeys));
const { key, childrenEventKeys } = storeValue.get(eventKey);
keys.push(key, ...getChildrenKeys(unref(childrenEventKeys)));
});
return keys;
};
@ -355,11 +365,12 @@ export default defineComponent({
};
const registerMenuInfo = (key: string, info: StoreMenuInfo) => {
store.value = { ...store.value, [key]: info as any };
store.value.set(key, info);
store.value = new Map(store.value);
};
const unRegisterMenuInfo = (key: string) => {
delete store.value[key];
store.value = { ...store.value };
store.value.delete(key);
store.value = new Map(store.value);
};
const lastVisibleIndex = ref(0);
@ -379,7 +390,6 @@ export default defineComponent({
: null,
);
useProvideMenu({
store,
prefixCls,
activeKeys,
openKeys: mergedOpenKeys,
@ -408,9 +418,10 @@ export default defineComponent({
isRootMenu: ref(true),
expandIcon,
forceSubMenuRender: computed(() => props.forceSubMenuRender),
rootClassName: computed(() => ''),
});
return () => {
const childList = flattenChildren(slots.default?.());
const childList = itemsNodes.value || flattenChildren(slots.default?.());
const allVisible =
lastVisibleIndex.value >= childList.length - 1 ||
mergedMode.value !== 'horizontal' ||

View File

@ -42,6 +42,7 @@ export default defineComponent({
forceSubMenuRender,
motion,
defaultMotions,
rootClassName,
} = useInjectMenu();
const forceRender = useInjectForceRender();
const placement = computed(() =>
@ -86,6 +87,7 @@ export default defineComponent({
[`${prefixCls}-rtl`]: rtl.value,
},
popupClassName,
rootClassName.value,
)}
stretch={mode === 'horizontal' ? 'minWidth' : null}
getPopupContainer={

View File

@ -21,6 +21,7 @@ import devWarning from '../../vc-util/devWarning';
import isValid from '../../_util/isValid';
import type { MouseEventHandler } from '../../_util/EventInterface';
import type { Key } from 'ant-design-vue/es/_util/type';
import type { MenuTheme } from './interface';
let indexGuid = 0;
@ -34,6 +35,7 @@ export const subMenuProps = () => ({
internalPopupClose: Boolean,
eventKey: String,
expandIcon: Function as PropType<(p?: { isOpen: boolean; [key: string]: any }) => any>,
theme: String as PropType<MenuTheme>,
onMouseenter: Function as PropType<MouseEventHandler>,
onMouseleave: Function as PropType<MouseEventHandler>,
onTitleClick: Function as PropType<(e: MouseEvent, key: Key) => void>,
@ -193,7 +195,7 @@ export default defineComponent({
const popupClassName = computed(() =>
classNames(
prefixCls.value,
`${prefixCls.value}-${antdMenuTheme.value}`,
`${prefixCls.value}-${props.theme || antdMenuTheme.value}`,
props.popupClassName,
),
);

View File

@ -0,0 +1,136 @@
import type {
MenuItemType as VcMenuItemType,
MenuDividerType as VcMenuDividerType,
SubMenuType as VcSubMenuType,
MenuItemGroupType as VcMenuItemGroupType,
} from '../interface';
import SubMenu from '../SubMenu';
import ItemGroup from '../ItemGroup';
import MenuDivider from '../Divider';
import MenuItem from '../MenuItem';
import type { Key } from '../../../_util/type';
import { ref, shallowRef, watch } from 'vue';
import type { MenuProps } from '../Menu';
import type { StoreMenuInfo } from './useMenuContext';
interface MenuItemType extends VcMenuItemType {
danger?: boolean;
icon?: any;
title?: string;
}
interface SubMenuType extends Omit<VcSubMenuType, 'children'> {
icon?: any;
theme?: 'dark' | 'light';
children: ItemType[];
}
interface MenuItemGroupType extends Omit<VcMenuItemGroupType, 'children'> {
children?: MenuItemType[];
key?: Key;
}
interface MenuDividerType extends VcMenuDividerType {
dashed?: boolean;
key?: Key;
}
export type ItemType = MenuItemType | SubMenuType | MenuItemGroupType | MenuDividerType | null;
function convertItemsToNodes(
list: ItemType[],
store: Map<string, StoreMenuInfo>,
parentMenuInfo?: {
childrenEventKeys: string[];
parentKeys: string[];
},
) {
return (list || [])
.map((opt, index) => {
if (opt && typeof opt === 'object') {
const { label, children, key, type, ...restProps } = opt as any;
const mergedKey = key ?? `tmp-${index}`;
// eventKey === key, children eventKey
const parentKeys = parentMenuInfo ? parentMenuInfo.parentKeys.slice() : [];
const childrenEventKeys = [];
// if
const menuInfo = {
eventKey: mergedKey,
key: mergedKey,
parentEventKeys: ref<string[]>(parentKeys),
parentKeys: ref<string[]>(parentKeys),
childrenEventKeys: ref<string[]>(childrenEventKeys),
isLeaf: false,
};
// MenuItemGroup & SubMenuItem
if (children || type === 'group') {
if (type === 'group') {
const childrenNodes = convertItemsToNodes(children, store, parentMenuInfo);
// Group
return (
<ItemGroup key={mergedKey} {...restProps} title={label}>
{childrenNodes}
</ItemGroup>
);
}
store.set(mergedKey, menuInfo);
if (parentMenuInfo) {
parentMenuInfo.childrenEventKeys.push(mergedKey);
}
// Sub Menu
const childrenNodes = convertItemsToNodes(children, store, {
childrenEventKeys,
parentKeys: [].concat(parentKeys, mergedKey),
});
return (
<SubMenu key={mergedKey} {...restProps} title={label}>
{childrenNodes}
</SubMenu>
);
}
// MenuItem & Divider
if (type === 'divider') {
return <MenuDivider key={mergedKey} {...restProps} />;
}
menuInfo.isLeaf = true;
store.set(mergedKey, menuInfo);
return (
<MenuItem key={mergedKey} {...restProps}>
{label}
</MenuItem>
);
}
return null;
})
.filter(opt => opt);
}
// FIXME: Move logic here in v4
/**
* We simply convert `items` to VueNode for reuse origin component logic. But we need move all the
* logic from component into this hooks when in v4
*/
export default function useItems(props: MenuProps) {
const itemsNodes = shallowRef([]);
const hasItmes = ref(false);
const store = shallowRef<Map<string, StoreMenuInfo>>(new Map());
watch(
() => props.items,
() => {
const newStore = new Map<string, StoreMenuInfo>();
hasItmes.value = false;
if (props.items) {
hasItmes.value = true;
itemsNodes.value = convertItemsToNodes(props.items as ItemType[], newStore);
} else {
itemsNodes.value = undefined;
}
store.value = newStore;
},
{ immediate: true, deep: true },
);
return { itemsNodes, store, hasItmes };
}

View File

@ -1,5 +1,5 @@
import type { Key } from '../../../_util/type';
import type { ComputedRef, InjectionKey, PropType, Ref, UnwrapRef } from 'vue';
import type { ComputedRef, InjectionKey, PropType, Ref } from 'vue';
import { defineComponent, inject, provide, toRef } from 'vue';
import type {
BuiltinPlacements,
@ -13,15 +13,14 @@ import type { CSSMotionProps } from '../../../_util/transition';
export interface StoreMenuInfo {
eventKey: string;
key: Key;
parentEventKeys: ComputedRef<string[]>;
parentEventKeys: Ref<string[]>;
childrenEventKeys?: Ref<string[]>;
isLeaf?: boolean;
parentKeys: ComputedRef<Key[]>;
parentKeys: Ref<Key[]>;
}
export interface MenuContextProps {
isRootMenu: Ref<boolean>;
store: Ref<Record<string, UnwrapRef<StoreMenuInfo>>>;
rootClassName: Ref<string>;
registerMenuInfo: (key: string, info: StoreMenuInfo) => void;
unRegisterMenuInfo: (key: string) => void;
prefixCls: ComputedRef<string>;

View File

@ -1,6 +1,74 @@
import type { CSSProperties } from 'vue';
import type { Key } from '../../_util/type';
import type { MenuItemProps } from './MenuItem';
// ========================= Options =========================
interface ItemSharedProps {
style?: CSSProperties;
class?: string;
}
export interface SubMenuType extends ItemSharedProps {
label?: any;
children: ItemType[];
disabled?: boolean;
key: string;
rootClassName?: string;
// >>>>> Icon
itemIcon?: RenderIconType;
expandIcon?: RenderIconType;
// >>>>> Active
onMouseenter?: MenuHoverEventHandler;
onMouseleave?: MenuHoverEventHandler;
// >>>>> Popup
popupClassName?: string;
popupOffset?: number[];
// >>>>> Events
onClick?: MenuClickEventHandler;
onTitleClick?: (info: MenuTitleInfo) => void;
onTitleMouseenter?: MenuHoverEventHandler;
onTitleMouseleave?: MenuHoverEventHandler;
}
export interface MenuItemType extends ItemSharedProps {
label?: any;
disabled?: boolean;
itemIcon?: RenderIconType;
key: Key;
// >>>>> Active
onMouseenter?: MenuHoverEventHandler;
onMouseleave?: MenuHoverEventHandler;
// >>>>> Events
onClick?: MenuClickEventHandler;
}
export interface MenuItemGroupType extends ItemSharedProps {
type: 'group';
label?: any;
children?: ItemType[];
}
export interface MenuDividerType extends ItemSharedProps {
type: 'divider';
}
export type ItemType = SubMenuType | MenuItemType | MenuItemGroupType | MenuDividerType | null;
export type MenuTheme = 'light' | 'dark';
// ========================== Basic ==========================