refactor(components): refactor menu (#3639)

* refactor(menu): rename to kebab-case

* refactor(menu): rename RootMenuProvider to MenuProvider

* refactor(menu): rename menu.type to types

* refactor(menu): extract menu props and emits

* refactor(menu): change sub-menu.vue to ts

* refactor(menu): extract menu-item-group props and emits

* refactor(menu): extract menu-item props and emits

* refactor(menu): extract sub-menu props and emits

* refactor(menu): rename type RegisterMenuItem to MenuItemRegistered

* refactor(menu): MenuProvider ref to reactive

* refactor(menu): MenuProvider remove methods

* refactor(menu): change submenus to subMenus

* refactor(menu): remove RootMenuProps type

* refactor(menu): MenuProvider improve types & rename submenu to subMenu

* refactor(menu): menu add block to provide

* refactor(menu): menu improve expose

* refactor(menu): menu improve render types

* refactor(menu): menu refactor types & change handle(Sub)MenuItemClick params

* refactor(menu): menu refactor types

* refactor(menu): menu-item-group improve types

* refactor(menu): menu-item improve types

* refactor(menu): sub-menu improve types

* refactor(menu): use-menu improve types

* refactor(menu): sub-menu fix types

* refactor(menu): menu-collapse-transition improve types

* refactor(menu): menu-item-group improve template

* refactor(menu): menu-item rename emit param

* refactor(menu): finally improve types

* fix lint

* chore: re-order import

* chore: remove reactive
This commit is contained in:
三咲智子 2021-09-28 10:59:22 +08:00 committed by GitHub
parent 8e29813609
commit c68d59c6a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 938 additions and 1013 deletions

View File

@ -4,9 +4,9 @@ import { sleep } from '@element-plus/test-utils'
import { rAF } from '@element-plus/test-utils/tick'
import Menu from '../src/menu'
import MenuGroup from '../src/menuItemGroup.vue'
import MenuItem from '../src/menuItem.vue'
import SubMenu from '../src/submenu.vue'
import MenuGroup from '../src/menu-item-group.vue'
import MenuItem from '../src/menu-item.vue'
import SubMenu from '../src/sub-menu'
const _mount = (template: string, options = {}) =>
mount({
@ -119,7 +119,7 @@ describe('menu', () => {
default-active="2"
class="el-menu-vertical-demo"
>
<el-sub-menu index="1">
<el-sub-menu index="1" ref="subMenu">
<template #title>
<i class="el-icon-location"></i>
<span></span>
@ -153,11 +153,13 @@ describe('menu', () => {
},
}
)
const elSubMenu = wrapper.findComponent({ name: 'ElSubMenu' })
const elSubMenu = wrapper.findComponent({ ref: 'subMenu' })
const instance: any = elSubMenu.vm
const button = wrapper.find('button')
button.trigger('click')
await nextTick()
const instance = elSubMenu.vm as any
expect(instance.opened).toBeTruthy()
})

View File

@ -1,9 +1,9 @@
import { withInstall, withNoopInstall } from '@element-plus/utils/with-install'
import Menu from './src/menu'
import MenuItem from './src/menuItem.vue'
import MenuItemGroup from './src/menuItemGroup.vue'
import SubMenu from './src/submenu.vue'
import MenuItem from './src/menu-item.vue'
import MenuItemGroup from './src/menu-item-group.vue'
import SubMenu from './src/sub-menu'
export const ElMenu = withInstall(Menu, {
MenuItem,
@ -15,4 +15,8 @@ export const ElMenuItem = withNoopInstall(MenuItem)
export const ElMenuItemGroup = withNoopInstall(MenuItemGroup)
export const ElSubMenu = withNoopInstall(SubMenu)
export * from './src/menu.type'
export * from './src/menu'
export * from './src/menu-item'
export * from './src/menu-item-group'
export * from './src/sub-menu'
export * from './src/types'

View File

@ -1,58 +1,59 @@
<template>
<transition mode="out-in" v-on="on">
<slot></slot>
<transition mode="out-in" v-bind="listeners">
<slot />
</transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { addClass, removeClass, hasClass } from '@element-plus/utils/dom'
import type { TransitionProps, BaseTransitionProps } from 'vue'
export default defineComponent({
name: 'ElMenuCollapseTransition',
setup() {
return {
on: {
beforeEnter(el: HTMLElement) {
el.style.opacity = '0.2'
},
enter(el: HTMLElement, done) {
addClass(el, 'el-opacity-transition')
el.style.opacity = '1'
done()
},
afterEnter(el: HTMLElement) {
removeClass(el, 'el-opacity-transition')
el.style.opacity = ''
},
beforeLeave(el: HTMLElement) {
if (!el.dataset) {
;(el as any).dataset = {}
}
if (hasClass(el, 'el-menu--collapse')) {
removeClass(el, 'el-menu--collapse')
el.dataset.oldOverflow = el.style.overflow
el.dataset.scrollWidth = el.clientWidth.toString()
addClass(el, 'el-menu--collapse')
} else {
addClass(el, 'el-menu--collapse')
el.dataset.oldOverflow = el.style.overflow
el.dataset.scrollWidth = el.clientWidth.toString()
removeClass(el, 'el-menu--collapse')
}
el.style.width = `${el.scrollWidth}px`
el.style.overflow = 'hidden'
},
leave(el: HTMLElement) {
addClass(el, 'horizontal-collapse-transition')
el.style.width = `${el.dataset.scrollWidth}px`
},
const listeners = {
onBeforeEnter: (el) => (el.style.opacity = '0.2'),
onEnter(el, done) {
addClass(el, 'el-opacity-transition')
el.style.opacity = '1'
done()
},
onAfterEnter(el) {
removeClass(el, 'el-opacity-transition')
el.style.opacity = ''
},
onBeforeLeave(el) {
if (!el.dataset) {
;(el as any).dataset = {}
}
if (hasClass(el, 'el-menu--collapse')) {
removeClass(el, 'el-menu--collapse')
el.dataset.oldOverflow = el.style.overflow
el.dataset.scrollWidth = el.clientWidth.toString()
addClass(el, 'el-menu--collapse')
} else {
addClass(el, 'el-menu--collapse')
el.dataset.oldOverflow = el.style.overflow
el.dataset.scrollWidth = el.clientWidth.toString()
removeClass(el, 'el-menu--collapse')
}
el.style.width = `${el.scrollWidth}px`
el.style.overflow = 'hidden'
},
onLeave(el: HTMLElement) {
addClass(el, 'horizontal-collapse-transition')
el.style.width = `${el.dataset.scrollWidth}px`
},
} as BaseTransitionProps<HTMLElement> as TransitionProps
return {
listeners,
}
},
})

View File

@ -0,0 +1,6 @@
import type { ExtractPropTypes } from 'vue'
export const menuItemGroupProps = {
title: String,
} as const
export type MenuItemGroupProps = ExtractPropTypes<typeof menuItemGroupProps>

View File

@ -0,0 +1,53 @@
<template>
<li class="el-menu-item-group">
<div
class="el-menu-item-group__title"
:style="{ paddingLeft: `${levelPadding}px` }"
>
<template v-if="!$slots.title">{{ title }}</template>
<slot v-else name="title" />
</div>
<ul>
<slot />
</ul>
</li>
</template>
<script lang="ts">
import { defineComponent, computed, getCurrentInstance, inject } from 'vue'
import { throwError } from '@element-plus/utils/error'
import { menuItemGroupProps } from './menu-item-group'
import type { MenuProvider } from './types'
const COMPONENT_NAME = 'ElMenuItemGroup'
export default defineComponent({
name: COMPONENT_NAME,
props: menuItemGroupProps,
setup() {
const instance = getCurrentInstance()!
const menu = inject<MenuProvider>('rootMenu')
if (!menu) throwError(COMPONENT_NAME, 'can not inject root menu')
const levelPadding = computed(() => {
if (menu.props.collapse) return 20
let padding = 20
let parent = instance.parent
while (parent && parent.type.name !== 'ElMenu') {
if (parent.type.name === 'ElSubMenu') {
padding += 20
}
parent = parent.parent
}
return padding
})
return {
levelPadding,
}
},
})
</script>

View File

@ -0,0 +1,24 @@
import { buildProp, definePropType } from '@element-plus/utils/props'
import { isString } from '@element-plus/utils/util'
import type { ExtractPropTypes } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
import type { MenuItemRegistered } from './types'
export const menuItemProps = {
index: {
type: String,
default: null,
},
route: buildProp({
type: definePropType<RouteLocationRaw>([String, Object]),
}),
disabled: Boolean,
} as const
export type MenuItemProps = ExtractPropTypes<typeof menuItemProps>
export const menuItemEmits = {
click: (item: MenuItemRegistered) =>
isString(item.index) && Array.isArray(item.indexPath),
}
export type MenuItemEmits = typeof menuItemEmits

View File

@ -0,0 +1,128 @@
<template>
<li
class="el-menu-item"
role="menuitem"
tabindex="-1"
:style="paddingStyle"
:class="{
'is-active': active,
'is-disabled': disabled,
}"
@click="handleClick"
>
<el-tooltip
v-if="
parentMenu.type.name === 'ElMenu' &&
rootMenu.props.collapse &&
$slots.title
"
:effect="Effect.DARK"
placement="right"
>
<template #content>
<slot name="title" />
</template>
<div
:style="{
position: 'absolute',
left: 0,
top: 0,
height: '100%',
width: '100%',
display: 'inline-block',
boxSizing: 'border-box',
padding: '0 20px',
}"
>
<slot />
</div>
</el-tooltip>
<template v-else>
<slot />
<slot name="title" />
</template>
</li>
</template>
<script lang="ts">
import {
defineComponent,
computed,
onMounted,
onBeforeUnmount,
inject,
getCurrentInstance,
toRef,
reactive,
} from 'vue'
import ElTooltip from '@element-plus/components/tooltip'
import { Effect } from '@element-plus/components/popper'
import { throwError } from '@element-plus/utils/error'
import useMenu from './use-menu'
import { menuItemEmits, menuItemProps } from './menu-item'
import type { MenuItemRegistered, MenuProvider, SubMenuProvider } from './types'
const COMPONENT_NAME = 'ElMenuItem'
export default defineComponent({
name: COMPONENT_NAME,
components: {
ElTooltip,
},
props: menuItemProps,
emits: menuItemEmits,
setup(props, { emit }) {
const instance = getCurrentInstance()!
const rootMenu = inject<MenuProvider>('rootMenu')
if (!rootMenu) throwError(COMPONENT_NAME, 'can not inject root menu')
const { parentMenu, paddingStyle, indexPath } = useMenu(
instance,
toRef(props, 'index')
)
const subMenu = inject<SubMenuProvider>(`subMenu:${parentMenu.value.uid}`)
if (!subMenu) throwError(COMPONENT_NAME, 'can not inject sub menu')
const active = computed(() => props.index === rootMenu.activeIndex)
const item: MenuItemRegistered = reactive({
index: props.index,
indexPath,
active,
})
const handleClick = () => {
if (!props.disabled) {
rootMenu.handleMenuItemClick({
index: props.index,
indexPath: indexPath.value,
route: props.route,
})
emit('click', item)
}
}
onMounted(() => {
subMenu.addSubMenu(item)
rootMenu.addMenuItem(item)
})
onBeforeUnmount(() => {
subMenu.removeSubMenu(item)
rootMenu.removeMenuItem(item)
})
return {
Effect,
parentMenu,
rootMenu,
paddingStyle,
active,
handleClick,
}
},
})
</script>

View File

@ -6,70 +6,102 @@ import {
ref,
provide,
onMounted,
isRef,
h,
withDirectives,
reactive,
} from 'vue'
import { Resize } from '@element-plus/directives'
import Menubar from '@element-plus/utils/menu/menu-bar'
import { buildProp, definePropType, mutable } from '@element-plus/utils/props'
import { isString, isObject } from '@element-plus/utils/util'
import ElMenuCollapseTransition from './menu-collapse-transition.vue'
import ElSubMenu from './submenu.vue'
import ElSubMenu from './sub-menu'
import { useMenuCssVar } from './use-menu-css-var'
import type { VNode, Ref, ComputedRef } from 'vue'
import type {
IMenuProps,
RootMenuProvider,
RegisterMenuItem,
SubMenuProvider,
} from './menu.type'
import type { MenuItemClicked, MenuProvider, SubMenuProvider } from './types'
import type { NavigationFailure, Router } from 'vue-router'
import type { VNode, ExtractPropTypes, VNodeNormalizedChildren } from 'vue'
export const menuProps = {
mode: buildProp({
type: String,
values: ['horizontal', 'vertical'],
default: 'vertical',
} as const),
defaultActive: buildProp({
type: String,
default: '',
} as const),
defaultOpeneds: buildProp({
type: definePropType<string[]>(Array),
default: () => mutable([] as const),
}),
uniqueOpened: Boolean,
router: Boolean,
menuTrigger: buildProp({
type: String,
values: ['hover', 'click'],
default: 'hover',
} as const),
collapse: Boolean,
backgroundColor: String,
textColor: String,
activeTextColor: String,
collapseTransition: buildProp({
type: Boolean,
default: true,
} as const),
} as const
export type MenuProps = ExtractPropTypes<typeof menuProps>
const checkIndexPath = (indexPath: unknown): indexPath is string[] =>
Array.isArray(indexPath) && indexPath.every((path) => isString(path))
export const menuEmits = {
close: (index: string, indexPath: string[]) =>
isString(index) && checkIndexPath(indexPath),
open: (index: string, indexPath: string[]) =>
isString(index) && checkIndexPath(indexPath),
select: (
index: string,
indexPath: string[],
item: MenuItemClicked,
routerResult?: Promise<void | NavigationFailure>
) =>
isString(index) &&
checkIndexPath(indexPath) &&
isObject(item) &&
(routerResult === undefined || routerResult instanceof Promise),
}
export type MenuEmits = typeof menuEmits
export default defineComponent({
name: 'ElMenu',
props: {
mode: {
type: String,
default: 'vertical',
},
defaultActive: {
type: String,
default: '',
},
defaultOpeneds: Array,
uniqueOpened: Boolean,
router: Boolean,
menuTrigger: {
type: String,
default: 'hover',
},
collapse: Boolean,
backgroundColor: { type: String },
textColor: { type: String },
activeTextColor: { type: String },
collapseTransition: {
type: Boolean,
default: true,
},
},
emits: ['close', 'open', 'select'],
props: menuProps,
emits: menuEmits,
setup(props, { emit, slots, expose }) {
const instance = getCurrentInstance()!
const router = instance.appContext.config.globalProperties.$router as Router
const menu = ref<HTMLUListElement>()
setup(props: IMenuProps, { emit, slots, expose }) {
// data
const openedMenus = ref(
const openedMenus = ref<MenuProvider['openedMenus']>(
props.defaultOpeneds && !props.collapse
? props.defaultOpeneds.slice(0)
: []
)
const instance = getCurrentInstance()
const activeIndex = ref(props.defaultActive)
const items = ref({})
const submenus = ref({})
const activeIndex = ref<MenuProvider['activeIndex']>(props.defaultActive)
const items = ref<MenuProvider['items']>({})
const subMenus = ref<MenuProvider['subMenus']>({})
const alteredCollapse = ref(false)
const router = instance.appContext.config.globalProperties.$router
const menu = ref(null)
// computed
const isMenuPopup = computed(() => {
const isMenuPopup = computed<MenuProvider['isMenuPopup']>(() => {
return (
props.mode === 'horizontal' ||
(props.mode === 'vertical' && props.collapse)
@ -77,111 +109,80 @@ export default defineComponent({
})
// methods
const initializeMenu = () => {
const index = activeIndex.value
const activeItem = items.value[index]
const initMenu = () => {
const activeItem = activeIndex.value && items.value[activeIndex.value]
if (!activeItem || props.mode === 'horizontal' || props.collapse) return
const indexPath = activeItem.indexPath
// 展开该菜单项的路径上所有子菜单
// expand all submenus of the menu item
// expand all subMenus of the menu item
indexPath.forEach((index) => {
const submenu = submenus.value[index]
submenu && openMenu(index, submenu?.indexPath)
const subMenu = subMenus.value[index]
subMenu && openMenu(index, subMenu.indexPath)
})
}
const addSubMenu = (item: RegisterMenuItem) => {
submenus.value[item.index] = item
}
const removeSubMenu = (item: RegisterMenuItem) => {
delete submenus.value[item.index]
}
const addMenuItem = (item: RegisterMenuItem) => {
items.value[item.index] = item
}
const removeMenuItem = (item: RegisterMenuItem) => {
delete items.value[item.index]
}
const openMenu = (index: string, indexPath?: Ref<string[]> | string[]) => {
const openMenu: MenuProvider['openMenu'] = (index, indexPath) => {
if (openedMenus.value.includes(index)) return
// 将不在该菜单路径下的其余菜单收起
// collapse all menu that are not under current menu item
if (props.uniqueOpened) {
openedMenus.value = openedMenus.value.filter((index: string) => {
return (
(isRef(indexPath) ? indexPath.value : indexPath).indexOf(index) !==
-1
)
})
openedMenus.value = openedMenus.value.filter((index: string) =>
indexPath.includes(index)
)
}
openedMenus.value.push(index)
}
const closeMenu = (index) => {
const closeMenu: MenuProvider['closeMenu'] = (index) => {
const i = openedMenus.value.indexOf(index)
if (i !== -1) {
openedMenus.value.splice(i, 1)
}
}
const open = (index) => {
const { indexPath } = submenus.value[index.toString()]
indexPath.forEach((i) => openMenu(i, indexPath))
}
const close = (index) => {
closeMenu(index)
}
const handleSubMenuClick = (submenu) => {
const { index, indexPath } = submenu
const handleSubMenuClick: MenuProvider['handleSubMenuClick'] = ({
index,
indexPath,
}) => {
const isOpened = openedMenus.value.includes(index)
if (isOpened) {
closeMenu(index)
emit('close', index, indexPath.value)
emit('close', index, indexPath)
} else {
openMenu(index, indexPath)
emit('open', index, indexPath.value)
emit('open', index, indexPath)
}
}
const handleMenuItemClick = (item: {
index: string
indexPath: ComputedRef<string[]>
route?: any
}) => {
const { index, indexPath } = item
const hasIndex = item.index !== null
const emitParams = [index, indexPath.value, item]
const handleMenuItemClick: MenuProvider['handleMenuItemClick'] = (
menuItem
) => {
if (props.mode === 'horizontal' || props.collapse) {
openedMenus.value = []
}
if (!hasIndex) {
return
}
const { index, indexPath } = menuItem
if (index === undefined || indexPath === undefined) return
if (props.router && router) {
const route = item.route || item.index
const routerResult = router.push(route).then((navigationResult) => {
if (!navigationResult) {
activeIndex.value = item.index
}
return navigationResult
const route = menuItem.route || index
const routerResult = router.push(route).then((res) => {
if (!res) activeIndex.value = index
return res
})
emit('select', ...emitParams.concat(routerResult))
emit(
'select',
index,
indexPath,
{ index, indexPath, route },
routerResult
)
} else {
activeIndex.value = item.index
emit('select', ...emitParams)
activeIndex.value = index
emit('select', index, indexPath, { index, indexPath })
}
}
@ -189,24 +190,24 @@ export default defineComponent({
const itemsInData = items.value
const item =
itemsInData[val] ||
itemsInData[activeIndex.value] ||
(activeIndex.value && itemsInData[activeIndex.value]) ||
itemsInData[props.defaultActive]
if (item) {
activeIndex.value = item.index
initializeMenu()
initMenu()
} else {
// Can't find item when collapsing
// and activeIndex shouldn't be changed when 'collapse' was changed.
// Then reset 'alteredCollapse' immediately.
if (!alteredCollapse.value) {
activeIndex.value = null
activeIndex.value = undefined
} else {
alteredCollapse.value = false
}
}
}
const handleResize = () => instance.proxy.$forceUpdate()
const handleResize = () => instance.proxy!.$forceUpdate()
watch(
() => props.defaultActive,
@ -218,9 +219,7 @@ export default defineComponent({
}
)
watch(items.value, () => {
initializeMenu()
})
watch(items.value, () => initMenu())
watch(
() => props.collapse,
@ -233,55 +232,78 @@ export default defineComponent({
)
// provide
provide<RootMenuProvider>('rootMenu', {
props,
openedMenus,
items,
submenus,
activeIndex,
isMenuPopup,
{
const addSubMenu: MenuProvider['addSubMenu'] = (item) => {
subMenus.value[item.index] = item
}
methods: {
addMenuItem,
removeMenuItem,
const removeSubMenu: MenuProvider['removeSubMenu'] = (item) => {
delete subMenus.value[item.index]
}
const addMenuItem: MenuProvider['addMenuItem'] = (item) => {
items.value[item.index] = item
}
const removeMenuItem: MenuProvider['removeMenuItem'] = (item) => {
delete items.value[item.index]
}
provide<MenuProvider>(
'rootMenu',
reactive({
props,
openedMenus,
items,
subMenus,
activeIndex,
isMenuPopup,
addMenuItem,
removeMenuItem,
addSubMenu,
removeSubMenu,
openMenu,
closeMenu,
handleMenuItemClick,
handleSubMenuClick,
})
)
provide<SubMenuProvider>(`subMenu:${instance.uid}`, {
addSubMenu,
removeSubMenu,
openMenu,
closeMenu,
handleMenuItemClick,
handleSubMenuClick,
},
})
provide<SubMenuProvider>(`subMenu:${instance.uid}`, {
addSubMenu,
removeSubMenu,
})
})
}
// lifecycle
onMounted(() => {
initializeMenu()
initMenu()
if (props.mode === 'horizontal') {
new Menubar(instance.vnode.el)
new Menubar(instance.vnode.el!)
}
})
expose({
open,
close,
})
{
const open = (index: string) => {
const { indexPath } = subMenus.value[index]
indexPath.forEach((i) => openMenu(i, indexPath))
}
expose({
open,
close: closeMenu,
})
}
const flattedChildren = (children) => {
const temp = Array.isArray(children) ? children : [children]
const res = []
temp.forEach((child) => {
const flattedChildren = (children: VNodeNormalizedChildren) => {
const vnodes = Array.isArray(children) ? children : [children]
const result: any[] = []
vnodes.forEach((child: any) => {
if (Array.isArray(child.children)) {
res.push(...flattedChildren(child.children))
result.push(...flattedChildren(child.children))
} else {
res.push(child)
result.push(child)
}
})
return res
return result
}
const useVNodeResize = (vnode: VNode) =>
@ -290,12 +312,10 @@ export default defineComponent({
: vnode
return () => {
let slot = slots.default?.() ?? []
const showMore = []
const vShowMore: VNode[] = []
if (props.mode === 'horizontal' && menu.value) {
const items = Array.from(
(menu.value as Node | undefined)?.childNodes ?? []
).filter(
const items = Array.from(menu.value?.childNodes ?? []).filter(
(item) => item.nodeName !== '#text' || item.nodeValue
) as HTMLElement[]
const originalSlot = flattedChildren(slot)
@ -317,11 +337,11 @@ export default defineComponent({
sliceIndex = index + 1
}
})
const defaultSlot = originalSlot.slice(0, sliceIndex)
const moreSlot = originalSlot.slice(sliceIndex)
if (moreSlot?.length) {
slot = defaultSlot
showMore.push(
const slotDefault = originalSlot.slice(0, sliceIndex)
const slotMore = originalSlot.slice(sliceIndex)
if (slotMore?.length) {
slot = slotDefault
vShowMore.push(
h(
ElSubMenu,
{
@ -333,7 +353,7 @@ export default defineComponent({
h('i', {
class: ['el-icon-more', 'el-sub-menu__icon-more'],
}),
default: () => moreSlot,
default: () => slotMore,
}
)
)
@ -342,7 +362,7 @@ export default defineComponent({
const ulStyle = useMenuCssVar(props)
const vnodeMenu = useVNodeResize(
const vMenu = useVNodeResize(
h(
'ul',
{
@ -356,15 +376,15 @@ export default defineComponent({
'el-menu--collapse': props.collapse,
},
},
[...slot.map((vnode) => useVNodeResize(vnode)), ...showMore]
[...slot.map((vnode) => useVNodeResize(vnode)), ...vShowMore]
)
)
if (props.collapseTransition && props.mode === 'vertical') {
return h(ElMenuCollapseTransition, () => vnodeMenu)
return h(ElMenuCollapseTransition, () => vMenu)
}
return vnodeMenu
return vMenu
}
},
})

View File

@ -1,103 +0,0 @@
import type { Ref, ComputedRef } from 'vue'
export interface RegisterMenuItem {
index: string
indexPath: ComputedRef<string[]>
active: ComputedRef<boolean>
}
export interface RootMenuData {
activeIndex: string
openedMenus: unknown[]
items: any
submenus: any
isMenuPopup: boolean
}
export interface RootMenuProvider {
openedMenus: Ref<RootMenuData['openedMenus']>
items: Ref<RootMenuData['items']>
submenus: Ref<RootMenuData['submenus']>
activeIndex: Ref<RootMenuData['activeIndex']>
hoverBackground: Ref<string>
isMenuPopup: Ref<RootMenuData['isMenuPopup']>
props: Readonly<Partial<RootMenuProps>>
methods: {
addMenuItem: (item: RegisterMenuItem) => void
removeMenuItem: (item: RegisterMenuItem) => void
addSubMenu: (item: RegisterMenuItem) => void
removeSubMenu: (item: RegisterMenuItem) => void
openMenu: (index: string, indexPath: Ref<string[]>) => void
closeMenu: (index: string) => void
handleMenuItemClick: (item: {
index: string
indexPath: ComputedRef<string[]>
route?: any
}) => void
handleSubMenuClick: (submenu: {
index: string
indexPath: ComputedRef<string[]>
}) => void
}
}
export interface SubMenuProvider {
addSubMenu: (item: RegisterMenuItem) => void
removeSubMenu: (item: RegisterMenuItem) => void
handleMouseleave?: (deepDispatch: boolean) => void
}
// root menu
export interface IMenuProps {
mode?: string | 'vertical' | 'horizontal'
defaultActive?: string
defaultOpeneds?: unknown[]
uniqueOpened?: boolean
router?: boolean
menuTrigger?: string | 'hover' | 'click'
collapse?: boolean
backgroundColor?: string
textColor?: string
activeTextColor?: string
collapseTransition?: boolean
}
export interface RootMenuProps {
mode: string
defaultActive: string
defaultOpeneds: unknown[]
uniqueOpened: boolean
router: boolean
menuTrigger: string
collapse: boolean
backgroundColor: string
textColor: string
activeTextColor: string
collapseTransition: boolean
}
// submenu
export interface ISubMenuProps {
index: string
showTimeout?: number
hideTimeout?: number
popperClass?: string
disabled?: boolean
popperAppendToBody?: boolean
}
// menuItem
export interface IMenuItemProps {
index: string
route: string | Record<string, unknown>
disabled: boolean
}
// menuGroup
export interface IMenuGroupProps {
title: string
}

View File

@ -1,128 +0,0 @@
<template>
<li
class="el-menu-item"
role="menuitem"
tabindex="-1"
:style="paddingStyle"
:class="{
'is-active': active,
'is-disabled': disabled,
}"
@click="handleClick"
>
<el-tooltip
v-if="
parentMenu.type.name === 'ElMenu' &&
rootMenu.props.collapse &&
slots.title
"
:effect="Effect.DARK"
placement="right"
>
<template #content>
<slot name="title"></slot>
</template>
<div
style="
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
display: inline-block;
box-sizing: border-box;
padding: 0 20px;
"
>
<slot></slot>
</div>
</el-tooltip>
<template v-else>
<slot></slot>
<slot name="title"></slot>
</template>
</li>
</template>
<script lang="ts">
import {
defineComponent,
computed,
onMounted,
onBeforeUnmount,
inject,
getCurrentInstance,
} from 'vue'
import ElTooltip from '@element-plus/components/tooltip'
import { Effect } from '@element-plus/components/popper'
import useMenu from './use-menu'
import type { RootMenuProvider, SubMenuProvider } from './menu.type'
export default defineComponent({
name: 'ElMenuItem',
// componentName: 'ElMenuItem',
components: { ElTooltip },
props: {
index: {
type: String,
default: null,
},
route: [String, Object],
disabled: Boolean,
},
emits: ['click'],
setup(props, { emit, slots }) {
const instance = getCurrentInstance()
const rootMenu = inject<RootMenuProvider>('rootMenu')
const { parentMenu, paddingStyle, indexPath } = useMenu(
instance,
computed(() => props.index)
)
const { addSubMenu, removeSubMenu } = inject<SubMenuProvider>(
`subMenu:${parentMenu.value.uid}`
)
const active = computed(() => {
return props.index === rootMenu.activeIndex.value
})
const handleClick = () => {
if (!props.disabled) {
rootMenu.methods.handleMenuItemClick({
index: props.index,
indexPath,
route: props.route,
})
emit('click', {
index: props.index,
indexPath: indexPath.value,
})
}
}
onMounted(() => {
addSubMenu({ index: props.index, indexPath, active })
rootMenu.methods.addMenuItem({ index: props.index, indexPath, active })
})
onBeforeUnmount(() => {
removeSubMenu({ index: props.index, indexPath, active })
rootMenu.methods.removeMenuItem({ index: props.index, indexPath, active })
})
return {
Effect,
parentMenu,
rootMenu,
slots,
paddingStyle,
active,
handleClick,
}
},
})
</script>

View File

@ -1,67 +0,0 @@
<template>
<li class="el-menu-item-group">
<div
class="el-menu-item-group__title"
:style="{ paddingLeft: levelPadding + 'px' }"
>
<template v-if="!slots.title">{{ title }}</template>
<slot v-else name="title"></slot>
</div>
<ul>
<slot></slot>
</ul>
</li>
</template>
<script lang="ts">
import {
defineComponent,
computed,
getCurrentInstance,
inject,
reactive,
} from 'vue'
import type { IMenuGroupProps, RootMenuProvider } from './menu.type'
export default defineComponent({
name: 'ElMenuItemGroup',
componentName: 'ElMenuItemGroup',
props: {
title: {
type: String,
},
},
setup(props: IMenuGroupProps, { slots }) {
// data
const data = reactive({
paddingLeft: 20,
})
const instance = getCurrentInstance()
// computed
const levelPadding = computed(() => {
let padding = 20
let parent = instance.parent
if (rootProps.collapse) return 20
while (parent && parent.type.name !== 'ElMenu') {
if (parent.type.name === 'ElSubMenu') {
padding += 20
}
parent = parent.parent
}
return padding
})
// inject
const { props: rootProps } = inject<RootMenuProvider>('rootMenu')
return {
data,
levelPadding,
props,
slots,
}
},
})
</script>

View File

@ -0,0 +1,422 @@
import {
defineComponent,
computed,
ref,
provide,
inject,
getCurrentInstance,
watch,
onMounted,
onBeforeUnmount,
withDirectives,
Fragment,
vShow,
h,
reactive,
} from 'vue'
import { useTimeoutFn } from '@vueuse/core'
import ElCollapseTransition from '@element-plus/components/collapse-transition'
import ElPopper from '@element-plus/components/popper'
import { buildProp } from '@element-plus/utils/props'
import { throwError } from '@element-plus/utils/error'
import useMenu from './use-menu'
import { useMenuCssVar } from './use-menu-css-var'
import type { Placement } from '@element-plus/components/popper'
import type { ExtractPropTypes, VNodeArrayChildren, CSSProperties } from 'vue'
import type { MenuProvider, SubMenuProvider } from './types'
export const subMenuProps = {
index: {
type: String,
required: true,
},
showTimeout: buildProp({
type: Number,
default: 300,
} as const),
hideTimeout: buildProp({
type: Number,
default: 300,
} as const),
popperClass: String,
disabled: Boolean,
popperAppendToBody: buildProp({
type: Boolean,
default: undefined,
} as const),
} as const
export type SubMenuProps = ExtractPropTypes<typeof subMenuProps>
const COMPONENT_NAME = 'ElSubMenu'
export default defineComponent({
name: COMPONENT_NAME,
props: subMenuProps,
setup(props, { slots, expose }) {
const instance = getCurrentInstance()!
const { paddingStyle, indexPath, parentMenu } = useMenu(
instance,
computed(() => props.index)
)
// inject
const rootMenu = inject<MenuProvider>('rootMenu')
if (!rootMenu) throwError(COMPONENT_NAME, 'can not inject root menu')
const subMenu = inject<SubMenuProvider>(`subMenu:${parentMenu.value!.uid}`)
if (!subMenu) throwError(COMPONENT_NAME, 'can not inject sub menu')
const items = ref<MenuProvider['items']>({})
const subMenus = ref<MenuProvider['subMenus']>({})
let timeout: (() => void) | undefined
const currentPlacement = ref<Placement | ''>('')
const mouseInChild = ref(false)
const verticalTitleRef = ref<HTMLDivElement>()
const vPopper = ref()
// computed
const subMenuTitleIcon = computed(() => {
return (mode.value === 'horizontal' && isFirstLevel.value) ||
(mode.value === 'vertical' && !rootMenu.props.collapse)
? 'el-icon-arrow-down'
: 'el-icon-arrow-right'
})
const isFirstLevel = computed(() => {
let isFirstLevel = true
let parent = instance.parent
while (parent && parent.type.name !== 'ElMenu') {
if (['ElSubMenu', 'ElMenuItemGroup'].includes(parent.type.name!)) {
isFirstLevel = false
break
} else {
parent = parent.parent
}
}
return isFirstLevel
})
const appendToBody = computed(() => {
return props.popperAppendToBody === undefined
? isFirstLevel.value
: Boolean(props.popperAppendToBody)
})
const menuTransitionName = computed(() =>
rootMenu.props.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top'
)
const fallbackPlacements = computed<Placement[]>(() =>
mode.value === 'horizontal' && isFirstLevel.value
? [
'bottom-start',
'bottom-end',
'top-start',
'top-end',
'right-start',
'left-start',
]
: [
'right-start',
'left-start',
'bottom-start',
'bottom-end',
'top-start',
'top-end',
]
)
const opened = computed(() => rootMenu.openedMenus.includes(props.index))
const active = computed(() => {
let isActive = false
Object.values(items.value).forEach((item) => {
if (item.active) {
isActive = true
}
})
Object.values(subMenus.value).forEach((subItem) => {
if (subItem.active) {
isActive = true
}
})
return isActive
})
const backgroundColor = computed(() => rootMenu.props.backgroundColor || '')
const activeTextColor = computed(() => rootMenu.props.activeTextColor || '')
const textColor = computed(() => rootMenu.props.textColor || '')
const mode = computed(() => rootMenu.props.mode)
const item = reactive({
index: props.index,
indexPath,
active,
})
const titleStyle = computed<CSSProperties>(() => {
if (mode.value !== 'horizontal') {
return {
color: textColor.value,
}
}
return {
borderBottomColor: active.value
? rootMenu.props.activeTextColor
? activeTextColor.value
: ''
: 'transparent',
color: active.value ? activeTextColor.value : textColor.value,
}
})
// methods
const doDestroy = () => vPopper.value?.doDestroy()
const handleCollapseToggle = (value: boolean) => {
if (value) {
updatePlacement()
} else {
doDestroy()
}
}
const handleClick = () => {
if (
(rootMenu.props.menuTrigger === 'hover' &&
rootMenu.props.mode === 'horizontal') ||
(rootMenu.props.collapse && rootMenu.props.mode === 'vertical') ||
props.disabled
)
return
rootMenu.handleSubMenuClick({
index: props.index,
indexPath: indexPath.value,
active: active.value,
})
}
const handleMouseenter = (
event: MouseEvent | FocusEvent,
showTimeout = props.showTimeout
) => {
if (event.type === 'focus' && !event.relatedTarget) {
return
}
if (
(rootMenu.props.menuTrigger === 'click' &&
rootMenu.props.mode === 'horizontal') ||
(!rootMenu.props.collapse && rootMenu.props.mode === 'vertical') ||
props.disabled
) {
return
}
mouseInChild.value = true
timeout?.()
;({ stop: timeout } = useTimeoutFn(
() => rootMenu.openMenu(props.index, indexPath.value),
showTimeout
))
if (appendToBody.value) {
parentMenu.value.vnode.el?.dispatchEvent(new MouseEvent('mouseenter'))
}
}
const handleMouseleave = (deepDispatch = false) => {
if (
(rootMenu.props.menuTrigger === 'click' &&
rootMenu.props.mode === 'horizontal') ||
(!rootMenu.props.collapse && rootMenu.props.mode === 'vertical')
) {
return
}
mouseInChild.value = false
timeout?.()
;({ stop: timeout } = useTimeoutFn(
() => !mouseInChild.value && rootMenu.closeMenu(props.index),
props.hideTimeout
))
if (appendToBody.value && deepDispatch) {
if (instance.parent?.type.name === 'ElSubMenu') {
subMenu.handleMouseleave?.(true)
}
}
}
const updatePlacement = () => {
currentPlacement.value =
mode.value === 'horizontal' && isFirstLevel.value
? 'bottom-start'
: 'right-start'
}
watch(
() => rootMenu.props.collapse,
(value) => handleCollapseToggle(Boolean(value))
)
// provide
{
const addSubMenu: SubMenuProvider['addSubMenu'] = (item) => {
subMenus.value[item.index] = item
}
const removeSubMenu: SubMenuProvider['removeSubMenu'] = (item) => {
delete subMenus.value[item.index]
}
provide<SubMenuProvider>(`subMenu:${instance.uid}`, {
addSubMenu,
removeSubMenu,
handleMouseleave,
})
}
// expose
expose({
opened,
})
// lifecycle
onMounted(() => {
rootMenu.addSubMenu(item)
subMenu.addSubMenu(item)
updatePlacement()
})
onBeforeUnmount(() => {
subMenu.removeSubMenu(item)
rootMenu.removeSubMenu(item)
})
return () => {
const titleTag: VNodeArrayChildren = [
slots.title?.(),
h('i', {
class: ['el-sub-menu__icon-arrow', subMenuTitleIcon.value],
}),
]
const ulStyle = useMenuCssVar(rootMenu.props)
// this render function is only used for bypass `Vue`'s compiler caused patching issue.
// temporarily mark ElPopper as any due to type inconsistency.
const child = rootMenu.isMenuPopup
? h(
// TODO: correct popper's type.
ElPopper as any,
{
ref: vPopper,
manualMode: true,
visible: opened.value,
effect: 'light',
pure: true,
offset: 6,
showArrow: false,
popperClass: props.popperClass,
placement: currentPlacement.value,
appendToBody: appendToBody.value,
fallbackPlacements: fallbackPlacements.value,
transition: menuTransitionName.value,
gpuAcceleration: false,
},
{
default: () =>
h(
'div',
{
class: [`el-menu--${mode.value}`, props.popperClass],
onMouseenter: (evt: MouseEvent) =>
handleMouseenter(evt, 100),
onMouseleave: () => handleMouseleave(true),
onFocus: (evt: FocusEvent) => handleMouseenter(evt, 100),
},
[
h(
'ul',
{
class: [
'el-menu el-menu--popup',
`el-menu--popup-${currentPlacement.value}`,
],
style: ulStyle.value,
},
[slots.default?.()]
),
]
),
trigger: () =>
h(
'div',
{
class: 'el-sub-menu__title',
style: [
paddingStyle.value,
titleStyle.value,
{ backgroundColor: backgroundColor.value },
],
onClick: handleClick,
},
titleTag
),
}
)
: h(Fragment, {}, [
h(
'div',
{
class: 'el-sub-menu__title',
style: [
paddingStyle.value,
titleStyle.value,
{ backgroundColor: backgroundColor.value },
],
ref: verticalTitleRef,
onClick: handleClick,
},
titleTag
),
h(
ElCollapseTransition,
{},
{
default: () =>
withDirectives(
h(
'ul',
{
role: 'menu',
class: 'el-menu el-menu--inline',
style: ulStyle.value,
},
[slots.default?.()]
),
[[vShow, opened.value]]
),
}
),
])
return h(
'li',
{
class: [
'el-sub-menu',
{
'is-active': active.value,
'is-opened': opened.value,
'is-disabled': props.disabled,
},
],
role: 'menuitem',
ariaHaspopup: true,
ariaExpanded: opened.value,
onMouseenter: handleMouseenter,
onMouseleave: () => handleMouseleave(true),
onFocus: handleMouseenter,
},
[child]
)
}
},
})

View File

@ -1,483 +0,0 @@
<script lang="ts">
import {
defineComponent,
computed,
ref,
provide,
inject,
getCurrentInstance,
reactive,
watch,
onMounted,
onBeforeUnmount,
withDirectives,
Fragment,
vShow,
h,
} from 'vue'
import ElCollapseTransition from '@element-plus/components/collapse-transition'
import ElPopper from '@element-plus/components/popper'
import useMenu from './use-menu'
import { useMenuCssVar } from './use-menu-css-var'
import type {
ISubMenuProps,
RootMenuProvider,
SubMenuProvider,
} from './menu.type'
export default defineComponent({
name: 'ElSubMenu',
props: {
index: {
type: String,
required: true,
},
showTimeout: {
type: Number,
default: 300,
},
hideTimeout: {
type: Number,
default: 300,
},
popperClass: String,
disabled: Boolean,
popperAppendToBody: {
type: Boolean,
default: undefined,
},
},
setup(props: ISubMenuProps) {
const data = reactive({
popperJS: null,
timeout: null,
items: {},
submenus: {},
currentPlacement: '',
mouseInChild: false,
opened: false,
})
const verticalTitleRef = ref<HTMLElement>(null)
const popperVnode = ref(null)
// instance
const instance = getCurrentInstance()
const { paddingStyle, indexPath, parentMenu } = useMenu(
instance,
computed(() => props.index)
)
// inject
const {
openedMenus,
isMenuPopup,
methods: rootMethods,
props: rootProps,
methods: { closeMenu },
} = inject<RootMenuProvider>('rootMenu')
const {
addSubMenu: parentAddSubMenu,
removeSubMenu: parentRemoveSubMenu,
handleMouseleave: parentHandleMouseleave,
} = inject<SubMenuProvider>(`subMenu:${parentMenu.value.uid}`)
// computed
const submenuTitleIcon = computed(() => {
return (mode.value === 'horizontal' && isFirstLevel.value) ||
(mode.value === 'vertical' && !rootProps.collapse)
? 'el-icon-arrow-down'
: 'el-icon-arrow-right'
})
const isFirstLevel = computed(() => {
let isFirstLevel = true
let parent = instance.parent
while (parent && parent.type.name !== 'ElMenu') {
if (['ElSubMenu', 'ElMenuItemGroup'].includes(parent.type.name)) {
isFirstLevel = false
break
} else {
parent = parent.parent
}
}
return isFirstLevel
})
const appendToBody = computed(() => {
return props.popperAppendToBody === undefined
? isFirstLevel.value
: Boolean(props.popperAppendToBody)
})
const menuTransitionName = computed(() => {
return rootProps.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top'
})
const fallbackPlacements = computed(() =>
mode.value === 'horizontal' && isFirstLevel.value
? [
'bottom-start',
'bottom-end',
'top-start',
'top-end',
'right-start',
'left-start',
]
: [
'right-start',
'left-start',
'bottom-start',
'bottom-end',
'top-start',
'top-end',
]
)
const opened = computed(() => {
return openedMenus.value.includes(props.index)
})
const active = computed(() => {
let isActive = false
const submenus = data.submenus
const items = data.items
Object.keys(items).forEach((index) => {
if (items[index].active) {
isActive = true
}
})
Object.keys(submenus).forEach((index) => {
if (submenus[index].active) {
isActive = true
}
})
return isActive
})
const backgroundColor = computed(() => {
return rootProps.backgroundColor || ''
})
const activeTextColor = computed(() => {
return rootProps.activeTextColor || ''
})
const textColor = computed(() => {
return rootProps.textColor || ''
})
const mode = computed(() => {
return rootProps.mode
})
const titleStyle = computed(() => {
if (mode.value !== 'horizontal') {
return {
color: textColor.value,
}
}
return {
borderBottomColor: active.value
? rootProps.activeTextColor
? activeTextColor.value
: ''
: 'transparent',
color: active.value ? activeTextColor.value : textColor.value,
}
})
const doDestroy = () => {
popperVnode.value?.doDestroy()
}
// methods
const handleCollapseToggle = (value) => {
if (value) {
updatePlacement()
} else {
doDestroy()
}
}
const addItem = (item) => {
data.items[item.index] = item
}
const removeItem = (item) => {
delete data.items[item.index]
}
const addSubMenu = (item) => {
data.submenus[item.index] = item
}
const removeSubMenu = (item) => {
delete data.submenus[item.index]
}
const handleClick = () => {
const disabled = props.disabled
if (
(rootProps.menuTrigger === 'hover' &&
rootProps.mode === 'horizontal') ||
(rootProps.collapse && rootProps.mode === 'vertical') ||
disabled
) {
return
}
rootMethods.handleSubMenuClick({ index: props.index, indexPath })
}
const handleMouseenter = (event, showTimeout = props.showTimeout) => {
if (
!('ActiveXObject' in window) &&
event.type === 'focus' &&
!event.relatedTarget
) {
return
}
const disabled = props.disabled
if (
(rootProps.menuTrigger === 'click' &&
rootProps.mode === 'horizontal') ||
(!rootProps.collapse && rootProps.mode === 'vertical') ||
disabled
) {
return
}
data.mouseInChild = true
clearTimeout(data.timeout)
data.timeout = setTimeout(() => {
rootMethods.openMenu(props.index, indexPath)
}, showTimeout)
if (appendToBody.value) {
parentMenu.value.vnode.el.dispatchEvent(new MouseEvent('mouseenter'))
}
}
const handleMouseleave = (deepDispatch = false) => {
if (
(rootProps.menuTrigger === 'click' &&
rootProps.mode === 'horizontal') ||
(!rootProps.collapse && rootProps.mode === 'vertical')
) {
return
}
data.mouseInChild = false
clearTimeout(data.timeout)
data.timeout = setTimeout(() => {
!data.mouseInChild && closeMenu(props.index)
}, props.hideTimeout)
if (appendToBody.value && deepDispatch) {
if (instance.parent.type.name === 'ElSubMenu') {
parentHandleMouseleave(true)
}
}
}
const updatePlacement = () => {
data.currentPlacement =
mode.value === 'horizontal' && isFirstLevel.value
? 'bottom-start'
: 'right-start'
}
watch(
() => rootProps.collapse,
(value) => {
handleCollapseToggle(Boolean(value))
}
)
// provide
provide<SubMenuProvider>(`subMenu:${instance.uid}`, {
addSubMenu,
removeSubMenu,
handleMouseleave,
})
// lifecycle
onMounted(() => {
rootMethods.addSubMenu({
index: props.index,
indexPath,
active,
})
parentAddSubMenu({
index: props.index,
indexPath,
active,
})
updatePlacement()
})
onBeforeUnmount(() => {
parentRemoveSubMenu({
index: props.index,
indexPath,
active,
})
rootMethods.removeSubMenu({
index: props.index,
indexPath,
active,
})
})
return {
data,
props,
mode,
active,
isMenuPopup,
opened,
paddingStyle,
titleStyle,
backgroundColor,
rootProps,
menuTransitionName,
fallbackPlacements,
submenuTitleIcon,
appendToBody,
handleClick,
handleMouseenter,
handleMouseleave,
addItem,
removeItem,
addSubMenu,
removeSubMenu,
popperVnode,
verticalTitleRef,
}
},
render() {
const titleTag = [
this.$slots.title?.(),
h(
'i',
{
class: ['el-sub-menu__icon-arrow', this.submenuTitleIcon],
},
null
),
]
const ulStyle = useMenuCssVar(this.rootProps)
// this render function is only used for bypass `Vue`'s compiler caused patching issue.
// temporaryly mark ElPopper as any due to type inconsistency.
// TODO: correct popper's type.
const child = this.isMenuPopup
? h(
ElPopper as any,
{
ref: 'popperVNode',
manualMode: true,
visible: this.opened,
'onUpdate:visible': (val: boolean) => (this.opened = val),
effect: 'light',
pure: true,
offset: 6,
showArrow: false,
popperClass: this.popperClass,
placement: this.data.currentPlacement,
appendToBody: this.appendToBody,
fallbackPlacements: this.fallbackPlacements,
transition: this.menuTransitionName,
gpuAcceleration: false,
},
{
default: () =>
h(
'div',
{
ref: 'menu',
class: [`el-menu--${this.mode}`, this.popperClass],
onMouseenter: ($event: Event) =>
this.handleMouseenter($event, 100),
onMouseleave: () => this.handleMouseleave(true),
onFocus: ($event: Event) =>
this.handleMouseenter($event, 100),
},
[
h(
'ul',
{
class: [
'el-menu el-menu--popup',
`el-menu--popup-${this.data.currentPlacement}`,
],
style: ulStyle.value,
},
[this.$slots.default?.()]
),
]
),
trigger: () =>
h(
'div',
{
class: 'el-sub-menu__title',
style: [
this.paddingStyle,
this.titleStyle,
{ backgroundColor: this.backgroundColor },
],
onClick: this.handleClick,
},
titleTag
),
}
)
: h(Fragment, {}, [
h(
'div',
{
class: 'el-sub-menu__title',
style: [
this.paddingStyle,
this.titleStyle,
{ backgroundColor: this.backgroundColor },
],
ref: 'verticalTitleRef',
onClick: this.handleClick,
},
titleTag
),
h(
ElCollapseTransition,
{},
{
default: () =>
withDirectives(
h(
'ul',
{
role: 'menu',
class: 'el-menu el-menu--inline',
style: ulStyle.value,
},
[this.$slots.default?.()]
),
[[vShow, this.opened]]
),
}
),
])
return h(
'li',
{
class: [
'el-sub-menu',
{
'is-active': this.active,
'is-opened': this.opened,
'is-disabled': this.disabled,
},
],
role: 'menuitem',
ariaHaspopup: true,
ariaExpanded: this.opened,
onMouseenter: this.handleMouseenter,
onMouseleave: () => this.handleMouseleave(true),
onFocus: this.handleMouseenter,
},
[child]
)
},
})
</script>

View File

@ -0,0 +1,39 @@
import type { RouteLocationRaw } from 'vue-router'
import type { MenuProps } from './menu'
export interface MenuItemRegistered {
index: string
indexPath: string[]
active: boolean
}
export interface MenuItemClicked {
index: string
indexPath: string[]
route?: RouteLocationRaw
}
export interface MenuProvider {
openedMenus: string[]
items: Record<string, MenuItemRegistered>
subMenus: Record<string, MenuItemRegistered>
activeIndex?: string
isMenuPopup: boolean
props: MenuProps
addMenuItem: (item: MenuItemRegistered) => void
removeMenuItem: (item: MenuItemRegistered) => void
addSubMenu: (item: MenuItemRegistered) => void
removeSubMenu: (item: MenuItemRegistered) => void
openMenu: (index: string, indexPath: string[]) => void
closeMenu: (index: string) => void
handleMenuItemClick: (item: MenuItemClicked) => void
handleSubMenuClick: (subMenu: MenuItemRegistered) => void
}
export interface SubMenuProvider {
addSubMenu: (item: MenuItemRegistered) => void
removeSubMenu: (item: MenuItemRegistered) => void
handleMouseleave?: (deepDispatch: boolean) => void
}

View File

@ -1,9 +1,9 @@
import { computed } from 'vue'
import { darken } from '@element-plus/utils/color'
import type { IMenuProps } from './menu.type'
import type { MenuProps } from './menu'
export default function useMenuColor(props: IMenuProps) {
export default function useMenuColor(props: MenuProps) {
const menuBarColor = computed(() => {
const color = props.backgroundColor
if (!color) {

View File

@ -1,9 +1,9 @@
import { computed } from 'vue'
import useMenuColor from './use-menu-color'
import type { IMenuProps } from './menu.type'
import type { MenuProps } from './menu'
export const useMenuCssVar = (props: IMenuProps) => {
export const useMenuCssVar = (props: MenuProps) => {
return computed(() => {
return {
'--el-menu-text-color': props.textColor || '',

View File

@ -1,34 +1,36 @@
import { computed, inject } from 'vue'
import { throwError } from '@element-plus/utils/error'
import type { ComponentInternalInstance, ComputedRef } from 'vue'
import type { RootMenuProvider } from './menu.type'
import type { ComponentInternalInstance, Ref, CSSProperties } from 'vue'
import type { MenuProvider } from './types'
export default function useMenu(
instance: ComponentInternalInstance,
currentIndex: ComputedRef<string>
currentIndex: Ref<string>
) {
const rootMenu = inject<RootMenuProvider>('rootMenu')
const rootMenu = inject<MenuProvider>('rootMenu')
if (!rootMenu) throwError('useMenu', 'can not inject root menu')
const indexPath = computed(() => {
let parent = instance.parent
let parent = instance.parent!
const path = [currentIndex.value]
while (parent.type.name !== 'ElMenu') {
if (parent.props.index) {
path.unshift(parent.props.index as string)
}
parent = parent.parent
parent = parent.parent!
}
return path
})
const parentMenu = computed(() => {
let parent = instance.parent
while (parent && ['ElMenu', 'ElSubMenu'].indexOf(parent.type.name) === -1) {
while (parent && !['ElMenu', 'ElSubMenu'].includes(parent.type.name!)) {
parent = parent.parent
}
return parent
return parent!
})
const paddingStyle = computed(() => {
const paddingStyle = computed<CSSProperties>(() => {
let parent = instance.parent
if (rootMenu.props.mode !== 'vertical') return {}
@ -46,5 +48,10 @@ export default function useMenu(
}
return { paddingLeft: `${padding}px` }
})
return { parentMenu, paddingStyle, indexPath }
return {
parentMenu,
paddingStyle,
indexPath,
}
}