refactor: menu
parent
2ab77978f2
commit
16f051d593
|
@ -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';
|
import { findDOMNode } from './props-util';
|
||||||
|
|
||||||
export const getTransitionProps = (transitionName: string, opt: object = {}) => {
|
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;
|
export default Transition;
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -8,13 +8,14 @@ export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
title: PropTypes.VNodeChild,
|
title: PropTypes.VNodeChild,
|
||||||
},
|
},
|
||||||
|
inheritAttrs: false,
|
||||||
slots: ['title'],
|
slots: ['title'],
|
||||||
setup(props, { slots }) {
|
setup(props, { slots, attrs }) {
|
||||||
const { prefixCls } = useInjectMenu();
|
const { prefixCls } = useInjectMenu();
|
||||||
const groupPrefixCls = computed(() => `${prefixCls.value}-item-group`);
|
const groupPrefixCls = computed(() => `${prefixCls.value}-item-group`);
|
||||||
return () => {
|
return () => {
|
||||||
return (
|
return (
|
||||||
<li onClick={e => e.stopPropagation()} class={groupPrefixCls.value}>
|
<li {...attrs} onClick={e => e.stopPropagation()} class={groupPrefixCls.value}>
|
||||||
<div
|
<div
|
||||||
title={typeof props.title === 'string' ? props.title : undefined}
|
title={typeof props.title === 'string' ? props.title : undefined}
|
||||||
class={`${groupPrefixCls.value}-title`}
|
class={`${groupPrefixCls.value}-title`}
|
||||||
|
|
|
@ -1,14 +1,36 @@
|
||||||
import { Key } from '../../_util/type';
|
import { Key } from '../../_util/type';
|
||||||
import { computed, defineComponent, ExtractPropTypes, ref, PropType } from 'vue';
|
import {
|
||||||
import useProvideMenu from './hooks/useMenuContext';
|
computed,
|
||||||
|
defineComponent,
|
||||||
|
ExtractPropTypes,
|
||||||
|
ref,
|
||||||
|
PropType,
|
||||||
|
inject,
|
||||||
|
watchEffect,
|
||||||
|
} from 'vue';
|
||||||
|
import useProvideMenu, { useProvideFirstLevel } from './hooks/useMenuContext';
|
||||||
import useConfigInject from '../../_util/hooks/useConfigInject';
|
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 = {
|
export const menuProps = {
|
||||||
prefixCls: String,
|
prefixCls: String,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
|
inlineCollapsed: 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' },
|
||||||
|
|
||||||
|
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>>;
|
export type MenuProps = Partial<ExtractPropTypes<typeof menuProps>>;
|
||||||
|
@ -18,6 +40,33 @@ export default defineComponent({
|
||||||
props: menuProps,
|
props: menuProps,
|
||||||
setup(props, { slots }) {
|
setup(props, { slots }) {
|
||||||
const { prefixCls, direction } = useConfigInject('menu', props);
|
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 activeKeys = ref([]);
|
||||||
const openKeys = ref([]);
|
const openKeys = ref([]);
|
||||||
const selectedKeys = ref([]);
|
const selectedKeys = ref([]);
|
||||||
|
@ -25,10 +74,18 @@ export default defineComponent({
|
||||||
activeKeys.value = keys;
|
activeKeys.value = keys;
|
||||||
};
|
};
|
||||||
const disabled = computed(() => !!props.disabled);
|
const disabled = computed(() => !!props.disabled);
|
||||||
useProvideMenu({ prefixCls, activeKeys, openKeys, selectedKeys, changeActiveKeys, disabled });
|
|
||||||
const isRtl = computed(() => direction.value === 'rtl');
|
const isRtl = computed(() => direction.value === 'rtl');
|
||||||
const mergedMode = ref('vertical');
|
const mergedMode = ref<MenuMode>('vertical');
|
||||||
const mergedInlineCollapsed = ref(false);
|
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(() => {
|
const className = computed(() => {
|
||||||
return {
|
return {
|
||||||
[`${prefixCls.value}`]: true,
|
[`${prefixCls.value}`]: true,
|
||||||
|
@ -39,6 +96,35 @@ export default defineComponent({
|
||||||
[`${prefixCls.value}-${props.theme}`]: true,
|
[`${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 () => {
|
||||||
return <ul class={className.value}>{slots.default?.()}</ul>;
|
return <ul class={className.value}>{slots.default?.()}</ul>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 { computed, defineComponent, getCurrentInstance, ref, watch } from 'vue';
|
||||||
import { useInjectKeyPath } from './hooks/useKeyPath';
|
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;
|
let indexGuid = 0;
|
||||||
|
|
||||||
|
@ -9,15 +13,29 @@ export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
role: String,
|
role: String,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
|
danger: Boolean,
|
||||||
|
title: { type: [String, Boolean] },
|
||||||
|
icon: PropTypes.VNodeChild,
|
||||||
},
|
},
|
||||||
emits: ['mouseenter', 'mouseleave'],
|
emits: ['mouseenter', 'mouseleave'],
|
||||||
setup(props, { slots, emit }) {
|
slots: ['icon'],
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup(props, { slots, emit, attrs }) {
|
||||||
const instance = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
const key = instance.vnode.key;
|
const key = instance.vnode.key;
|
||||||
const uniKey = `menu_item_${++indexGuid}`;
|
const uniKey = `menu_item_${++indexGuid}`;
|
||||||
const parentKeys = useInjectKeyPath();
|
const parentKeys = useInjectKeyPath();
|
||||||
console.log(parentKeys.value);
|
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);
|
const isActive = ref(false);
|
||||||
watch(
|
watch(
|
||||||
activeKeys,
|
activeKeys,
|
||||||
|
@ -32,6 +50,7 @@ export default defineComponent({
|
||||||
const itemCls = `${prefixCls.value}-item`;
|
const itemCls = `${prefixCls.value}-item`;
|
||||||
return {
|
return {
|
||||||
[`${itemCls}`]: true,
|
[`${itemCls}`]: true,
|
||||||
|
[`${itemCls}-danger`]: props.danger,
|
||||||
[`${itemCls}-active`]: isActive.value,
|
[`${itemCls}-active`]: isActive.value,
|
||||||
[`${itemCls}-selected`]: selected.value,
|
[`${itemCls}-selected`]: selected.value,
|
||||||
[`${itemCls}-disabled`]: mergedDisabled.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 () => {
|
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 ============================
|
// ============================ Render ============================
|
||||||
const optionRoleProps = {};
|
const optionRoleProps = {};
|
||||||
|
|
||||||
if (props.role === 'option') {
|
if (props.role === 'option') {
|
||||||
optionRoleProps['aria-selected'] = selected.value;
|
optionRoleProps['aria-selected'] = selected.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const icon = getPropsSlot(slots, props, 'icon');
|
||||||
return (
|
return (
|
||||||
<li
|
<Tooltip
|
||||||
class={classNames.value}
|
{...tooltipProps}
|
||||||
role={props.role || 'menuitem'}
|
placement={rtl.value ? 'left' : 'right'}
|
||||||
tabindex={props.disabled ? null : -1}
|
overlayClassName={`${prefixCls.value}-inline-collapsed-tooltip`}
|
||||||
data-menu-id={key}
|
|
||||||
aria-disabled={props.disabled}
|
|
||||||
{...optionRoleProps}
|
|
||||||
onMouseenter={onMouseEnter}
|
|
||||||
onMouseleave={onMouseLeave}
|
|
||||||
>
|
>
|
||||||
{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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,12 +1,74 @@
|
||||||
import { defineComponent } from 'vue';
|
import PropTypes from '../../_util/vue-types';
|
||||||
import useProvideKeyPath from './hooks/useKeyPath';
|
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({
|
export default defineComponent({
|
||||||
name: 'ASubMenu',
|
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();
|
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 () => {
|
||||||
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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
|
@ -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 },
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,22 +1,22 @@
|
||||||
import { Key } from '../../../_util/type';
|
import { Key } from '../../../_util/type';
|
||||||
import { ComputedRef, inject, InjectionKey, provide, Ref } from 'vue';
|
import { ComputedRef, FunctionalComponent, inject, InjectionKey, provide, Ref } from 'vue';
|
||||||
|
import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface';
|
||||||
// import {
|
|
||||||
// BuiltinPlacements,
|
|
||||||
// MenuClickEventHandler,
|
|
||||||
// MenuMode,
|
|
||||||
// RenderIconType,
|
|
||||||
// TriggerSubMenuAction,
|
|
||||||
// } from '../interface';
|
|
||||||
|
|
||||||
export interface MenuContextProps {
|
export interface MenuContextProps {
|
||||||
prefixCls: ComputedRef<string>;
|
prefixCls: ComputedRef<string>;
|
||||||
openKeys: Ref<Key[]>;
|
openKeys: Ref<Key[]>;
|
||||||
selectedKeys: Ref<Key[]>;
|
selectedKeys: Ref<Key[]>;
|
||||||
// rtl?: boolean;
|
rtl?: ComputedRef<boolean>;
|
||||||
|
|
||||||
|
locked?: Ref<boolean>;
|
||||||
|
|
||||||
|
inlineCollapsed: Ref<boolean>;
|
||||||
|
antdMenuTheme?: ComputedRef<MenuTheme>;
|
||||||
|
|
||||||
|
siderCollapsed?: ComputedRef<boolean>;
|
||||||
|
|
||||||
// // Mode
|
// // Mode
|
||||||
// mode: MenuMode;
|
mode: Ref<MenuMode>;
|
||||||
|
|
||||||
// // Disabled
|
// // Disabled
|
||||||
disabled?: ComputedRef<boolean>;
|
disabled?: ComputedRef<boolean>;
|
||||||
|
@ -33,18 +33,18 @@ export interface MenuContextProps {
|
||||||
// selectedKeys: string[];
|
// selectedKeys: string[];
|
||||||
|
|
||||||
// // Level
|
// // Level
|
||||||
// inlineIndent: number;
|
inlineIndent: ComputedRef<number>;
|
||||||
|
|
||||||
// // Motion
|
// // Motion
|
||||||
// // motion?: CSSMotionProps;
|
motion?: any;
|
||||||
// // defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>;
|
defaultMotions?: Partial<{ [key in MenuMode | 'other']: any }>;
|
||||||
|
|
||||||
// // Popup
|
// // Popup
|
||||||
// subMenuOpenDelay: number;
|
subMenuOpenDelay: ComputedRef<number>;
|
||||||
// subMenuCloseDelay: number;
|
subMenuCloseDelay: ComputedRef<number>;
|
||||||
// forceSubMenuRender?: boolean;
|
// forceSubMenuRender?: boolean;
|
||||||
// builtinPlacements?: BuiltinPlacements;
|
builtinPlacements?: ComputedRef<BuiltinPlacements>;
|
||||||
// triggerSubMenuAction?: TriggerSubMenuAction;
|
triggerSubMenuAction?: ComputedRef<TriggerSubMenuAction>;
|
||||||
|
|
||||||
// // Icon
|
// // Icon
|
||||||
// itemIcon?: RenderIconType;
|
// itemIcon?: RenderIconType;
|
||||||
|
@ -53,7 +53,7 @@ export interface MenuContextProps {
|
||||||
// // Function
|
// // Function
|
||||||
// onItemClick: MenuClickEventHandler;
|
// onItemClick: MenuClickEventHandler;
|
||||||
// onOpenChange: (key: string, open: boolean) => void;
|
// onOpenChange: (key: string, open: boolean) => void;
|
||||||
// getPopupContainer: (node: HTMLElement) => HTMLElement;
|
getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuContextKey: InjectionKey<MenuContextProps> = Symbol('menuContextKey');
|
const MenuContextKey: InjectionKey<MenuContextProps> = Symbol('menuContextKey');
|
||||||
|
@ -66,6 +66,34 @@ const useInjectMenu = () => {
|
||||||
return inject(MenuContextKey);
|
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;
|
export default useProvideMenu;
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default defineComponent({
|
||||||
showAction: PropTypes.any.def([]),
|
showAction: PropTypes.any.def([]),
|
||||||
hideAction: PropTypes.any.def([]),
|
hideAction: PropTypes.any.def([]),
|
||||||
getPopupClassNameFromAlign: PropTypes.any.def(returnEmptyString),
|
getPopupClassNameFromAlign: PropTypes.any.def(returnEmptyString),
|
||||||
// onPopupVisibleChange: PropTypes.func.def(noop),
|
onPopupVisibleChange: PropTypes.func.def(noop),
|
||||||
afterPopupVisibleChange: PropTypes.func.def(noop),
|
afterPopupVisibleChange: PropTypes.func.def(noop),
|
||||||
popup: PropTypes.any,
|
popup: PropTypes.any,
|
||||||
popupStyle: PropTypes.object.def(() => ({})),
|
popupStyle: PropTypes.object.def(() => ({})),
|
||||||
|
@ -443,7 +443,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
setPopupVisible(sPopupVisible, event) {
|
setPopupVisible(sPopupVisible, event) {
|
||||||
const { alignPoint, sPopupVisible: prevPopupVisible, $attrs } = this;
|
const { alignPoint, sPopupVisible: prevPopupVisible, onPopupVisibleChange } = this;
|
||||||
this.clearDelayTimer();
|
this.clearDelayTimer();
|
||||||
if (prevPopupVisible !== sPopupVisible) {
|
if (prevPopupVisible !== sPopupVisible) {
|
||||||
if (!hasProp(this, 'popupVisible')) {
|
if (!hasProp(this, 'popupVisible')) {
|
||||||
|
@ -452,7 +452,7 @@ export default defineComponent({
|
||||||
prevPopupVisible,
|
prevPopupVisible,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$attrs.onPopupVisibleChange && $attrs.onPopupVisibleChange(sPopupVisible);
|
onPopupVisibleChange && onPopupVisibleChange(sPopupVisible);
|
||||||
}
|
}
|
||||||
// Always record the point position since mouseEnterDelay will delay the show
|
// Always record the point position since mouseEnterDelay will delay the show
|
||||||
if (alignPoint && event) {
|
if (alignPoint && event) {
|
||||||
|
|
|
@ -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}`);
|
||||||
|
};
|
|
@ -15,14 +15,11 @@
|
||||||
<span>Navigation One</span>
|
<span>Navigation One</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<a-menu-item-group key="g1">
|
<a-menu-item key="1">
|
||||||
<template #title>
|
<template #icon><QqOutlined /></template>
|
||||||
<QqOutlined />
|
Option 1
|
||||||
<span>Item 1</span>
|
</a-menu-item>
|
||||||
</template>
|
<a-menu-item key="2">Option 2</a-menu-item>
|
||||||
<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-group key="g2" title="Item 2">
|
<a-menu-item-group key="g2" title="Item 2">
|
||||||
<a-menu-item key="3">Option 3</a-menu-item>
|
<a-menu-item key="3">Option 3</a-menu-item>
|
||||||
<a-menu-item key="4">Option 4</a-menu-item>
|
<a-menu-item key="4">Option 4</a-menu-item>
|
||||||
|
|
Loading…
Reference in New Issue