diff --git a/packages/directives/click-outside/index.ts b/packages/directives/click-outside/index.ts index 575a927576..a26339aeb3 100644 --- a/packages/directives/click-outside/index.ts +++ b/packages/directives/click-outside/index.ts @@ -3,7 +3,7 @@ import { on } from '@element-plus/utils/dom' import type { DirectiveBinding, ObjectDirective, ComponentPublicInstance } from 'vue' -type DocumentHandler = (mouseup: T, mousedown: T) => void; +type DocumentHandler = (mouseup: T, mousedown: T) => void; type FlushList = Map< HTMLElement, @@ -15,11 +15,11 @@ type FlushList = Map< const nodeList: FlushList = new Map() -let startClick: Event +let startClick: MouseEvent if (!isServer) { - on(document, 'mousedown', e => (startClick = e)) - on(document, 'mouseup', e => { + on(document, 'mousedown', (e: MouseEvent) => (startClick = e)) + on(document, 'mouseup', (e: MouseEvent) => { for (const { documentHandler } of nodeList.values()) { documentHandler(e, startClick) } @@ -30,21 +30,44 @@ function createDocumentHandler( el: HTMLElement, binding: DirectiveBinding, ): DocumentHandler { + let excludes: HTMLElement[] = [] + if (Array.isArray(binding.arg)) { + excludes = binding.arg + } else { + // due to current implementation on binding type is wrong the type casting is necessary here + excludes.push(binding.arg as unknown as HTMLElement) + } return function(mouseup, mousedown) { const popperRef = (binding.instance as ComponentPublicInstance<{ popperRef: Nullable }>).popperRef + const mouseUpTarget = mouseup.target as Node + const mouseDownTarget = mousedown.target as Node + const isBound = !binding || !binding.instance + const isTargetExists = !mouseUpTarget || !mouseDownTarget + const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget) + const isSelf = el === mouseUpTarget + + const isTargetExcluded = + ( excludes.length && + excludes.some(item => item?.contains(mouseUpTarget)) + ) || ( + excludes.length && excludes.includes(mouseDownTarget as HTMLElement) + ) + const isContainedByPopper = ( + popperRef && + ( + popperRef.contains(mouseUpTarget) || + popperRef.contains(mouseDownTarget) + ) + ) if ( - !binding || - !binding.instance || - !mouseup.target || - !mousedown.target || - el.contains(mouseup.target as Node) || - el.contains(mousedown.target as Node) || - el === mouseup.target || - (popperRef && - (popperRef.contains(mouseup.target as Node) || - popperRef.contains(mousedown.target as Node))) + isBound || + isTargetExists || + isContainedByEl || + isSelf || + isTargetExcluded || + isContainedByPopper ) { return } diff --git a/packages/hooks/use-events/index.ts b/packages/hooks/use-events/index.ts index b9e416175f..c9cc28cf4a 100644 --- a/packages/hooks/use-events/index.ts +++ b/packages/hooks/use-events/index.ts @@ -3,21 +3,22 @@ import { on, off } from '@element-plus/utils/dom' import type { Ref } from 'vue' -type Events = Array<{ +export type Event = { name: string handler: (...args: any[]) => any -}> +} -export default (el: Ref, events: Events) => { - events.map(({ name, handler }) => { - on(el.value, name, handler) - }) +export default (el: Ref, events: Event[]) => { - watch(el, (_, __, onCleanup) => { - onCleanup(() => { + watch(el, val => { + if (val) { + events.map(({ name, handler }) => { + on(el.value, name, handler) + }) + } else { events.map(({ name, handler }) => { off(el.value, name, handler) }) - }) + } }) } diff --git a/packages/popper/__tests__/popper.spec.ts b/packages/popper/__tests__/popper.spec.ts index 5f52271577..282754dfb7 100644 --- a/packages/popper/__tests__/popper.spec.ts +++ b/packages/popper/__tests__/popper.spec.ts @@ -17,6 +17,9 @@ const selector = '[role="tooltip"]' const TEST_TRIGGER = 'test-trigger' const MOUSE_ENTER_EVENT = 'mouseenter' const MOUSE_LEAVE_EVENT = 'mouseleave' +const CLICK_EVENT = 'click' +const FOCUS_EVENT = 'focus' +const BLUR_EVENT = 'blur' const DISPLAY_NONE = 'display: none' const Wrapped = (props: UnKnownProps, { slots }) => h('div', h(ElPopper, props, slots)) @@ -81,7 +84,6 @@ describe('Popper.vue', () => { test('append to body', () => { let wrapper = _mount() - const selector = '[role="tooltip"]' expect(wrapper.find(selector).exists()).toBe(false) /** * Current layout of `ElPopper` @@ -199,4 +201,113 @@ describe('Popper.vue', () => { // the only way to test this is by providing an error handler to catch it expect(errorHandler).toHaveBeenCalledTimes(1) }) + describe('trigger', () => { + + test('should work with click trigger', async () => { + const wrapper = _mount({ + trigger: ['click'], + appendToBody: false, + }) + await nextTick() + + const trigger = wrapper.find(`.${TEST_TRIGGER}`) + const popper = wrapper.findComponent(ElPopper) + expect(popper.vm.visible).toBe(false) + // for now triggering event on element via DOMWrapper is not available so we need to apply + // old way + await trigger.trigger(CLICK_EVENT) + + expect(popper.vm.visible).toBe(true) + + await trigger.trigger(MOUSE_ENTER_EVENT) + expect(popper.vm.visible).toBe(true) + await trigger.trigger(FOCUS_EVENT) + expect(popper.vm.visible).toBe(true) + }) + + test('should work with hover trigger', async () => { + const wrapper = _mount({ + trigger: ['hover'], + appendToBody: false, + }) + await nextTick() + + const trigger = wrapper.find(`.${TEST_TRIGGER}`) + const popper = wrapper.findComponent(ElPopper) + expect(popper.vm.visible).toBe(false) + // for now triggering event on element via DOMWrapper is not available so we need to apply + // old way + await trigger.trigger(MOUSE_ENTER_EVENT) + + expect(popper.vm.visible).toBe(true) + + await trigger.trigger(MOUSE_LEAVE_EVENT) + expect(popper.vm.visible).toBe(false) + + await trigger.trigger(FOCUS_EVENT) + expect(popper.vm.visible).toBe(false) + + await trigger.trigger(CLICK_EVENT) + expect(popper.vm.visible).toBe(false) + }) + + test('should work with focus trigger', async () => { + const wrapper = _mount({ + trigger: [FOCUS_EVENT], + appendToBody: false, + }) + await nextTick() + + + const trigger = wrapper.find(`.${TEST_TRIGGER}`) + const popper = wrapper.findComponent(ElPopper) + expect(popper.vm.visible).toBe(false) + // for now triggering event on element via DOMWrapper is not available so we need to apply + // old way + await trigger.trigger(FOCUS_EVENT) + + expect(popper.vm.visible).toBe(true) + + await trigger.trigger(BLUR_EVENT) + expect(popper.vm.visible).toBe(false) + + await trigger.trigger(MOUSE_ENTER_EVENT) + expect(popper.vm.visible).toBe(false) + + await trigger.trigger(CLICK_EVENT) + expect(popper.vm.visible).toBe(false) + }) + + test('combined trigger', async () => { + const wrapper = _mount({ + trigger: [FOCUS_EVENT, CLICK_EVENT, 'hover'], + appendToBody: false, + }) + await nextTick() + + const trigger = wrapper.find(`.${TEST_TRIGGER}`) + const popper = wrapper.findComponent(ElPopper) + expect(popper.vm.visible).toBe(false) + // for now triggering event on element via DOMWrapper is not available so we need to apply + // old way + await trigger.trigger(CLICK_EVENT) + + expect(popper.vm.visible).toBe(true) + + await trigger.trigger(BLUR_EVENT) + expect(popper.vm.visible).toBe(false) + + await trigger.trigger(MOUSE_ENTER_EVENT) + expect(popper.vm.visible).toBe(true) + + await trigger.trigger(CLICK_EVENT) + expect(popper.vm.visible).toBe(false) + + await trigger.trigger(FOCUS_EVENT) + expect(popper.vm.visible).toBe(true) + + await trigger.trigger(CLICK_EVENT) + expect(popper.vm.visible).toBe(false) + }) + }) }) diff --git a/packages/popper/src/index.vue b/packages/popper/src/index.vue index 0fe9922dc3..c8fb8d465a 100644 --- a/packages/popper/src/index.vue +++ b/packages/popper/src/index.vue @@ -153,31 +153,7 @@ export default defineComponent({ // this is a reference that we need to pass down to child component // to obtain the child instance - const { - arrowRef, - clickMask, - doDestroy, - onShow, - onHide, - popperInstance, - popperId, - popperRef, - initializePopper, - visible, - } = usePopper(props as IPopperOptions, ctx) - - return { - arrowRef, - clickMask, - popperId, - doDestroy, - onShow, - onHide, - popperRef, - popperInstance, - initializePopper, - visible, - } + return usePopper(props as IPopperOptions) }, deactivated() { this.doDestroy() @@ -186,6 +162,7 @@ export default defineComponent({ this.initializePopper() }, render() { + const { $slots } = this const arrow = this.showArrow ? h( 'div', @@ -225,7 +202,7 @@ export default defineComponent({ onClick: stop, }, [ - this.$slots.default ? this.$slots.default() : this.content, + ($slots.default?.()) || this.content, arrow, ], ), @@ -235,11 +212,13 @@ export default defineComponent({ ), }, ) + + const _t = $slots.trigger?.() return h( Fragment, null, [ - this.$slots.trigger?.(), + _t, this.appendToBody ? h( Teleport, @@ -251,11 +230,10 @@ export default defineComponent({ 'div', { class: 'el-popper__mask', - onClick: this.clickMask, }, popper, ), - [[ClickOutside, () => this.$emit(UPDATE_VALUE_EVENT, false)]], + [[ClickOutside, this.onHide, [this.excludes] as any]], ), ) : popper, @@ -268,16 +246,12 @@ export default defineComponent({