Skip to content

Commit c4a61f2

Browse files
committed
feat: menu add itemsType
1 parent 0244e7f commit c4a61f2

File tree

6 files changed

+234
-16
lines changed

6 files changed

+234
-16
lines changed

components/menu/src/Menu.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Key } from '../../_util/type';
22
import type { ExtractPropTypes, PropType, VNode } from 'vue';
33
import {
4+
shallowRef,
45
Teleport,
56
computed,
67
defineComponent,
@@ -38,10 +39,14 @@ import { cloneElement } from '../../_util/vnode';
3839
import { OVERFLOW_KEY, PathContext } from './hooks/useKeyPath';
3940
import type { FocusEventHandler, MouseEventHandler } from '../../_util/EventInterface';
4041
import collapseMotion from '../../_util/collapseMotion';
42+
import type { ItemType } from './hooks/useItems';
43+
import useItems from './hooks/useItems';
4144

4245
export const menuProps = () => ({
4346
id: String,
4447
prefixCls: String,
48+
// donot use items, now only support inner use
49+
items: Array as PropType<ItemType[]>,
4550
disabled: Boolean,
4651
inlineCollapsed: Boolean,
4752
disabledOverflow: Boolean,
@@ -90,15 +95,15 @@ export default defineComponent({
9095
slots: ['expandIcon', 'overflowedIndicator'],
9196
setup(props, { slots, emit, attrs }) {
9297
const { prefixCls, direction, getPrefixCls } = useConfigInject('menu', props);
93-
const store = ref<Record<string, StoreMenuInfo>>({});
98+
const store = shallowRef<Map<string, StoreMenuInfo>>(new Map());
9499
const siderCollapsed = inject(SiderCollapsedKey, ref(undefined));
95100
const inlineCollapsed = computed(() => {
96101
if (siderCollapsed.value !== undefined) {
97102
return siderCollapsed.value;
98103
}
99104
return props.inlineCollapsed;
100105
});
101-
106+
const { itemsNodes } = useItems(props);
102107
const isMounted = ref(false);
103108
onMounted(() => {
104109
isMounted.value = true;
@@ -115,6 +120,11 @@ export default defineComponent({
115120
'Menu',
116121
'`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.',
117122
);
123+
// devWarning(
124+
// !!props.items && !slots.default,
125+
// 'Menu',
126+
// '`children` will be removed in next major version. Please use `items` instead.',
127+
// );
118128
});
119129

120130
const activeKeys = ref([]);
@@ -124,7 +134,7 @@ export default defineComponent({
124134
store,
125135
() => {
126136
const newKeyMapStore = {};
127-
for (const menuInfo of Object.values(store.value)) {
137+
for (const menuInfo of store.value.values()) {
128138
newKeyMapStore[menuInfo.key] = menuInfo;
129139
}
130140
keyMapStore.value = newKeyMapStore;
@@ -322,8 +332,8 @@ export default defineComponent({
322332
const keys = [];
323333
const storeValue = store.value;
324334
eventKeys.forEach(eventKey => {
325-
const { key, childrenEventKeys } = storeValue[eventKey];
326-
keys.push(key, ...getChildrenKeys(childrenEventKeys));
335+
const { key, childrenEventKeys } = storeValue.get(eventKey);
336+
keys.push(key, ...getChildrenKeys(unref(childrenEventKeys)));
327337
});
328338
return keys;
329339
};
@@ -355,11 +365,12 @@ export default defineComponent({
355365
};
356366

357367
const registerMenuInfo = (key: string, info: StoreMenuInfo) => {
358-
store.value = { ...store.value, [key]: info as any };
368+
store.value.set(key, info);
369+
store.value = new Map(store.value);
359370
};
360371
const unRegisterMenuInfo = (key: string) => {
361-
delete store.value[key];
362-
store.value = { ...store.value };
372+
store.value.delete(key);
373+
store.value = new Map(store.value);
363374
};
364375

365376
const lastVisibleIndex = ref(0);
@@ -379,7 +390,6 @@ export default defineComponent({
379390
: null,
380391
);
381392
useProvideMenu({
382-
store,
383393
prefixCls,
384394
activeKeys,
385395
openKeys: mergedOpenKeys,
@@ -408,9 +418,10 @@ export default defineComponent({
408418
isRootMenu: ref(true),
409419
expandIcon,
410420
forceSubMenuRender: computed(() => props.forceSubMenuRender),
421+
rootClassName: computed(() => ''),
411422
});
412423
return () => {
413-
const childList = flattenChildren(slots.default?.());
424+
const childList = itemsNodes.value || flattenChildren(slots.default?.());
414425
const allVisible =
415426
lastVisibleIndex.value >= childList.length - 1 ||
416427
mergedMode.value !== 'horizontal' ||

components/menu/src/PopupTrigger.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export default defineComponent({
4242
forceSubMenuRender,
4343
motion,
4444
defaultMotions,
45+
rootClassName,
4546
} = useInjectMenu();
4647
const forceRender = useInjectForceRender();
4748
const placement = computed(() =>
@@ -86,6 +87,7 @@ export default defineComponent({
8687
[`${prefixCls}-rtl`]: rtl.value,
8788
},
8889
popupClassName,
90+
rootClassName.value,
8991
)}
9092
stretch={mode === 'horizontal' ? 'minWidth' : null}
9193
getPopupContainer={

components/menu/src/SubMenu.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import devWarning from '../../vc-util/devWarning';
2121
import isValid from '../../_util/isValid';
2222
import type { MouseEventHandler } from '../../_util/EventInterface';
2323
import type { Key } from 'ant-design-vue/es/_util/type';
24+
import type { MenuTheme } from './interface';
2425

2526
let indexGuid = 0;
2627

@@ -34,6 +35,7 @@ export const subMenuProps = () => ({
3435
internalPopupClose: Boolean,
3536
eventKey: String,
3637
expandIcon: Function as PropType<(p?: { isOpen: boolean; [key: string]: any }) => any>,
38+
theme: String as PropType<MenuTheme>,
3739
onMouseenter: Function as PropType<MouseEventHandler>,
3840
onMouseleave: Function as PropType<MouseEventHandler>,
3941
onTitleClick: Function as PropType<(e: MouseEvent, key: Key) => void>,
@@ -193,7 +195,7 @@ export default defineComponent({
193195
const popupClassName = computed(() =>
194196
classNames(
195197
prefixCls.value,
196-
`${prefixCls.value}-${antdMenuTheme.value}`,
198+
`${prefixCls.value}-${props.theme || antdMenuTheme.value}`,
197199
props.popupClassName,
198200
),
199201
);
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import type {
2+
MenuItemType as VcMenuItemType,
3+
MenuDividerType as VcMenuDividerType,
4+
SubMenuType as VcSubMenuType,
5+
MenuItemGroupType as VcMenuItemGroupType,
6+
} from '../interface';
7+
import SubMenu from '../SubMenu';
8+
import ItemGroup from '../ItemGroup';
9+
import MenuDivider from '../Divider';
10+
import MenuItem from '../MenuItem';
11+
import type { Key } from '../../../_util/type';
12+
import { ref, shallowRef, watch } from 'vue';
13+
import type { MenuProps } from '../Menu';
14+
import type { StoreMenuInfo } from './useMenuContext';
15+
16+
interface MenuItemType extends VcMenuItemType {
17+
danger?: boolean;
18+
icon?: any;
19+
title?: string;
20+
}
21+
22+
interface SubMenuType extends Omit<VcSubMenuType, 'children'> {
23+
icon?: any;
24+
theme?: 'dark' | 'light';
25+
children: ItemType[];
26+
}
27+
28+
interface MenuItemGroupType extends Omit<VcMenuItemGroupType, 'children'> {
29+
children?: MenuItemType[];
30+
key?: Key;
31+
}
32+
33+
interface MenuDividerType extends VcMenuDividerType {
34+
dashed?: boolean;
35+
key?: Key;
36+
}
37+
38+
export type ItemType = MenuItemType | SubMenuType | MenuItemGroupType | MenuDividerType | null;
39+
40+
function convertItemsToNodes(
41+
list: ItemType[],
42+
store: Map<string, StoreMenuInfo>,
43+
parentMenuInfo?: {
44+
childrenEventKeys: string[];
45+
parentKeys: string[];
46+
},
47+
) {
48+
return (list || [])
49+
.map((opt, index) => {
50+
if (opt && typeof opt === 'object') {
51+
const { label, children, key, type, ...restProps } = opt as any;
52+
const mergedKey = key ?? `tmp-${index}`;
53+
// 此处 eventKey === key, 移除 children 后可以移除 eventKey
54+
const parentKeys = parentMenuInfo ? parentMenuInfo.parentKeys.slice() : [];
55+
const childrenEventKeys = [];
56+
// if
57+
const menuInfo = {
58+
eventKey: mergedKey,
59+
key: mergedKey,
60+
parentEventKeys: ref<string[]>(parentKeys),
61+
parentKeys: ref<string[]>(parentKeys),
62+
childrenEventKeys: ref<string[]>(childrenEventKeys),
63+
isLeaf: false,
64+
};
65+
66+
// MenuItemGroup & SubMenuItem
67+
if (children || type === 'group') {
68+
if (type === 'group') {
69+
const childrenNodes = convertItemsToNodes(children, store, parentMenuInfo);
70+
// Group
71+
return (
72+
<ItemGroup key={mergedKey} {...restProps} title={label}>
73+
{childrenNodes}
74+
</ItemGroup>
75+
);
76+
}
77+
store.set(mergedKey, menuInfo);
78+
if (parentMenuInfo) {
79+
parentMenuInfo.childrenEventKeys.push(mergedKey);
80+
}
81+
// Sub Menu
82+
const childrenNodes = convertItemsToNodes(children, store, {
83+
childrenEventKeys,
84+
parentKeys: [].concat(parentKeys, mergedKey),
85+
});
86+
return (
87+
<SubMenu key={mergedKey} {...restProps} title={label}>
88+
{childrenNodes}
89+
</SubMenu>
90+
);
91+
}
92+
93+
// MenuItem & Divider
94+
if (type === 'divider') {
95+
return <MenuDivider key={mergedKey} {...restProps} />;
96+
}
97+
menuInfo.isLeaf = true;
98+
store.set(mergedKey, menuInfo);
99+
return (
100+
<MenuItem key={mergedKey} {...restProps}>
101+
{label}
102+
</MenuItem>
103+
);
104+
}
105+
106+
return null;
107+
})
108+
.filter(opt => opt);
109+
}
110+
111+
// FIXME: Move logic here in v4
112+
/**
113+
* We simply convert `items` to VueNode for reuse origin component logic. But we need move all the
114+
* logic from component into this hooks when in v4
115+
*/
116+
export default function useItems(props: MenuProps) {
117+
const itemsNodes = shallowRef([]);
118+
const hasItmes = ref(false);
119+
const store = shallowRef<Map<string, StoreMenuInfo>>(new Map());
120+
watch(
121+
() => props.items,
122+
() => {
123+
const newStore = new Map<string, StoreMenuInfo>();
124+
hasItmes.value = false;
125+
if (props.items) {
126+
hasItmes.value = true;
127+
itemsNodes.value = convertItemsToNodes(props.items as ItemType[], newStore);
128+
} else {
129+
itemsNodes.value = undefined;
130+
}
131+
store.value = newStore;
132+
},
133+
{ immediate: true, deep: true },
134+
);
135+
return { itemsNodes, store, hasItmes };
136+
}

components/menu/src/hooks/useMenuContext.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Key } from '../../../_util/type';
2-
import type { ComputedRef, InjectionKey, PropType, Ref, UnwrapRef } from 'vue';
2+
import type { ComputedRef, InjectionKey, PropType, Ref } from 'vue';
33
import { defineComponent, inject, provide, toRef } from 'vue';
44
import type {
55
BuiltinPlacements,
@@ -13,15 +13,14 @@ import type { CSSMotionProps } from '../../../_util/transition';
1313
export interface StoreMenuInfo {
1414
eventKey: string;
1515
key: Key;
16-
parentEventKeys: ComputedRef<string[]>;
16+
parentEventKeys: Ref<string[]>;
1717
childrenEventKeys?: Ref<string[]>;
1818
isLeaf?: boolean;
19-
parentKeys: ComputedRef<Key[]>;
19+
parentKeys: Ref<Key[]>;
2020
}
2121
export interface MenuContextProps {
2222
isRootMenu: Ref<boolean>;
23-
24-
store: Ref<Record<string, UnwrapRef<StoreMenuInfo>>>;
23+
rootClassName: Ref<string>;
2524
registerMenuInfo: (key: string, info: StoreMenuInfo) => void;
2625
unRegisterMenuInfo: (key: string) => void;
2726
prefixCls: ComputedRef<string>;

components/menu/src/interface.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,74 @@
1+
import type { CSSProperties } from 'vue';
12
import type { Key } from '../../_util/type';
23
import type { MenuItemProps } from './MenuItem';
34

5+
// ========================= Options =========================
6+
interface ItemSharedProps {
7+
style?: CSSProperties;
8+
class?: string;
9+
}
10+
11+
export interface SubMenuType extends ItemSharedProps {
12+
label?: any;
13+
14+
children: ItemType[];
15+
16+
disabled?: boolean;
17+
18+
key: string;
19+
20+
rootClassName?: string;
21+
22+
// >>>>> Icon
23+
itemIcon?: RenderIconType;
24+
expandIcon?: RenderIconType;
25+
26+
// >>>>> Active
27+
onMouseenter?: MenuHoverEventHandler;
28+
onMouseleave?: MenuHoverEventHandler;
29+
30+
// >>>>> Popup
31+
popupClassName?: string;
32+
popupOffset?: number[];
33+
34+
// >>>>> Events
35+
onClick?: MenuClickEventHandler;
36+
onTitleClick?: (info: MenuTitleInfo) => void;
37+
onTitleMouseenter?: MenuHoverEventHandler;
38+
onTitleMouseleave?: MenuHoverEventHandler;
39+
}
40+
41+
export interface MenuItemType extends ItemSharedProps {
42+
label?: any;
43+
44+
disabled?: boolean;
45+
46+
itemIcon?: RenderIconType;
47+
48+
key: Key;
49+
50+
// >>>>> Active
51+
onMouseenter?: MenuHoverEventHandler;
52+
onMouseleave?: MenuHoverEventHandler;
53+
54+
// >>>>> Events
55+
onClick?: MenuClickEventHandler;
56+
}
57+
58+
export interface MenuItemGroupType extends ItemSharedProps {
59+
type: 'group';
60+
61+
label?: any;
62+
63+
children?: ItemType[];
64+
}
65+
66+
export interface MenuDividerType extends ItemSharedProps {
67+
type: 'divider';
68+
}
69+
70+
export type ItemType = SubMenuType | MenuItemType | MenuItemGroupType | MenuDividerType | null;
71+
472
export type MenuTheme = 'light' | 'dark';
573

674
// ========================== Basic ==========================

0 commit comments

Comments
 (0)