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
This commit is contained in:
cuongle-hdwebsoft 2023-12-15 12:34:22 +07:00 committed by GitHub
parent 1b4bc76f75
commit 7f687ae598
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 65 additions and 31 deletions

View File

@ -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

View File

@ -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<typeof setTimeout> | 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<SubMenuProvider>(`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') {

View File

@ -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?.()