v-menu pointer event tweaks (#16512)

* v-menu pointer event tweaks

* apply tweak to .v-menu click & closeOnContentClick
This commit is contained in:
Azri Kahar
2022-11-29 03:39:23 +08:00
committed by GitHub
parent 2f5640d383
commit 0a839b53b4
4 changed files with 145 additions and 49 deletions

View File

@@ -0,0 +1,9 @@
// Vitest Snapshot v1
exports[`Mount component 1`] = `
"<div class=\\"v-menu\\" data-v-41b8fe03=\\"\\">
<div class=\\"v-menu-activator\\" data-v-41b8fe03=\\"\\"></div>
<!--teleport start-->
<!--teleport end-->
</div>"
`;

View File

@@ -1,36 +0,0 @@
import { test, expect, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import VMenu from './v-menu.vue';
import TransitionBounce from './transition/bounce.vue';
import { GlobalMountOptions } from '@vue/test-utils/dist/types';
import { directive } from '../../../../app/src/directives/click-outside';
beforeEach(() => {
// create teleport target
const el = document.createElement('div');
el.id = 'menu-outlet';
document.body.appendChild(el);
});
const global: GlobalMountOptions = {
directives: {
'click-outside': directive as any,
},
components: {
TransitionBounce,
},
};
test('Mount component', () => {
expect(VMenu).toBeTruthy();
const wrapper = mount(VMenu, {
slots: {
default: 'Slot Content',
},
global,
});
expect(wrapper.html()).toMatchSnapshot();
});

View File

@@ -0,0 +1,124 @@
import { mount } from '@vue/test-utils';
import { GlobalMountOptions } from '@vue/test-utils/dist/types';
import { beforeEach, expect, test, vi } from 'vitest';
import { directive } from '@/directives/click-outside';
import TransitionBounce from './transition/bounce.vue';
import VMenu from './v-menu.vue';
beforeEach(() => {
// create teleport target
const el = document.createElement('div');
el.id = 'menu-outlet';
document.body.appendChild(el);
// mocking this as it seems like there's observer undefined error in happy-dom
// but it is not crucial for the current test cases at the moment
vi.spyOn(MutationObserver.prototype, 'disconnect').mockResolvedValue();
});
const global: GlobalMountOptions = {
directives: {
'click-outside': directive as any,
},
components: {
TransitionBounce,
},
};
test('Mount component', () => {
expect(VMenu).toBeTruthy();
const wrapper = mount(VMenu, {
slots: {
default: 'Slot Content',
},
global,
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should not have click event listener when trigger is not "click"', () => {
const wrapper = mount(VMenu, {
global,
});
const vMenuListeners = (wrapper.find('.v-menu').element as any)._vei;
expect(vMenuListeners).toBeUndefined();
});
test('should have click event listener when trigger is "click"', () => {
const wrapper = mount(VMenu, {
props: {
trigger: 'click',
},
global,
});
const vMenuListeners = (wrapper.find('.v-menu').element as any)._vei;
expect(vMenuListeners).toHaveProperty('onClick');
});
test('should not have click event listener when closeOnContentClick prop is false', () => {
const wrapper = mount(VMenu, {
props: {
modelValue: true, // make it open in the beginning to ensure '.v-menu-content' is in the dom
closeOnContentClick: false,
},
global,
});
const vMenuContentListeners = (wrapper.getComponent(TransitionBounce).find('.v-menu-content').element as any)._vei;
expect(vMenuContentListeners).toBeUndefined();
});
test('should have click event listener when closeOnContentClick prop is true', () => {
const wrapper = mount(VMenu, {
props: {
modelValue: true, // make it open in the beginning to ensure '.v-menu-content' is in the dom
closeOnContentClick: true,
},
global,
});
const vMenuContentListeners = (wrapper.getComponent(TransitionBounce).find('.v-menu-content').element as any)._vei;
expect(vMenuContentListeners).toHaveProperty('onClick');
});
test('should not have pointerenter and pointerleave event listener when trigger is not "hover"', () => {
const wrapper = mount(VMenu, {
props: {
modelValue: true, // make it open in the beginning to ensure '.v-menu-content' is in the dom
},
global,
});
const activatorListeners = (wrapper.find({ ref: 'activator' }).element as any)._vei;
expect(activatorListeners).toBeUndefined();
expect(activatorListeners).toBeUndefined();
// we need to use getComponent because it's teleported
const vMenuContentListeners = (wrapper.getComponent(TransitionBounce).find('.v-menu-content').element as any)._vei;
expect(vMenuContentListeners).not.toHaveProperty('onPointerenter');
expect(vMenuContentListeners).not.toHaveProperty('onPointerleave');
});
test('should have pointerenter and pointerleave event listener when trigger is "hover"', () => {
const wrapper = mount(VMenu, {
props: {
modelValue: true, // make it open in the beginning to ensure '.v-menu-content' is in the dom
trigger: 'hover',
},
global,
});
const activatorListeners = (wrapper.find({ ref: 'activator' }).element as any)._vei;
expect(activatorListeners).toHaveProperty('onPointerenter');
expect(activatorListeners).toHaveProperty('onPointerleave');
// we need to use getComponent because it's teleported
const vMenuContentListeners = (wrapper.getComponent(TransitionBounce).find('.v-menu-content').element as any)._vei;
expect(vMenuContentListeners).toHaveProperty('onPointerenter');
expect(vMenuContentListeners).toHaveProperty('onPointerleave');
});

View File

@@ -1,11 +1,10 @@
<template>
<div class="v-menu" @click="onClick">
<div ref="v-menu" class="v-menu" v-on="trigger === 'click' ? { click: onClick } : {}">
<div
ref="activator"
class="v-menu-activator"
:class="{ attached }"
@pointerenter.stop="onPointerEnter"
@pointerleave.stop="onPointerLeave"
v-on="trigger === 'hover' ? { pointerenter: onPointerEnter, pointerleave: onPointerLeave } : {}"
>
<slot
name="activator"
@@ -39,9 +38,10 @@
<div
class="v-menu-content"
:class="{ 'full-height': fullHeight, seamless }"
@click.stop="onContentClick"
@pointerenter.stop="onPointerEnter"
@pointerleave.stop="onPointerLeave"
v-on="{
...(closeOnContentClick ? { click: onContentClick } : {}),
...(trigger === 'hover' ? { pointerenter: onPointerEnter, pointerleave: onPointerLeave } : {}),
}"
>
<slot
v-bind="{
@@ -240,7 +240,8 @@ function onClickOutsideMiddleware(e: Event) {
}
function onContentClick(e: Event) {
if (props.closeOnContentClick === true && e.target !== e.currentTarget) {
e.stopPropagation();
if (e.target !== e.currentTarget) {
deactivate();
}
}
@@ -262,18 +263,16 @@ function useEvents() {
return { onClick, onPointerLeave, onPointerEnter };
function onClick() {
if (props.trigger !== 'click') return;
toggle();
}
function onPointerEnter() {
if (props.trigger !== 'hover') return;
function onPointerEnter(event: Event) {
event.stopPropagation();
isHovered.value = true;
}
function onPointerLeave() {
if (props.trigger !== 'hover') return;
function onPointerLeave(event: Event) {
event.stopPropagation();
isHovered.value = false;
}
}