refactor: menu
parent
aa8dc1a0c2
commit
f0baa6118b
|
@ -18,6 +18,7 @@ export const menuProps = {
|
||||||
prefixCls: String,
|
prefixCls: String,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
inlineCollapsed: Boolean,
|
inlineCollapsed: Boolean,
|
||||||
|
overflowDisabled: Boolean,
|
||||||
|
|
||||||
theme: { type: String as PropType<MenuTheme>, default: 'light' },
|
theme: { type: String as PropType<MenuTheme>, default: 'light' },
|
||||||
mode: { type: String as PropType<MenuMode>, default: 'vertical' },
|
mode: { type: String as PropType<MenuMode>, default: 'vertical' },
|
||||||
|
@ -38,7 +39,8 @@ export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'AMenu',
|
name: 'AMenu',
|
||||||
props: menuProps,
|
props: menuProps,
|
||||||
setup(props, { slots }) {
|
emits: ['update:openKeys', 'openChange'],
|
||||||
|
setup(props, { slots, emit }) {
|
||||||
const { prefixCls, direction } = useConfigInject('menu', props);
|
const { prefixCls, direction } = useConfigInject('menu', props);
|
||||||
|
|
||||||
const siderCollapsed = inject(
|
const siderCollapsed = inject(
|
||||||
|
@ -105,6 +107,11 @@ export default defineComponent({
|
||||||
|
|
||||||
useProvideFirstLevel(true);
|
useProvideFirstLevel(true);
|
||||||
|
|
||||||
|
const onOpenChange = (key: Key, open: boolean) => {
|
||||||
|
// emit('update:openKeys', openKeys);
|
||||||
|
emit('openChange', open);
|
||||||
|
};
|
||||||
|
|
||||||
useProvideMenu({
|
useProvideMenu({
|
||||||
prefixCls,
|
prefixCls,
|
||||||
activeKeys,
|
activeKeys,
|
||||||
|
@ -124,6 +131,8 @@ export default defineComponent({
|
||||||
antdMenuTheme: computed(() => props.theme),
|
antdMenuTheme: computed(() => props.theme),
|
||||||
siderCollapsed,
|
siderCollapsed,
|
||||||
defaultMotions,
|
defaultMotions,
|
||||||
|
overflowDisabled: computed(() => props.overflowDisabled),
|
||||||
|
onOpenChange,
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
return <ul class={className.value}>{slots.default?.()}</ul>;
|
return <ul class={className.value}>{slots.default?.()}</ul>;
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import PropTypes from '../../_util/vue-types';
|
import PropTypes from '../../_util/vue-types';
|
||||||
import { computed, defineComponent, getCurrentInstance, ref } from 'vue';
|
import { computed, defineComponent, getCurrentInstance, ref, watch, PropType } from 'vue';
|
||||||
import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath';
|
import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath';
|
||||||
import { useInjectMenu, useProvideFirstLevel } from './hooks/useMenuContext';
|
import { useInjectMenu, useProvideFirstLevel, MenuContextProvider } from './hooks/useMenuContext';
|
||||||
import { getPropsSlot, isValidElement } from 'ant-design-vue/es/_util/props-util';
|
import { getPropsSlot, isValidElement } from 'ant-design-vue/es/_util/props-util';
|
||||||
import classNames from 'ant-design-vue/es/_util/classNames';
|
import classNames from 'ant-design-vue/es/_util/classNames';
|
||||||
|
import useDirectionStyle from './hooks/useDirectionStyle';
|
||||||
|
import PopupTrigger from './PopupTrigger';
|
||||||
|
import SubMenuList from './SubMenuList';
|
||||||
|
import InlineSubMenuList from './InlineSubMenuList';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ASubMenu',
|
name: 'ASubMenu',
|
||||||
|
@ -13,30 +17,32 @@ export default defineComponent({
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
level: Number,
|
level: Number,
|
||||||
popupClassName: String,
|
popupClassName: String,
|
||||||
popupOffset: [Number, Number],
|
popupOffset: Array as PropType<number[]>,
|
||||||
|
internalPopupClose: Boolean,
|
||||||
},
|
},
|
||||||
slots: ['icon', 'title'],
|
slots: ['icon', 'title'],
|
||||||
|
emits: ['titleClick', 'titleMouseenter', 'titleMouseleave'],
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
setup(props, { slots, attrs }) {
|
setup(props, { slots, attrs, emit }) {
|
||||||
useProvideKeyPath();
|
useProvideKeyPath();
|
||||||
useProvideFirstLevel(false);
|
useProvideFirstLevel(false);
|
||||||
const instance = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
const key = instance.vnode.key;
|
const key = instance.vnode.key;
|
||||||
const keyPath = useInjectKeyPath();
|
const parentKeys = useInjectKeyPath();
|
||||||
const {
|
const {
|
||||||
prefixCls,
|
prefixCls,
|
||||||
activeKeys,
|
activeKeys,
|
||||||
disabled: contextDisabled,
|
disabled: contextDisabled,
|
||||||
changeActiveKeys,
|
changeActiveKeys,
|
||||||
rtl,
|
|
||||||
mode,
|
mode,
|
||||||
inlineCollapsed,
|
inlineCollapsed,
|
||||||
antdMenuTheme,
|
antdMenuTheme,
|
||||||
openKeys,
|
openKeys,
|
||||||
overflowDisabled,
|
overflowDisabled,
|
||||||
|
onOpenChange,
|
||||||
} = useInjectMenu();
|
} = useInjectMenu();
|
||||||
|
|
||||||
const subMenuPrefixCls = computed(() => `${prefixCls}-submenu`);
|
const subMenuPrefixCls = computed(() => `${prefixCls.value}-submenu`);
|
||||||
const mergedDisabled = computed(() => contextDisabled.value || props.disabled);
|
const mergedDisabled = computed(() => contextDisabled.value || props.disabled);
|
||||||
const elementRef = ref();
|
const elementRef = ref();
|
||||||
const popupRef = ref();
|
const popupRef = ref();
|
||||||
|
@ -49,8 +55,73 @@ export default defineComponent({
|
||||||
const originOpen = computed(() => openKeys.value.includes(key));
|
const originOpen = computed(() => openKeys.value.includes(key));
|
||||||
const open = computed(() => !overflowDisabled.value && originOpen.value);
|
const open = computed(() => !overflowDisabled.value && originOpen.value);
|
||||||
|
|
||||||
|
// =============================== Select ===============================
|
||||||
|
const childrenSelected = ref(true); // isSubPathKey(selectedKeys, eventKey);
|
||||||
|
|
||||||
|
const isActive = ref(false);
|
||||||
|
watch(
|
||||||
|
activeKeys,
|
||||||
|
() => {
|
||||||
|
isActive.value = !!activeKeys.value.find(val => val === key);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// =============================== Events ===============================
|
||||||
|
// >>>> Title click
|
||||||
|
const onInternalTitleClick = (e: Event) => {
|
||||||
|
// Skip if disabled
|
||||||
|
if (mergedDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('titleClick', e, key);
|
||||||
|
|
||||||
|
// Trigger open by click when mode is `inline`
|
||||||
|
if (mode.value === 'inline') {
|
||||||
|
onOpenChange(key, !originOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseEnter = (event: MouseEvent) => {
|
||||||
|
if (!mergedDisabled.value) {
|
||||||
|
changeActiveKeys([...parentKeys.value, key]);
|
||||||
|
emit('titleMouseenter', event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onMouseLeave = (event: MouseEvent) => {
|
||||||
|
if (!mergedDisabled.value) {
|
||||||
|
changeActiveKeys([]);
|
||||||
|
emit('titleMouseleave', event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================== DirectionStyle ==========================
|
||||||
|
const directionStyle = useDirectionStyle(computed(() => parentKeys.value.length));
|
||||||
|
|
||||||
|
// >>>>> Visible change
|
||||||
|
const onPopupVisibleChange = (newVisible: boolean) => {
|
||||||
|
if (mode.value !== 'inline') {
|
||||||
|
onOpenChange(key, newVisible);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for accessibility. Helper will focus element without key board.
|
||||||
|
* We should manually trigger an active
|
||||||
|
*/
|
||||||
|
const onInternalFocus = () => {
|
||||||
|
changeActiveKeys([...parentKeys.value, key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================== Render ===============================
|
||||||
|
const popupId = key && `${key}-popup`;
|
||||||
|
|
||||||
const popupClassName = computed(() =>
|
const popupClassName = computed(() =>
|
||||||
classNames(prefixCls, `${prefixCls.value}-${antdMenuTheme.value}`, props.popupClassName),
|
classNames(
|
||||||
|
prefixCls.value,
|
||||||
|
`${prefixCls.value}-${antdMenuTheme.value}`,
|
||||||
|
props.popupClassName,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const renderTitle = (title: any, icon: any) => {
|
const renderTitle = (title: any, icon: any) => {
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
|
@ -78,13 +149,103 @@ export default defineComponent({
|
||||||
`${prefixCls.value}-${mode.value === 'inline' ? 'inline' : 'vertical'}`,
|
`${prefixCls.value}-${mode.value === 'inline' ? 'inline' : 'vertical'}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Cache mode if it change to `inline` which do not have popup motion
|
||||||
|
const triggerModeRef = computed(() => {
|
||||||
|
return mode.value !== 'inline' && parentKeys.value.length > 1 ? 'vertical' : mode.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderMode = computed(() => (mode.value === 'horizontal' ? 'vertical' : mode.value));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const icon = getPropsSlot(slots, props, 'icon');
|
const icon = getPropsSlot(slots, props, 'icon');
|
||||||
const title = renderTitle(getPropsSlot(slots, props, 'title'), icon);
|
const title = renderTitle(getPropsSlot(slots, props, 'title'), icon);
|
||||||
return (
|
const subMenuPrefixClsValue = subMenuPrefixCls.value;
|
||||||
<ul {...attrs} class={[className.value, attrs.class]} data-menu-list>
|
let titleNode = (
|
||||||
|
<div
|
||||||
|
role="menuitem"
|
||||||
|
style={directionStyle.value}
|
||||||
|
class={`${subMenuPrefixClsValue}-title`}
|
||||||
|
tabindex={mergedDisabled.value ? null : -1}
|
||||||
|
ref={elementRef}
|
||||||
|
title={typeof title === 'string' ? title : null}
|
||||||
|
data-menu-id={key}
|
||||||
|
aria-expanded={open.value}
|
||||||
|
aria-haspopup
|
||||||
|
aria-controls={popupId}
|
||||||
|
aria-disabled={mergedDisabled.value}
|
||||||
|
onClick={onInternalTitleClick}
|
||||||
|
onFocus={onInternalFocus}
|
||||||
|
onMouseenter={onMouseEnter}
|
||||||
|
onMouseleave={onMouseLeave}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{/* Only non-horizontal mode shows the icon */}
|
||||||
|
{mode.value !== 'horizontal' && slots.expandIcon ? (
|
||||||
|
slots.expandIcon({ ...props, isOpen: open.value })
|
||||||
|
) : (
|
||||||
|
<i class={`${subMenuPrefixClsValue}-arrow`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!overflowDisabled.value) {
|
||||||
|
const triggerMode = triggerModeRef.value;
|
||||||
|
|
||||||
|
// Still wrap with Trigger here since we need avoid react re-mount dom node
|
||||||
|
// Which makes motion failed
|
||||||
|
titleNode = (
|
||||||
|
<PopupTrigger
|
||||||
|
mode={triggerMode}
|
||||||
|
prefixCls={subMenuPrefixClsValue}
|
||||||
|
visible={!props.internalPopupClose && open.value && mode.value !== 'inline'}
|
||||||
|
popupClassName={popupClassName.value}
|
||||||
|
popupOffset={props.popupOffset}
|
||||||
|
disabled={mergedDisabled.value}
|
||||||
|
onVisibleChange={onPopupVisibleChange}
|
||||||
|
v-slots={{
|
||||||
|
popup: () => (
|
||||||
|
<MenuContextProvider props={{ mode: triggerModeRef }}>
|
||||||
|
<SubMenuList id={popupId} ref={popupRef}>
|
||||||
{slots.default?.()}
|
{slots.default?.()}
|
||||||
</ul>
|
</SubMenuList>
|
||||||
|
</MenuContextProvider>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{titleNode}
|
||||||
|
</PopupTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<MenuContextProvider props={{ mode: renderMode }}>
|
||||||
|
<li
|
||||||
|
{...attrs}
|
||||||
|
role="none"
|
||||||
|
class={classNames(
|
||||||
|
subMenuPrefixClsValue,
|
||||||
|
`${subMenuPrefixClsValue}-${mode.value}`,
|
||||||
|
className.value,
|
||||||
|
attrs.class,
|
||||||
|
{
|
||||||
|
[`${subMenuPrefixClsValue}-open`]: open.value,
|
||||||
|
[`${subMenuPrefixClsValue}-active`]: isActive.value,
|
||||||
|
[`${subMenuPrefixClsValue}-selected`]: childrenSelected.value,
|
||||||
|
[`${subMenuPrefixClsValue}-disabled`]: mergedDisabled.value,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{titleNode}
|
||||||
|
|
||||||
|
{/* Inline mode */}
|
||||||
|
{!overflowDisabled.value && (
|
||||||
|
<InlineSubMenuList id={popupId} open={open.value} keyPath={parentKeys.value}>
|
||||||
|
{slots.default?.()}
|
||||||
|
</InlineSubMenuList>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</MenuContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,9 +7,9 @@ const InternalSubMenuList: FunctionalComponent<any> = (_props, { slots, attrs })
|
||||||
<ul
|
<ul
|
||||||
{...attrs}
|
{...attrs}
|
||||||
class={classNames(
|
class={classNames(
|
||||||
prefixCls,
|
prefixCls.value,
|
||||||
`${prefixCls}-sub`,
|
`${prefixCls.value}-sub`,
|
||||||
`${prefixCls}-${mode.value === 'inline' ? 'inline' : 'vertical'}`,
|
`${prefixCls.value}-${mode.value === 'inline' ? 'inline' : 'vertical'}`,
|
||||||
)}
|
)}
|
||||||
data-menu-list
|
data-menu-list
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Key } from '../../../_util/type';
|
import { Key } from '../../../_util/type';
|
||||||
import { ComputedRef, FunctionalComponent, inject, InjectionKey, provide, Ref } from 'vue';
|
import { ComputedRef, defineComponent, inject, InjectionKey, provide, Ref } from 'vue';
|
||||||
import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface';
|
import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface';
|
||||||
|
|
||||||
export interface MenuContextProps {
|
export interface MenuContextProps {
|
||||||
|
@ -21,7 +21,7 @@ export interface MenuContextProps {
|
||||||
// // Disabled
|
// // Disabled
|
||||||
disabled?: ComputedRef<boolean>;
|
disabled?: ComputedRef<boolean>;
|
||||||
// // Used for overflow only. Prevent hidden node trigger open
|
// // Used for overflow only. Prevent hidden node trigger open
|
||||||
// overflowDisabled?: boolean;
|
overflowDisabled?: ComputedRef<boolean>;
|
||||||
|
|
||||||
// // Active
|
// // Active
|
||||||
activeKeys: Ref<Key[]>;
|
activeKeys: Ref<Key[]>;
|
||||||
|
@ -52,7 +52,7 @@ export interface MenuContextProps {
|
||||||
|
|
||||||
// // Function
|
// // Function
|
||||||
// onItemClick: MenuClickEventHandler;
|
// onItemClick: MenuClickEventHandler;
|
||||||
// onOpenChange: (key: string, open: boolean) => void;
|
onOpenChange: (key: Key, open: boolean) => void;
|
||||||
getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>;
|
getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,16 +75,17 @@ const useInjectFirstLevel = () => {
|
||||||
return inject(MenuFirstLevelContextKey, true);
|
return inject(MenuFirstLevelContextKey, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuContextProvider: FunctionalComponent<{ props: Record<string, any> }> = (
|
const MenuContextProvider = defineComponent({
|
||||||
props,
|
name: 'MenuContextProvider',
|
||||||
{ slots },
|
inheritAttrs: false,
|
||||||
) => {
|
props: {
|
||||||
|
props: Object,
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
useProvideMenu({ ...useInjectMenu(), ...props });
|
useProvideMenu({ ...useInjectMenu(), ...props });
|
||||||
return slots.default?.();
|
return () => slots.default?.();
|
||||||
};
|
},
|
||||||
MenuContextProvider.props = { props: Object };
|
});
|
||||||
MenuContextProvider.inheritAttrs = false;
|
|
||||||
MenuContextProvider.displayName = 'MenuContextProvider';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useProvideMenu,
|
useProvideMenu,
|
||||||
|
|
2
v2-doc
2
v2-doc
|
@ -1 +1 @@
|
||||||
Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557
|
Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2
|
Loading…
Reference in New Issue