diff --git a/docs/en-US/component/menu.md b/docs/en-US/component/menu.md index 428d7aadb5..8a6473e894 100644 --- a/docs/en-US/component/menu.md +++ b/docs/en-US/component/menu.md @@ -63,23 +63,24 @@ menu/popper-offset ## Menu Attributes -| Name | Description | Type | Accepted Values | Default | -| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | --------------------- | -------- | -| mode | menu display mode | string | horizontal / vertical | vertical | -| collapse | whether the menu is collapsed (available only in vertical mode) | boolean | — | false | -| ellipsis | whether the menu is ellipsis (available only in horizontal mode) | boolean | — | true | -| ellipsis-icon^(2.5.0) | custom ellipsis icon (available only in horizontal mode and ellipsis is true) | `string \| Component` | — | — | -| popper-offset^(2.5.0) | offset of the popper (effective for all submenus) | number | — | 6 | -| background-color | background color of Menu (hex format) (deprecated, use `--bg-color` instead) | string | — | #ffffff | -| text-color | text color of Menu (hex format) (deprecated, use `--text-color` instead) | string | — | #303133 | -| active-text-color | text color of currently active menu item (hex format) (deprecated, use `--active-color` instead) | string | — | #409EFF | -| default-active | index of active menu on page load | string | — | — | -| default-openeds | array that contains indexes of currently active sub-menus | Array | — | — | -| unique-opened | whether only one sub-menu can be active | boolean | — | false | -| menu-trigger | how sub-menus are triggered, only works when `mode` is 'horizontal' | string | hover / click | hover | -| router | whether `vue-router` mode is activated. If true, index will be used as 'path' to activate the route action. Use with `default-active` to set the active item on load. | boolean | — | false | -| collapse-transition | whether to enable the collapse transition | boolean | — | true | -| popper-effect ^(2.2.26) | Tooltip theme, built-in theme: `dark` / `light` when menu is collapsed | string | dark / light | dark | +| Name | Description | Type | Accepted Values | Default | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | --------------------- | -------- | +| mode | menu display mode | string | horizontal / vertical | vertical | +| collapse | whether the menu is collapsed (available only in vertical mode) | boolean | — | false | +| ellipsis | whether the menu is ellipsis (available only in horizontal mode) | boolean | — | true | +| ellipsis-icon^(2.5.0) | custom ellipsis icon (available only in horizontal mode and ellipsis is true) | `string \| Component` | — | — | +| popper-offset^(2.5.0) | offset of the popper (effective for all submenus) | number | — | 6 | +| background-color | background color of Menu (hex format) (deprecated, use `--bg-color` instead) | string | — | #ffffff | +| text-color | text color of Menu (hex format) (deprecated, use `--text-color` instead) | string | — | #303133 | +| active-text-color | text color of currently active menu item (hex format) (deprecated, use `--active-color` instead) | string | — | #409EFF | +| default-active | index of active menu on page load | string | — | — | +| default-openeds | array that contains indexes of currently active sub-menus | Array | — | — | +| unique-opened | whether only one sub-menu can be active | boolean | — | false | +| menu-trigger | how sub-menus are triggered, only works when `mode` is 'horizontal' | string | hover / click | hover | +| router | whether `vue-router` mode is activated. If true, index will be used as 'path' to activate the route action. Use with `default-active` to set the active item on load. | boolean | — | false | +| collapse-transition | whether to enable the collapse transition | boolean | — | true | +| popper-effect ^(2.2.26) | Tooltip theme, built-in theme: `dark` / `light` when menu is collapsed | string | dark / light | dark | +| close-on-click-outside ^(2.5.0) | optional, whether menu is collapsed when clicking outside | `boolean` | — | false | ## Menu Methods diff --git a/packages/components/menu/src/menu.ts b/packages/components/menu/src/menu.ts index 5f2829d762..21380221a9 100644 --- a/packages/components/menu/src/menu.ts +++ b/packages/components/menu/src/menu.ts @@ -10,7 +10,9 @@ import { ref, watch, watchEffect, + withDirectives, } from 'vue' + import { useResizeObserver } from '@vueuse/core' import { isNil } from 'lodash-unified' import ElIcon from '@element-plus/components/icon' @@ -25,6 +27,7 @@ import { mutable, } from '@element-plus/utils' import { useNamespace } from '@element-plus/hooks' +import { ClickOutside as vClickoutside } from '@element-plus/directives' import Menubar from './utils/menu-bar' import ElMenuCollapseTransition from './menu-collapse-transition.vue' import ElSubMenu from './sub-menu' @@ -34,6 +37,7 @@ import type { MenuItemClicked, MenuProvider, SubMenuProvider } from './types' import type { NavigationFailure, Router } from 'vue-router' import type { Component, + DirectiveArguments, ExtractPropTypes, VNode, VNodeArrayChildren, @@ -65,6 +69,7 @@ export const menuProps = buildProps({ backgroundColor: String, textColor: String, activeTextColor: String, + closeOnClickOutside: Boolean, collapseTransition: { type: Boolean, default: true, @@ -270,6 +275,8 @@ export default defineComponent({ return sliceIndex === items.length ? -1 : sliceIndex } + const getIndexPath = (index: string) => subMenus.value[index].indexPath + // Common computer monitor FPS is 60Hz, which means 60 redraws per second. Calculation formula: 1000ms/60 ≈ 16.67ms, In order to avoid a certain chance of repeated triggering when `resize`, set wait to 16.67 * 2 = 33.34 const debounce = (fn: () => void, wait = 33.34) => { let timmer: ReturnType | null @@ -320,6 +327,8 @@ export default defineComponent({ else resizeStopper?.() }) + const mouseInChild = ref(false) + // provide { const addSubMenu: MenuProvider['addSubMenu'] = (item) => { @@ -360,7 +369,7 @@ export default defineComponent({ provide(`subMenu:${instance.uid}`, { addSubMenu, removeSubMenu, - mouseInChild: ref(false), + mouseInChild, level: 0, }) } @@ -429,20 +438,42 @@ export default defineComponent({ const ulStyle = useMenuCssVar(props, 0) - const vMenu = h( - 'ul', - { - key: String(props.collapse), - role: 'menubar', - ref: menu, - style: ulStyle.value, - class: { - [nsMenu.b()]: true, - [nsMenu.m(props.mode)]: true, - [nsMenu.m('collapse')]: props.collapse, + const directives: DirectiveArguments = props.closeOnClickOutside + ? [ + [ + vClickoutside, + () => { + if (!openedMenus.value.length) return + + if (!mouseInChild.value) { + openedMenus.value.forEach((openedMenu) => + emit('close', openedMenu, getIndexPath(openedMenu)) + ) + + openedMenus.value = [] + } + }, + ], + ] + : [] + + const vMenu = withDirectives( + h( + 'ul', + { + key: String(props.collapse), + role: 'menubar', + ref: menu, + style: ulStyle.value, + class: { + [nsMenu.b()]: true, + [nsMenu.m(props.mode)]: true, + [nsMenu.m('collapse')]: props.collapse, + }, }, - }, - [...slot, ...vShowMore] + [...slot, ...vShowMore] + ), + directives ) if (props.collapseTransition && props.mode === 'vertical') { diff --git a/packages/components/menu/src/sub-menu.ts b/packages/components/menu/src/sub-menu.ts index 457eb5e3c4..dda48438d2 100644 --- a/packages/components/menu/src/sub-menu.ts +++ b/packages/components/menu/src/sub-menu.ts @@ -239,6 +239,7 @@ export default defineComponent({ (!rootMenu.props.collapse && rootMenu.props.mode === 'vertical') || props.disabled ) { + subMenu.mouseInChild.value = true return } subMenu.mouseInChild.value = true @@ -259,6 +260,7 @@ export default defineComponent({ rootMenu.props.mode === 'horizontal') || (!rootMenu.props.collapse && rootMenu.props.mode === 'vertical') ) { + subMenu.mouseInChild.value = false return } timeout?.()