From 7f687ae5981ad284cb5c2b4d741b8fd95628bbd2 Mon Sep 17 00:00:00 2001 From: cuongle-hdwebsoft <102006695+cuongle-hdwebsoft@users.noreply.github.com> Date: Fri, 15 Dec 2023 12:34:22 +0700 Subject: [PATCH] feat(components): [el-menu] allow user to hide menu when clicking outside (#14742) * fix(components): allow user to hide menu when clicking outside when user sets `menu-trigger` to `click` and then clicking outside, it should be close menu closed #14738 * revert settings.json config * fix: debounce hide menu when clicking out side * fix: only trigger click outside when menu is open * fix: hide parent menu * fix: only trigger close menu when its opened * refactor: remove redundant code * refactor: remove redundant code * fix(docs): `collapse-on-click-outside` prop * fix(docs): `collapse-on-click-outside` prop * fix: move `clickOutSide` to `menu.ts` * fix: submenu does not emit `close` when clicking outside * fix: submenu does not emit `close` when clicking outside * fix: ts type * fix: ts type * fix: only close when passing `props.collapseOnClickOutside` to `true` * refactor: create `directives` variable * improvement: allow `recusiveMouseInSubMenu` to receive array * refactor: remove redundant code * fix: rename `recusiveMouseInSubMenu` to `recursiveMouseInSubMenu` * feat: add `hide-timeout` prop * fix: revert code * fix: code owner * fix: coder owner * fix: menu closed when clicking on submenu * refactor: remove redundant code * fix: get correct index path * fix: build * refactor: update docs and remove `unref` * fix: change `collapseOnClickOutside` to `closeOnClickOutside` * fix: remove `hideTimeout` prop --- docs/en-US/component/menu.md | 35 +++++++------- packages/components/menu/src/menu.ts | 59 ++++++++++++++++++------ packages/components/menu/src/sub-menu.ts | 2 + 3 files changed, 65 insertions(+), 31 deletions(-) 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?.()