ant-design-vue/components/menu/src/SubMenu.tsx

295 lines
9.5 KiB
Vue

import PropTypes from '../../_util/vue-types';
import {
computed,
defineComponent,
getCurrentInstance,
ref,
watch,
PropType,
onBeforeUnmount,
} from 'vue';
import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath';
import { useInjectMenu, useProvideFirstLevel, MenuContextProvider } from './hooks/useMenuContext';
import { getPropsSlot, isValidElement } from 'ant-design-vue/es/_util/props-util';
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';
import Transition, { getTransitionProps } from 'ant-design-vue/es/_util/transition';
let indexGuid = 0;
export default defineComponent({
name: 'ASubMenu',
props: {
icon: PropTypes.VNodeChild,
title: PropTypes.VNodeChild,
disabled: Boolean,
level: Number,
popupClassName: String,
popupOffset: Array as PropType<number[]>,
internalPopupClose: Boolean,
},
slots: ['icon', 'title'],
emits: ['titleClick', 'titleMouseenter', 'titleMouseleave'],
inheritAttrs: false,
setup(props, { slots, attrs, emit }) {
useProvideFirstLevel(false);
const instance = getCurrentInstance();
const key = instance.vnode.key;
const eventKey = `sub_menu_${++indexGuid}_$$_${key}`;
const { parentEventKeys, parentInfo } = useInjectKeyPath();
const keysPath = computed(() => [...parentEventKeys.value, eventKey]);
const childrenEventKeys = ref([]);
const menuInfo = {
eventKey,
key,
parentEventKeys,
childrenEventKeys,
};
parentInfo.childrenEventKeys?.value.push(eventKey);
onBeforeUnmount(() => {
if (parentInfo.childrenEventKeys) {
parentInfo.childrenEventKeys.value = parentInfo.childrenEventKeys?.value.filter(
k => k != eventKey,
);
}
});
useProvideKeyPath(eventKey, menuInfo);
const {
prefixCls,
activeKeys,
disabled: contextDisabled,
changeActiveKeys,
mode,
inlineCollapsed,
antdMenuTheme,
openKeys,
overflowDisabled,
onOpenChange,
registerMenuInfo,
unRegisterMenuInfo,
selectedSubMenuEventKeys,
motion,
defaultMotions,
} = useInjectMenu();
registerMenuInfo(eventKey, menuInfo);
onBeforeUnmount(() => {
unRegisterMenuInfo(eventKey);
});
const subMenuPrefixCls = computed(() => `${prefixCls.value}-submenu`);
const mergedDisabled = computed(() => contextDisabled.value || props.disabled);
const elementRef = ref();
const popupRef = ref();
// // ================================ Icon ================================
// const mergedItemIcon = itemIcon || contextItemIcon;
// const mergedExpandIcon = expandIcon || contextExpandIcon;
// ================================ Open ================================
const originOpen = computed(() => openKeys.value.includes(key));
const open = computed(() => !overflowDisabled.value && originOpen.value);
// =============================== Select ===============================
const childrenSelected = computed(() => {
return selectedSubMenuEventKeys.value.includes(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.value) {
return;
}
emit('titleClick', e, key);
// Trigger open by click when mode is `inline`
if (mode.value === 'inline') {
onOpenChange(eventKey, !originOpen.value);
}
};
const onMouseEnter = (event: MouseEvent) => {
if (!mergedDisabled.value) {
changeActiveKeys(keysPath.value);
emit('titleMouseenter', event);
}
};
const onMouseLeave = (event: MouseEvent) => {
if (!mergedDisabled.value) {
changeActiveKeys([]);
emit('titleMouseleave', event);
}
};
// ========================== DirectionStyle ==========================
const directionStyle = useDirectionStyle(computed(() => keysPath.value.length));
// >>>>> Visible change
const onPopupVisibleChange = (newVisible: boolean) => {
if (mode.value !== 'inline') {
onOpenChange(eventKey, newVisible);
}
};
/**
* Used for accessibility. Helper will focus element without key board.
* We should manually trigger an active
*/
const onInternalFocus = () => {
changeActiveKeys(keysPath.value);
};
// =============================== Render ===============================
const popupId = eventKey && `${eventKey}-popup`;
const popupClassName = computed(() =>
classNames(
prefixCls.value,
`${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>}
</>
);
};
// Cache mode if it change to `inline` which do not have popup motion
const triggerModeRef = computed(() => {
return mode.value !== 'inline' && keysPath.value.length > 1 ? 'vertical' : mode.value;
});
const renderMode = computed(() => (mode.value === 'horizontal' ? 'vertical' : mode.value));
const style = ref({});
const className = ref('');
const mergedMotion = computed(() => {
const m = motion.value || defaultMotions.value?.[mode.value] || defaultMotions.value?.other;
const res = typeof m === 'function' ? m(style, className) : m;
return res ? getTransitionProps(res.name) : undefined;
});
return () => {
const icon = getPropsSlot(slots, props, 'icon');
const title = renderTitle(getPropsSlot(slots, props, 'title'), icon);
const subMenuPrefixClsValue = subMenuPrefixCls.value;
let titleNode = (
<div
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}
>
{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;
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: ({ visible }) => (
<MenuContextProvider props={{ mode: triggerModeRef }}>
<Transition {...mergedMotion.value}>
<SubMenuList v-show={visible} id={popupId} ref={popupRef}>
{slots.default?.()}
</SubMenuList>
</Transition>
</MenuContextProvider>
),
}}
>
{titleNode}
</PopupTrigger>
);
}
return (
<MenuContextProvider props={{ mode: renderMode }}>
<li
{...attrs}
role="none"
class={classNames(
subMenuPrefixClsValue,
`${subMenuPrefixClsValue}-${mode.value}`,
attrs.class,
{
[`${subMenuPrefixClsValue}-open`]: open.value,
[`${subMenuPrefixClsValue}-active`]: isActive.value,
[`${subMenuPrefixClsValue}-selected`]: childrenSelected.value,
[`${subMenuPrefixClsValue}-disabled`]: mergedDisabled.value,
},
)}
onMouseenter={onMouseEnter}
onMouseleave={onMouseLeave}
>
{titleNode}
{/* Inline mode */}
{!overflowDisabled.value && mode.value !== 'vertical' && (
<InlineSubMenuList id={popupId} open={open.value} keyPath={keysPath.value}>
{slots.default?.()}
</InlineSubMenuList>
)}
</li>
</MenuContextProvider>
);
};
},
});