diff --git a/packages/directives/__tests__/click-outside.spec.ts b/packages/directives/__tests__/click-outside.spec.ts index 7b9c80982d..c7247d47f3 100644 --- a/packages/directives/__tests__/click-outside.spec.ts +++ b/packages/directives/__tests__/click-outside.spec.ts @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils' +import { withDirectives, h } from 'vue' import ClickOutside from '../click-outside' const AXIOM = 'Rem is the best girl' @@ -35,6 +36,16 @@ const _mount = () => mount(Component, { }, }) +const triggerDocumentClickEvent = () => { + const mousedown = document.createEvent('MouseEvents') + mousedown.initMouseEvent('mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null) + document.dispatchEvent(mousedown) + + const mouseup = document.createEvent('MouseEvents') + mouseup.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null) + document.dispatchEvent(mouseup) +} + describe('Directives.vue', () => { beforeEach(() => { // clear the previously assigned event object @@ -57,13 +68,7 @@ describe('Directives.vue', () => { await wrapper.find(`.${TRIGGER}`).trigger('mouseup') expect(handler).toHaveBeenCalledTimes(0) - const mousedown = document.createEvent('MouseEvents') - mousedown.initMouseEvent('mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null) - document.dispatchEvent(mousedown) - - const mouseup = document.createEvent('MouseEvents') - mouseup.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null) - document.dispatchEvent(mouseup) + triggerDocumentClickEvent() // here is the different part // we test the existence of the local variable. @@ -71,3 +76,51 @@ describe('Directives.vue', () => { expect(mouseupObject).toBeDefined() }) }) + +describe('Multiple click-outside directives', () => { + const firstHandler = jest.fn() + const InnerComponent = { + template: `
${AXIOM}
`, + setup() { + return { handler: firstHandler } + }, + } + + const secondHandler = jest.fn() + const OuterComponent = { + setup() { + return () => { + const triggerNode = withDirectives(h(InnerComponent), [[ClickOutside, secondHandler]]) + return h('div', { class: OTHER_CLASS }, triggerNode) + } + }, + } + + + it('should support for multiple directives', async () => { + const wrapper = mount(OuterComponent, { + global: { + directives: { + ClickOutside, + }, + }, + }) + + // click inside trigger element + await wrapper.find(`.${TRIGGER}`).trigger('mousedown') + await wrapper.find(`.${TRIGGER}`).trigger('mouseup') + expect(firstHandler).toHaveBeenCalledTimes(0) + expect(secondHandler).toHaveBeenCalledTimes(0) + + // click outside trigger element + triggerDocumentClickEvent() + expect(firstHandler).toHaveBeenCalledTimes(1) + expect(secondHandler).toHaveBeenCalledTimes(1) + + // update the component instance + wrapper.vm.$forceUpdate() + triggerDocumentClickEvent() + expect(firstHandler).toHaveBeenCalledTimes(2) + expect(secondHandler).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/directives/click-outside/index.ts b/packages/directives/click-outside/index.ts index 9c3ec13d11..1bd293f68b 100644 --- a/packages/directives/click-outside/index.ts +++ b/packages/directives/click-outside/index.ts @@ -10,7 +10,7 @@ type FlushList = Map< { documentHandler: DocumentHandler bindingFn: (...args: unknown[]) => unknown - } + }[] >; const nodeList: FlushList = new Map() @@ -20,8 +20,10 @@ let startClick: MouseEvent if (!isServer) { on(document, 'mousedown', (e: MouseEvent) => (startClick = e)) on(document, 'mouseup', (e: MouseEvent) => { - for (const { documentHandler } of nodeList.values()) { - documentHandler(e, startClick) + for (const handlers of nodeList.values()) { + for (const { documentHandler } of handlers) { + documentHandler(e, startClick) + } } }) } @@ -33,7 +35,7 @@ function createDocumentHandler( let excludes: HTMLElement[] = [] if (Array.isArray(binding.arg)) { excludes = binding.arg - } else { + } else if (binding.arg as unknown instanceof HTMLElement) { // due to current implementation on binding type is wrong the type casting is necessary here excludes.push(binding.arg as unknown as HTMLElement) } @@ -77,18 +79,37 @@ function createDocumentHandler( const ClickOutside: ObjectDirective = { beforeMount(el, binding) { - nodeList.set(el, { + // there could be multiple handlers on the element + if (!nodeList.has(el)) { + nodeList.set(el, []) + } + + nodeList.get(el).push({ documentHandler: createDocumentHandler(el, binding), bindingFn: binding.value, }) }, updated(el, binding) { - nodeList.set(el, { + if (!nodeList.has(el)) { + nodeList.set(el, []) + } + + const handlers = nodeList.get(el) + const oldHandlerIndex = handlers.findIndex(item => (item.bindingFn === binding.oldValue)) + const newHandler = { documentHandler: createDocumentHandler(el, binding), bindingFn: binding.value, - }) + } + + if (oldHandlerIndex >= 0) { + // replace the old handler to the new handler + handlers.splice(oldHandlerIndex, 1, newHandler) + } else { + handlers.push(newHandler) + } }, unmounted(el) { + // remove all listeners when a component unmounted nodeList.delete(el) }, }