Skip to content

Commit

Permalink
feat(useRootClose): add support for open shadow roots (#1004)
Browse files Browse the repository at this point in the history
* Support useRootClose inside shadow roots

* Fix lint errors in useRootClose.ts, useRootCloseSpec.js

* Ignore empty composedPath in useRootClose

Co-authored-by: Charles Roelli <charles.roelli@hey.com>
  • Loading branch information
charlesroelli and charlesroelli committed Jun 2, 2022
1 parent 23c27b3 commit 3b7fb53
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 141 deletions.
2 changes: 1 addition & 1 deletion src/useRootClose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function useRootClose(
!currentTarget ||
isModifiedEvent(e) ||
!isLeftClickEvent(e) ||
!!contains(currentTarget, e.target);
!!contains(currentTarget, e.composedPath?.()[0] ?? e.target);
},
[ref],
);
Expand Down
310 changes: 170 additions & 140 deletions test/useRootCloseSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div ref={ref} id="my-div">
hello there
</div>
);
}

it('should close when clicked outside', () => {
let spy = sinon.spy();

mount(<Wrapper onRootClose={spy} />, { 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 (
<div ref={ref} id="my-div">
hello there
</div>
);
}
it('should not close when right-clicked outside', () => {
let spy = sinon.spy();
mount(<Wrapper onRootClose={spy} />, { attachTo });

it('should close when clicked outside', () => {
let spy = sinon.spy();
fire(myDiv(), eventName, { button: 1 });

mount(<Wrapper onRootClose={spy} />, { 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(<Wrapper onRootClose={spy} disabled />, { 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(<Wrapper onRootClose={spy} />, { 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 (
<div ref={ref} id="my-other-div">
hello there
</div>
);
}

it('should not close when disabled', () => {
let spy = sinon.spy();
mount(<Wrapper onRootClose={spy} disabled />, { attachTo });
function Outer() {
const ref = useRef();
useRootClose(ref, outerSpy, { clickTrigger });

simulant.fire(document.getElementById('my-div'), eventName);
return (
<div ref={ref}>
<div id="my-div">hello there</div>
<Inner />
</div>
);
}

expect(spy).to.not.have.been.called;
mount(<Outer />, { 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 (
<div ref={ref} id="my-other-div">
hello there
<div ref={ref} id="my-div">
{children}
</div>
);
}

function Outer() {
const ref = useRef();
useRootClose(ref, outerSpy, { clickTrigger });

return (
<div ref={ref}>
it('should close when escape keyup', () => {
let spy = sinon.spy();
mount(
<Wrapper onRootClose={spy}>
<div id="my-div">hello there</div>
<Inner />
</div>
</Wrapper>,
);
}

mount(<Outer />, { 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 (
<div ref={ref} id="my-div">
{children}
</div>
);
}

it('should close when escape keyup', () => {
let spy = sinon.spy();
mount(
<Wrapper onRootClose={spy}>
<div id="my-div">hello there</div>
</Wrapper>,
);

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(
<Wrapper onRootClose={outerSpy}>
<div>
<div id="my-div">hello there</div>
<Wrapper onRootClose={innerSpy}>
<div id="my-other-div">hello there</div>
</Wrapper>
</div>
</Wrapper>,
);
it('should close when inside another RootCloseWrapper', () => {
let outerSpy = sinon.spy();
let innerSpy = sinon.spy();

mount(
<Wrapper onRootClose={outerSpy}>
<div>
<div id="my-div">hello there</div>
<Wrapper onRootClose={innerSpy}>
<div id="my-other-div">hello there</div>
</Wrapper>
</div>
</Wrapper>,
);

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');
});
});
});
});
}),
);

0 comments on commit 3b7fb53

Please sign in to comment.