diff --git a/src/useRootClose.ts b/src/useRootClose.ts index 1d5b4ad5..66fc19b6 100644 --- a/src/useRootClose.ts +++ b/src/useRootClose.ts @@ -66,7 +66,7 @@ function useRootClose( !currentTarget || isModifiedEvent(e) || !isLeftClickEvent(e) || - !!contains(currentTarget, e.target); + !!contains(currentTarget, e.composedPath?.()[0] ?? e.target); }, [ref], ); diff --git a/test/useRootCloseSpec.js b/test/useRootCloseSpec.js index 3b96b201..c8883bd6 100644 --- a/test/useRootCloseSpec.js +++ b/test/useRootCloseSpec.js @@ -7,190 +7,220 @@ import { mount } from 'enzyme'; import useRootClose from '../src/useRootClose'; const escapeKeyCode = 27; +const configs = [ + { + description: '', + useShadowRoot: false, + }, + { + description: 'with shadow root', + useShadowRoot: true, + }, +]; +// Wrap simulant's created event to add composed: true, which is the default +// for most events. +const fire = (node, event, params) => { + const simulatedEvent = simulant(event, params); + const fixedEvent = new simulatedEvent.constructor(simulatedEvent.type, { + bubbles: simulatedEvent.bubbles, + button: simulatedEvent.button, + cancelable: simulatedEvent.cancelable, + composed: true, + }); + fixedEvent.keyCode = simulatedEvent.keyCode; + node.dispatchEvent(fixedEvent); + return fixedEvent; +}; + +configs.map((config) => + // eslint-disable-next-line mocha/no-setup-in-describe + describe(`useRootClose ${config.description}`, () => { + let attachTo, renderRoot, myDiv; + + beforeEach(() => { + renderRoot = document.createElement('div'); + if (config.useShadowRoot) { + renderRoot.attachShadow({ mode: 'open' }); + } + document.body.appendChild(renderRoot); + attachTo = config.useShadowRoot ? renderRoot.shadowRoot : renderRoot; + myDiv = () => attachTo.querySelector('#my-div'); + }); -describe('useRootClose', () => { - let attachTo; + afterEach(() => { + ReactDOM.unmountComponentAtNode(renderRoot); + document.body.removeChild(renderRoot); + }); - beforeEach(() => { - attachTo = document.createElement('div'); - document.body.appendChild(attachTo); - }); + describe('using default event', () => { + // eslint-disable-next-line mocha/no-setup-in-describe + shouldCloseOn(undefined, 'click'); + }); - afterEach(() => { - ReactDOM.unmountComponentAtNode(attachTo); - document.body.removeChild(attachTo); - }); + describe('using click event', () => { + // eslint-disable-next-line mocha/no-setup-in-describe + shouldCloseOn('click', 'click'); + }); - describe('using default event', () => { - // eslint-disable-next-line mocha/no-setup-in-describe - shouldCloseOn(undefined, 'click'); - }); + describe('using mousedown event', () => { + // eslint-disable-next-line mocha/no-setup-in-describe + shouldCloseOn('mousedown', 'mousedown'); + }); - describe('using click event', () => { - // eslint-disable-next-line mocha/no-setup-in-describe - shouldCloseOn('click', 'click'); - }); + function shouldCloseOn(clickTrigger, eventName) { + function Wrapper({ onRootClose, disabled }) { + const ref = useRef(); + useRootClose(ref, onRootClose, { + disabled, + clickTrigger, + }); - describe('using mousedown event', () => { - // eslint-disable-next-line mocha/no-setup-in-describe - shouldCloseOn('mousedown', 'mousedown'); - }); + return ( +
+ hello there +
+ ); + } + + it('should close when clicked outside', () => { + let spy = sinon.spy(); + + mount(, { attachTo }); - function shouldCloseOn(clickTrigger, eventName) { - function Wrapper({ onRootClose, disabled }) { - const ref = useRef(); - useRootClose(ref, onRootClose, { - disabled, - clickTrigger, + fire(myDiv(), eventName); + + expect(spy).to.not.have.been.called; + + fire(document.body, eventName); + + expect(spy).to.have.been.calledOnce; + + expect(spy.getCall(0).args[0].type).to.be.oneOf(['click', 'mousedown']); }); - return ( -
- hello there -
- ); - } + it('should not close when right-clicked outside', () => { + let spy = sinon.spy(); + mount(, { attachTo }); - it('should close when clicked outside', () => { - let spy = sinon.spy(); + fire(myDiv(), eventName, { button: 1 }); - mount(, { attachTo }); + expect(spy).to.not.have.been.called; - simulant.fire(document.getElementById('my-div'), eventName); + fire(document.body, eventName, { button: 1 }); - expect(spy).to.not.have.been.called; + expect(spy).to.not.have.been.called; + }); - simulant.fire(document.body, eventName); + it('should not close when disabled', () => { + let spy = sinon.spy(); + mount(, { attachTo }); - expect(spy).to.have.been.calledOnce; + fire(myDiv(), eventName); - expect(spy.getCall(0).args[0].type).to.be.oneOf(['click', 'mousedown']); - }); + expect(spy).to.not.have.been.called; - it('should not close when right-clicked outside', () => { - let spy = sinon.spy(); - mount(, { attachTo }); + fire(document.body, eventName); - simulant.fire(document.getElementById('my-div'), eventName, { - button: 1, + expect(spy).to.not.have.been.called; }); - expect(spy).to.not.have.been.called; + it('should close when inside another RootCloseWrapper', () => { + let outerSpy = sinon.spy(); + let innerSpy = sinon.spy(); - simulant.fire(document.body, eventName, { button: 1 }); + function Inner() { + const ref = useRef(); + useRootClose(ref, innerSpy, { clickTrigger }); - expect(spy).to.not.have.been.called; - }); + return ( +
+ hello there +
+ ); + } - it('should not close when disabled', () => { - let spy = sinon.spy(); - mount(, { attachTo }); + function Outer() { + const ref = useRef(); + useRootClose(ref, outerSpy, { clickTrigger }); - simulant.fire(document.getElementById('my-div'), eventName); + return ( +
+
hello there
+ +
+ ); + } - expect(spy).to.not.have.been.called; + mount(, { attachTo }); - simulant.fire(document.body, eventName); + fire(myDiv(), eventName); - expect(spy).to.not.have.been.called; - }); + expect(outerSpy).to.have.not.been.called; + expect(innerSpy).to.have.been.calledOnce; - it('should close when inside another RootCloseWrapper', () => { - let outerSpy = sinon.spy(); - let innerSpy = sinon.spy(); + expect(innerSpy.getCall(0).args[0].type).to.be.oneOf([ + 'click', + 'mousedown', + ]); + }); + } - function Inner() { + describe('using keyup event', () => { + function Wrapper({ children, onRootClose, event: clickTrigger }) { const ref = useRef(); - useRootClose(ref, innerSpy, { clickTrigger }); + useRootClose(ref, onRootClose, { clickTrigger }); return ( -
- hello there +
+ {children}
); } - function Outer() { - const ref = useRef(); - useRootClose(ref, outerSpy, { clickTrigger }); - - return ( -
+ it('should close when escape keyup', () => { + let spy = sinon.spy(); + mount( +
hello there
- -
+ , ); - } - - mount(, { attachTo }); - - simulant.fire(document.getElementById('my-div'), eventName); - expect(outerSpy).to.have.not.been.called; - expect(innerSpy).to.have.been.calledOnce; + expect(spy).to.not.have.been.called; - expect(innerSpy.getCall(0).args[0].type).to.be.oneOf([ - 'click', - 'mousedown', - ]); - }); - } - - describe('using keyup event', () => { - function Wrapper({ children, onRootClose, event: clickTrigger }) { - const ref = useRef(); - useRootClose(ref, onRootClose, { clickTrigger }); - - return ( -
- {children} -
- ); - } - - it('should close when escape keyup', () => { - let spy = sinon.spy(); - mount( - -
hello there
-
, - ); - - expect(spy).to.not.have.been.called; + fire(document.body, 'keyup', { keyCode: escapeKeyCode }); - simulant.fire(document.body, 'keyup', { keyCode: escapeKeyCode }); - - expect(spy).to.have.been.calledOnce; - - expect(spy.getCall(0).args.length).to.be.equal(1); - expect(spy.getCall(0).args[0].keyCode).to.be.equal(escapeKeyCode); - expect(spy.getCall(0).args[0].type).to.be.equal('keyup'); - }); + expect(spy).to.have.been.calledOnce; - it('should close when inside another RootCloseWrapper', () => { - let outerSpy = sinon.spy(); - let innerSpy = sinon.spy(); + expect(spy.getCall(0).args.length).to.be.equal(1); + expect(spy.getCall(0).args[0].keyCode).to.be.equal(escapeKeyCode); + expect(spy.getCall(0).args[0].type).to.be.equal('keyup'); + }); - mount( - -
-
hello there
- -
hello there
-
-
-
, - ); + it('should close when inside another RootCloseWrapper', () => { + let outerSpy = sinon.spy(); + let innerSpy = sinon.spy(); + + mount( + +
+
hello there
+ +
hello there
+
+
+
, + ); - simulant.fire(document.body, 'keyup', { keyCode: escapeKeyCode }); + fire(document.body, 'keyup', { keyCode: escapeKeyCode }); - // TODO: Update to match expectations. - // expect(outerSpy).to.have.not.been.called; - expect(innerSpy).to.have.been.calledOnce; + // TODO: Update to match expectations. + // expect(outerSpy).to.have.not.been.called; + expect(innerSpy).to.have.been.calledOnce; - expect(innerSpy.getCall(0).args.length).to.be.equal(1); - expect(innerSpy.getCall(0).args[0].keyCode).to.be.equal(escapeKeyCode); - expect(innerSpy.getCall(0).args[0].type).to.be.equal('keyup'); + expect(innerSpy.getCall(0).args.length).to.be.equal(1); + expect(innerSpy.getCall(0).args[0].keyCode).to.be.equal(escapeKeyCode); + expect(innerSpy.getCall(0).args[0].type).to.be.equal('keyup'); + }); }); - }); -}); + }), +);