feat: menu add itemsType
parent
0244e7f5b4
commit
c4a61f210f
|
@ -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' ||
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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 };
|
||||
}
|
|
@ -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>;
|
||||
|
|
|
@ -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 ==========================
|
||||
|
|
Loading…
Reference in New Issue