From f0baa6118bac6eed68b99199123020591d419528 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Mon, 17 May 2021 23:17:27 +0800 Subject: [PATCH] refactor: menu --- components/menu/src/Menu.tsx | 11 +- components/menu/src/SubMenu.tsx | 183 ++++++++++++++++++-- components/menu/src/SubMenuList.tsx | 6 +- components/menu/src/hooks/useMenuContext.ts | 27 +-- v2-doc | 2 +- 5 files changed, 200 insertions(+), 29 deletions(-) diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 49cbd8ff3..0ed4d8f3e 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -18,6 +18,7 @@ export const menuProps = { prefixCls: String, disabled: Boolean, inlineCollapsed: Boolean, + overflowDisabled: Boolean, theme: { type: String as PropType, default: 'light' }, mode: { type: String as PropType, default: 'vertical' }, @@ -38,7 +39,8 @@ export type MenuProps = Partial>; export default defineComponent({ name: 'AMenu', props: menuProps, - setup(props, { slots }) { + emits: ['update:openKeys', 'openChange'], + setup(props, { slots, emit }) { const { prefixCls, direction } = useConfigInject('menu', props); const siderCollapsed = inject( @@ -105,6 +107,11 @@ export default defineComponent({ useProvideFirstLevel(true); + const onOpenChange = (key: Key, open: boolean) => { + // emit('update:openKeys', openKeys); + emit('openChange', open); + }; + useProvideMenu({ prefixCls, activeKeys, @@ -124,6 +131,8 @@ export default defineComponent({ antdMenuTheme: computed(() => props.theme), siderCollapsed, defaultMotions, + overflowDisabled: computed(() => props.overflowDisabled), + onOpenChange, }); return () => { return
    {slots.default?.()}
; diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index 84ee6e242..98627c5bd 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -1,9 +1,13 @@ 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 { useInjectMenu, useProvideFirstLevel } from './hooks/useMenuContext'; +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'; export default defineComponent({ name: 'ASubMenu', @@ -13,30 +17,32 @@ export default defineComponent({ disabled: Boolean, level: Number, popupClassName: String, - popupOffset: [Number, Number], + popupOffset: Array as PropType, + internalPopupClose: Boolean, }, slots: ['icon', 'title'], + emits: ['titleClick', 'titleMouseenter', 'titleMouseleave'], inheritAttrs: false, - setup(props, { slots, attrs }) { + setup(props, { slots, attrs, emit }) { useProvideKeyPath(); useProvideFirstLevel(false); const instance = getCurrentInstance(); const key = instance.vnode.key; - const keyPath = useInjectKeyPath(); + const parentKeys = useInjectKeyPath(); const { prefixCls, activeKeys, disabled: contextDisabled, changeActiveKeys, - rtl, mode, inlineCollapsed, antdMenuTheme, openKeys, overflowDisabled, + onOpenChange, } = useInjectMenu(); - const subMenuPrefixCls = computed(() => `${prefixCls}-submenu`); + const subMenuPrefixCls = computed(() => `${prefixCls.value}-submenu`); const mergedDisabled = computed(() => contextDisabled.value || props.disabled); const elementRef = ref(); const popupRef = ref(); @@ -49,8 +55,73 @@ export default defineComponent({ const originOpen = computed(() => openKeys.value.includes(key)); 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(() => - classNames(prefixCls, `${prefixCls.value}-${antdMenuTheme.value}`, props.popupClassName), + classNames( + prefixCls.value, + `${prefixCls.value}-${antdMenuTheme.value}`, + props.popupClassName, + ), ); const renderTitle = (title: any, icon: any) => { if (!icon) { @@ -78,13 +149,103 @@ export default defineComponent({ `${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 () => { const icon = getPropsSlot(slots, props, 'icon'); const title = renderTitle(getPropsSlot(slots, props, 'title'), icon); + const subMenuPrefixClsValue = subMenuPrefixCls.value; + let titleNode = ( + + ); + + 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 = ( + ( + + + {slots.default?.()} + + + ), + }} + > + {titleNode} + + ); + } return ( -
    - {slots.default?.()} -
+ +
  • + {titleNode} + + {/* Inline mode */} + {!overflowDisabled.value && ( + + {slots.default?.()} + + )} +
  • +
    ); }; }, diff --git a/components/menu/src/SubMenuList.tsx b/components/menu/src/SubMenuList.tsx index d98343d6f..a5d904009 100644 --- a/components/menu/src/SubMenuList.tsx +++ b/components/menu/src/SubMenuList.tsx @@ -7,9 +7,9 @@ const InternalSubMenuList: FunctionalComponent = (_props, { slots, attrs })
      diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 060d70e59..097aea7a5 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -1,5 +1,5 @@ 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'; export interface MenuContextProps { @@ -21,7 +21,7 @@ export interface MenuContextProps { // // Disabled disabled?: ComputedRef; // // Used for overflow only. Prevent hidden node trigger open - // overflowDisabled?: boolean; + overflowDisabled?: ComputedRef; // // Active activeKeys: Ref; @@ -52,7 +52,7 @@ export interface MenuContextProps { // // Function // onItemClick: MenuClickEventHandler; - // onOpenChange: (key: string, open: boolean) => void; + onOpenChange: (key: Key, open: boolean) => void; getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>; } @@ -75,16 +75,17 @@ const useInjectFirstLevel = () => { return inject(MenuFirstLevelContextKey, true); }; -const MenuContextProvider: FunctionalComponent<{ props: Record }> = ( - props, - { slots }, -) => { - useProvideMenu({ ...useInjectMenu(), ...props }); - return slots.default?.(); -}; -MenuContextProvider.props = { props: Object }; -MenuContextProvider.inheritAttrs = false; -MenuContextProvider.displayName = 'MenuContextProvider'; +const MenuContextProvider = defineComponent({ + name: 'MenuContextProvider', + inheritAttrs: false, + props: { + props: Object, + }, + setup(props, { slots }) { + useProvideMenu({ ...useInjectMenu(), ...props }); + return () => slots.default?.(); + }, +}); export { useProvideMenu, diff --git a/v2-doc b/v2-doc index a7013ae87..d19705328 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557 +Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2