refactor: menu

feat-new-menu
tangjinzhou 2021-05-16 23:09:47 +08:00
parent 2ab77978f2
commit 16f051d593
13 changed files with 533 additions and 58 deletions

View File

@ -1,4 +1,11 @@
import { defineComponent, nextTick, Transition as T, TransitionGroup as TG } from 'vue';
import {
BaseTransitionProps,
CSSProperties,
defineComponent,
nextTick,
Transition as T,
TransitionGroup as TG,
} from 'vue';
import { findDOMNode } from './props-util';
export const getTransitionProps = (transitionName: string, opt: object = {}) => {
@ -80,6 +87,37 @@ if (process.env.NODE_ENV === 'test') {
});
}
export { Transition, TransitionGroup };
export declare type MotionEvent = (TransitionEvent | AnimationEvent) & {
deadline?: boolean;
};
export declare type MotionEventHandler = (
element: HTMLElement,
done?: () => void,
) => CSSProperties | void;
export declare type MotionEndEventHandler = (
element: HTMLElement,
done?: () => void,
) => boolean | void;
// ================== Collapse Motion ==================
const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 });
const getRealHeight: MotionEventHandler = node => ({ height: node.scrollHeight, opacity: 1 });
const getCurrentHeight: MotionEventHandler = node => ({ height: node.offsetHeight });
// const skipOpacityTransition: MotionEndEventHandler = (_, event) =>
// (event as TransitionEvent).propertyName === 'height';
const collapseMotion: BaseTransitionProps<HTMLElement> = {
// motionName: 'ant-motion-collapse',
appear: true,
// onAppearStart: getCollapsedHeight,
onBeforeEnter: getCollapsedHeight,
onEnter: getRealHeight,
onBeforeLeave: getCurrentHeight,
onLeave: getCollapsedHeight,
};
export { Transition, TransitionGroup, collapseMotion };
export default Transition;

View File

@ -0,0 +1,50 @@
import { computed, defineComponent, ref, watch } from '@vue/runtime-core';
import { useInjectMenu, MenuContextProvider } from './hooks/useMenuContext';
import { MenuMode } from './interface';
import SubMenuList from './SubMenuList';
export default defineComponent({
name: 'InlineSubMenuList',
inheritAttrs: false,
props: {
id: String,
open: Boolean,
keyPath: Array,
},
setup(props, { slots }) {
const fixedMode: MenuMode = 'inline';
const { prefixCls, forceSubMenuRender, motion, mode } = useInjectMenu();
const sameModeRef = computed(() => mode.value === fixedMode);
const destroy = ref(!sameModeRef.value);
// ================================= Effect =================================
// Reset destroy state when mode change back
watch(
mode,
() => {
if (sameModeRef.value) {
destroy.value = false;
}
},
{ flush: 'post' },
);
let transitionProps = computed(() => {
return { appear: props.keyPath.length > 1, css: false };
});
return () => {
if (destroy.value) {
return null;
}
return (
<MenuContextProvider
props={{
mode: fixedMode,
locked: !sameModeRef.value,
}}
>
<SubMenuList id={props.id}>{slots.default?.()}</SubMenuList>
</MenuContextProvider>
);
};
},
});

View File

@ -8,13 +8,14 @@ export default defineComponent({
props: {
title: PropTypes.VNodeChild,
},
inheritAttrs: false,
slots: ['title'],
setup(props, { slots }) {
setup(props, { slots, attrs }) {
const { prefixCls } = useInjectMenu();
const groupPrefixCls = computed(() => `${prefixCls.value}-item-group`);
return () => {
return (
<li onClick={e => e.stopPropagation()} class={groupPrefixCls.value}>
<li {...attrs} onClick={e => e.stopPropagation()} class={groupPrefixCls.value}>
<div
title={typeof props.title === 'string' ? props.title : undefined}
class={`${groupPrefixCls.value}-title`}

View File

@ -1,14 +1,36 @@
import { Key } from '../../_util/type';
import { computed, defineComponent, ExtractPropTypes, ref, PropType } from 'vue';
import useProvideMenu from './hooks/useMenuContext';
import {
computed,
defineComponent,
ExtractPropTypes,
ref,
PropType,
inject,
watchEffect,
} from 'vue';
import useProvideMenu, { useProvideFirstLevel } from './hooks/useMenuContext';
import useConfigInject from '../../_util/hooks/useConfigInject';
import { MenuTheme, MenuMode } from './interface';
import { MenuTheme, MenuMode, BuiltinPlacements, TriggerSubMenuAction } from './interface';
import devWarning from 'ant-design-vue/es/vc-util/devWarning';
import { collapseMotion } from 'ant-design-vue/es/_util/transition';
export const menuProps = {
prefixCls: String,
disabled: Boolean,
inlineCollapsed: Boolean,
theme: { type: String as PropType<MenuTheme>, default: 'light' },
mode: { type: String as PropType<MenuMode>, default: 'vertical' },
inlineIndent: { type: Number, default: 24 },
subMenuOpenDelay: { type: Number, default: 0.1 },
subMenuCloseDelay: { type: Number, default: 0.1 },
builtinPlacements: { type: Object as PropType<BuiltinPlacements> },
triggerSubMenuAction: { type: String as PropType<TriggerSubMenuAction>, default: 'hover' },
getPopupContainer: Function as PropType<(node: HTMLElement) => HTMLElement>,
};
export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
@ -18,6 +40,33 @@ export default defineComponent({
props: menuProps,
setup(props, { slots }) {
const { prefixCls, direction } = useConfigInject('menu', props);
const siderCollapsed = inject(
'layoutSiderCollapsed',
computed(() => undefined),
);
const inlineCollapsed = computed(() => {
const { inlineCollapsed } = props;
if (siderCollapsed.value !== undefined) {
return siderCollapsed.value;
}
return inlineCollapsed;
});
watchEffect(() => {
devWarning(
!('inlineCollapsed' in props && props.mode !== 'inline'),
'Menu',
'`inlineCollapsed` should only be used when `mode` is inline.',
);
devWarning(
!(siderCollapsed.value !== undefined && 'inlineCollapsed' in props),
'Menu',
'`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.',
);
});
const activeKeys = ref([]);
const openKeys = ref([]);
const selectedKeys = ref([]);
@ -25,10 +74,18 @@ export default defineComponent({
activeKeys.value = keys;
};
const disabled = computed(() => !!props.disabled);
useProvideMenu({ prefixCls, activeKeys, openKeys, selectedKeys, changeActiveKeys, disabled });
const isRtl = computed(() => direction.value === 'rtl');
const mergedMode = ref('vertical');
const mergedMode = ref<MenuMode>('vertical');
const mergedInlineCollapsed = ref(false);
watchEffect(() => {
if (props.mode === 'inline' && inlineCollapsed.value) {
mergedMode.value = 'vertical';
mergedInlineCollapsed.value = inlineCollapsed.value;
}
mergedMode.value = props.mode;
mergedInlineCollapsed.value = false;
});
const className = computed(() => {
return {
[`${prefixCls.value}`]: true,
@ -39,6 +96,35 @@ export default defineComponent({
[`${prefixCls.value}-${props.theme}`]: true,
};
});
const defaultMotions = {
horizontal: { motionName: `ant-slide-up` },
inline: collapseMotion,
other: { motionName: `ant-zoom-big` },
};
useProvideFirstLevel(true);
useProvideMenu({
prefixCls,
activeKeys,
openKeys,
selectedKeys,
changeActiveKeys,
disabled,
rtl: isRtl,
mode: mergedMode,
inlineIndent: computed(() => props.inlineIndent),
subMenuCloseDelay: computed(() => props.subMenuCloseDelay),
subMenuOpenDelay: computed(() => props.subMenuOpenDelay),
builtinPlacements: computed(() => props.builtinPlacements),
triggerSubMenuAction: computed(() => props.triggerSubMenuAction),
getPopupContainer: computed(() => props.getPopupContainer),
inlineCollapsed: mergedInlineCollapsed,
antdMenuTheme: computed(() => props.theme),
siderCollapsed,
defaultMotions,
});
return () => {
return <ul class={className.value}>{slots.default?.()}</ul>;
};

View File

@ -1,6 +1,10 @@
import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util';
import PropTypes from '../../_util/vue-types';
import { computed, defineComponent, getCurrentInstance, ref, watch } from 'vue';
import { useInjectKeyPath } from './hooks/useKeyPath';
import { useInjectMenu } from './hooks/useMenuContext';
import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext';
import { cloneElement } from '../../_util/vnode';
import Tooltip from '../../tooltip';
let indexGuid = 0;
@ -9,15 +13,29 @@ export default defineComponent({
props: {
role: String,
disabled: Boolean,
danger: Boolean,
title: { type: [String, Boolean] },
icon: PropTypes.VNodeChild,
},
emits: ['mouseenter', 'mouseleave'],
setup(props, { slots, emit }) {
slots: ['icon'],
inheritAttrs: false,
setup(props, { slots, emit, attrs }) {
const instance = getCurrentInstance();
const key = instance.vnode.key;
const uniKey = `menu_item_${++indexGuid}`;
const parentKeys = useInjectKeyPath();
console.log(parentKeys.value);
const { prefixCls, activeKeys, disabled, changeActiveKeys } = useInjectMenu();
const {
prefixCls,
activeKeys,
disabled,
changeActiveKeys,
rtl,
inlineCollapsed,
siderCollapsed,
} = useInjectMenu();
const firstLevel = useInjectFirstLevel();
const isActive = ref(false);
watch(
activeKeys,
@ -32,6 +50,7 @@ export default defineComponent({
const itemCls = `${prefixCls.value}-item`;
return {
[`${itemCls}`]: true,
[`${itemCls}-danger`]: props.danger,
[`${itemCls}-active`]: isActive.value,
[`${itemCls}-selected`]: selected.value,
[`${itemCls}-disabled`]: mergedDisabled.value,
@ -50,26 +69,80 @@ export default defineComponent({
}
};
const renderItemChildren = (icon: any, children: any) => {
// inline-collapsed.md demo span , icon span
// ref: https://github.com/ant-design/ant-design/pull/23456
if (!icon || (isValidElement(children) && children.type === 'span')) {
if (children && inlineCollapsed.value && firstLevel && typeof children === 'string') {
return (
<div class={`${prefixCls.value}-inline-collapsed-noicon`}>{children.charAt(0)}</div>
);
}
return children;
}
return <span class={`${prefixCls.value}-title-content`}>{children}</span>;
};
return () => {
const { title } = props;
const children = flattenChildren(slots.default?.());
const childrenLength = children.length;
let tooltipTitle: any = title;
if (typeof title === 'undefined') {
tooltipTitle = firstLevel ? children : '';
} else if (title === false) {
tooltipTitle = '';
}
const tooltipProps: any = {
title: tooltipTitle,
};
if (!siderCollapsed.value && !inlineCollapsed.value) {
tooltipProps.title = null;
// Reset `visible` to fix control mode tooltip display not correct
// ref: https://github.com/ant-design/ant-design/issues/16742
tooltipProps.visible = false;
}
// ============================ Render ============================
const optionRoleProps = {};
if (props.role === 'option') {
optionRoleProps['aria-selected'] = selected.value;
}
const icon = getPropsSlot(slots, props, 'icon');
return (
<li
class={classNames.value}
role={props.role || 'menuitem'}
tabindex={props.disabled ? null : -1}
data-menu-id={key}
aria-disabled={props.disabled}
{...optionRoleProps}
onMouseenter={onMouseEnter}
onMouseleave={onMouseLeave}
<Tooltip
{...tooltipProps}
placement={rtl.value ? 'left' : 'right'}
overlayClassName={`${prefixCls.value}-inline-collapsed-tooltip`}
>
{slots.default?.()}
</li>
<li
{...attrs}
class={[
classNames.value,
{
[`${attrs.class}`]: !!attrs.class,
[`${prefixCls.value}-item-only-child`]:
(icon ? childrenLength + 1 : childrenLength) === 1,
},
]}
role={props.role || 'menuitem'}
tabindex={props.disabled ? null : -1}
data-menu-id={key}
aria-disabled={props.disabled}
{...optionRoleProps}
onMouseenter={onMouseEnter}
onMouseleave={onMouseLeave}
title={typeof title === 'string' ? title : undefined}
>
{cloneElement(icon, {
class: `${prefixCls.value}-item-icon`,
})}
{renderItemChildren(icon, children)}
</li>
</Tooltip>
);
};
},

View File

@ -0,0 +1,96 @@
import Trigger from '../../vc-trigger';
import { computed, defineComponent, onBeforeUnmount, PropType, ref, watch } from 'vue';
import { MenuMode } from './interface';
import { useInjectMenu } from './hooks/useMenuContext';
import { placements, placementsRtl } from './placements';
import raf from '../../_util/raf';
import classNames from '../../_util/classNames';
const popupPlacementMap = {
horizontal: 'bottomLeft',
vertical: 'rightTop',
'vertical-left': 'rightTop',
'vertical-right': 'leftTop',
};
export default defineComponent({
name: 'PopupTrigger',
props: {
prefixCls: String,
mode: String as PropType<MenuMode>,
visible: Boolean,
// popup: React.ReactNode;
popupClassName: String,
popupOffset: Array as PropType<number[]>,
disabled: Boolean,
onVisibleChange: Function as PropType<(visible: boolean) => void>,
},
slots: ['popup'],
emits: ['visibleChange'],
inheritAttrs: false,
setup(props, { slots, emit }) {
const innerVisible = ref(false);
const {
getPopupContainer,
rtl,
subMenuOpenDelay,
subMenuCloseDelay,
builtinPlacements,
triggerSubMenuAction,
} = useInjectMenu();
const placement = computed(() =>
rtl
? { ...placementsRtl, ...builtinPlacements.value }
: { ...placements, ...builtinPlacements.value },
);
const popupPlacement = computed(() => popupPlacementMap[props.mode]);
const visibleRef = ref<number>();
watch(
() => props.visible,
visible => {
raf.cancel(visibleRef.value);
visibleRef.value = raf(() => {
innerVisible.value = visible;
});
},
{ immediate: true },
);
onBeforeUnmount(() => {
raf.cancel(visibleRef.value);
});
const onVisibleChange = (visible: boolean) => {
emit('visibleChange', visible);
};
return () => {
const { prefixCls, popupClassName, mode, popupOffset, disabled } = props;
return (
<Trigger
prefixCls={prefixCls}
popupClassName={classNames(
`${prefixCls}-popup`,
{
[`${prefixCls}-rtl`]: rtl,
},
popupClassName,
)}
stretch={mode === 'horizontal' ? 'minWidth' : null}
getPopupContainer={getPopupContainer.value}
builtinPlacements={placement.value}
popupPlacement={popupPlacement.value}
popupVisible={innerVisible.value}
popupAlign={popupOffset && { offset: popupOffset }}
action={disabled ? [] : [triggerSubMenuAction.value]}
mouseEnterDelay={subMenuOpenDelay.value}
mouseLeaveDelay={subMenuCloseDelay.value}
onPopupVisibleChange={onVisibleChange}
// forceRender={forceSubMenuRender}
// popupMotion={mergedMotion}
v-slots={{ popup: slots.popup, default: slots.default }}
></Trigger>
);
};
},
});

View File

@ -1,12 +1,74 @@
import { defineComponent } from 'vue';
import useProvideKeyPath from './hooks/useKeyPath';
import PropTypes from '../../_util/vue-types';
import { computed, defineComponent } from 'vue';
import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath';
import { useInjectMenu, useProvideFirstLevel } from './hooks/useMenuContext';
import { getPropsSlot, isValidElement } from 'ant-design-vue/es/_util/props-util';
import classNames from 'ant-design-vue/es/_util/classNames';
export default defineComponent({
name: 'ASubMenu',
setup(props, { slots }) {
props: {
icon: PropTypes.VNodeChild,
title: PropTypes.VNodeChild,
disabled: Boolean,
level: Number,
popupClassName: String,
popupOffset: [Number, Number],
},
slots: ['icon', 'title'],
inheritAttrs: false,
setup(props, { slots, attrs }) {
useProvideKeyPath();
useProvideFirstLevel(false);
const keyPath = useInjectKeyPath();
const {
prefixCls,
activeKeys,
disabled,
changeActiveKeys,
rtl,
mode,
inlineCollapsed,
antdMenuTheme,
} = useInjectMenu();
const popupClassName = computed(() =>
classNames(prefixCls, `${prefixCls.value}-${antdMenuTheme.value}`, props.popupClassName),
);
const renderTitle = (title: any, icon: any) => {
if (!icon) {
return inlineCollapsed.value && props.level === 1 && title && typeof title === 'string' ? (
<div class={`${prefixCls.value}-inline-collapsed-noicon`}>{title.charAt(0)}</div>
) : (
title
);
}
// inline-collapsed.md demo span , icon span
// ref: https://github.com/ant-design/ant-design/pull/23456
const titleIsSpan = isValidElement(title) && title.type === 'span';
return (
<>
{icon}
{titleIsSpan ? title : <span class={`${prefixCls.value}-title-content`}>{title}</span>}
</>
);
};
const className = computed(() =>
classNames(
prefixCls.value,
`${prefixCls.value}-sub`,
`${prefixCls.value}-${mode.value === 'inline' ? 'inline' : 'vertical'}`,
),
);
return () => {
return <ul>{slots.default?.()}</ul>;
const icon = getPropsSlot(slots, props, 'icon');
const title = renderTitle(getPropsSlot(slots, props, 'title'), icon);
return (
<ul {...attrs} class={[className.value, attrs.class]} data-menu-list>
{slots.default?.()}
</ul>
);
};
},
});

View File

@ -0,0 +1,23 @@
import classNames from '../../_util/classNames';
import { FunctionalComponent, provide } from 'vue';
import { useInjectMenu } from './hooks/useMenuContext';
const InternalSubMenuList: FunctionalComponent<any> = (_props, { slots, attrs }) => {
const { prefixCls, mode } = useInjectMenu();
return (
<ul
{...attrs}
class={classNames(
prefixCls,
`${prefixCls}-sub`,
`${prefixCls}-${mode.value === 'inline' ? 'inline' : 'vertical'}`,
)}
data-menu-list
>
{slots.default?.()}
</ul>
);
};
InternalSubMenuList.displayName = 'SubMenuList';
export default InternalSubMenuList;

View File

@ -0,0 +1,14 @@
import { computed, ComputedRef, CSSProperties } from 'vue';
import { useInjectMenu } from './useMenuContext';
export default function useDirectionStyle(level: ComputedRef<number>): ComputedRef<CSSProperties> {
const { mode, rtl, inlineIndent } = useInjectMenu();
return computed(() =>
mode.value !== 'inline'
? null
: rtl.value
? { paddingRight: level.value * inlineIndent.value }
: { paddingLeft: level.value * inlineIndent.value },
);
}

View File

@ -1,22 +1,22 @@
import { Key } from '../../../_util/type';
import { ComputedRef, inject, InjectionKey, provide, Ref } from 'vue';
// import {
// BuiltinPlacements,
// MenuClickEventHandler,
// MenuMode,
// RenderIconType,
// TriggerSubMenuAction,
// } from '../interface';
import { ComputedRef, FunctionalComponent, inject, InjectionKey, provide, Ref } from 'vue';
import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface';
export interface MenuContextProps {
prefixCls: ComputedRef<string>;
openKeys: Ref<Key[]>;
selectedKeys: Ref<Key[]>;
// rtl?: boolean;
rtl?: ComputedRef<boolean>;
locked?: Ref<boolean>;
inlineCollapsed: Ref<boolean>;
antdMenuTheme?: ComputedRef<MenuTheme>;
siderCollapsed?: ComputedRef<boolean>;
// // Mode
// mode: MenuMode;
mode: Ref<MenuMode>;
// // Disabled
disabled?: ComputedRef<boolean>;
@ -33,18 +33,18 @@ export interface MenuContextProps {
// selectedKeys: string[];
// // Level
// inlineIndent: number;
inlineIndent: ComputedRef<number>;
// // Motion
// // motion?: CSSMotionProps;
// // defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>;
motion?: any;
defaultMotions?: Partial<{ [key in MenuMode | 'other']: any }>;
// // Popup
// subMenuOpenDelay: number;
// subMenuCloseDelay: number;
subMenuOpenDelay: ComputedRef<number>;
subMenuCloseDelay: ComputedRef<number>;
// forceSubMenuRender?: boolean;
// builtinPlacements?: BuiltinPlacements;
// triggerSubMenuAction?: TriggerSubMenuAction;
builtinPlacements?: ComputedRef<BuiltinPlacements>;
triggerSubMenuAction?: ComputedRef<TriggerSubMenuAction>;
// // Icon
// itemIcon?: RenderIconType;
@ -53,7 +53,7 @@ export interface MenuContextProps {
// // Function
// onItemClick: MenuClickEventHandler;
// onOpenChange: (key: string, open: boolean) => void;
// getPopupContainer: (node: HTMLElement) => HTMLElement;
getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>;
}
const MenuContextKey: InjectionKey<MenuContextProps> = Symbol('menuContextKey');
@ -66,6 +66,34 @@ const useInjectMenu = () => {
return inject(MenuContextKey);
};
export { useProvideMenu, MenuContextKey, useInjectMenu };
const MenuFirstLevelContextKey: InjectionKey<Boolean> = Symbol('menuFirstLevelContextKey');
const useProvideFirstLevel = (firstLevel: Boolean) => {
provide(MenuFirstLevelContextKey, firstLevel);
};
const useInjectFirstLevel = () => {
return inject(MenuFirstLevelContextKey, true);
};
const MenuContextProvider: FunctionalComponent<{ props: Record<string, any> }> = (
props,
{ slots },
) => {
useProvideMenu({ ...useInjectMenu(), ...props });
return slots.default?.();
};
MenuContextProvider.props = { props: Object };
MenuContextProvider.inheritAttrs = false;
MenuContextProvider.displayName = 'MenuContextProvider';
export {
useProvideMenu,
MenuContextKey,
useInjectMenu,
MenuFirstLevelContextKey,
useProvideFirstLevel,
useInjectFirstLevel,
MenuContextProvider,
};
export default useProvideMenu;

View File

@ -47,7 +47,7 @@ export default defineComponent({
showAction: PropTypes.any.def([]),
hideAction: PropTypes.any.def([]),
getPopupClassNameFromAlign: PropTypes.any.def(returnEmptyString),
// onPopupVisibleChange: PropTypes.func.def(noop),
onPopupVisibleChange: PropTypes.func.def(noop),
afterPopupVisibleChange: PropTypes.func.def(noop),
popup: PropTypes.any,
popupStyle: PropTypes.object.def(() => ({})),
@ -443,7 +443,7 @@ export default defineComponent({
},
setPopupVisible(sPopupVisible, event) {
const { alignPoint, sPopupVisible: prevPopupVisible, $attrs } = this;
const { alignPoint, sPopupVisible: prevPopupVisible, onPopupVisibleChange } = this;
this.clearDelayTimer();
if (prevPopupVisible !== sPopupVisible) {
if (!hasProp(this, 'popupVisible')) {
@ -452,7 +452,7 @@ export default defineComponent({
prevPopupVisible,
});
}
$attrs.onPopupVisibleChange && $attrs.onPopupVisibleChange(sPopupVisible);
onPopupVisibleChange && onPopupVisibleChange(sPopupVisible);
}
// Always record the point position since mouseEnterDelay will delay the show
if (alignPoint && event) {

View File

@ -0,0 +1,7 @@
import devWarning, { resetWarned } from './warning';
export { resetWarned };
export default (valid: boolean, component: string, message: string): void => {
devWarning(valid, `[ant-design-vue: ${component}] ${message}`);
};

View File

@ -15,14 +15,11 @@
<span>Navigation One</span>
</span>
</template>
<a-menu-item-group key="g1">
<template #title>
<QqOutlined />
<span>Item 1</span>
</template>
<a-menu-item key="1">Option 1</a-menu-item>
<a-menu-item key="2">Option 2</a-menu-item>
</a-menu-item-group>
<a-menu-item key="1">
<template #icon><QqOutlined /></template>
Option 1
</a-menu-item>
<a-menu-item key="2">Option 2</a-menu-item>
<a-menu-item-group key="g2" title="Item 2">
<a-menu-item key="3">Option 3</a-menu-item>
<a-menu-item key="4">Option 4</a-menu-item>