Skip to content

Commit 3b7fb53

Browse files
charlesroelliCharles Roelli
and
Charles Roelli
authored
feat(useRootClose): add support for open shadow roots (#1004)
* Support useRootClose inside shadow roots * Fix lint errors in useRootClose.ts, useRootCloseSpec.js * Ignore empty composedPath in useRootClose Co-authored-by: Charles Roelli <[email protected]>
1 parent 23c27b3 commit 3b7fb53

File tree

2 files changed

+171
-141
lines changed

2 files changed

+171
-141
lines changed

src/useRootClose.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ function useRootClose(
6666
!currentTarget ||
6767
isModifiedEvent(e) ||
6868
!isLeftClickEvent(e) ||
69-
!!contains(currentTarget, e.target);
69+
!!contains(currentTarget, e.composedPath?.()[0] ?? e.target);
7070
},
7171
[ref],
7272
);

test/useRootCloseSpec.js

+170-140
Original file line numberDiff line numberDiff line change
@@ -7,190 +7,220 @@ import { mount } from 'enzyme';
77
import useRootClose from '../src/useRootClose';
88

99
const escapeKeyCode = 27;
10+
const configs = [
11+
{
12+
description: '',
13+
useShadowRoot: false,
14+
},
15+
{
16+
description: 'with shadow root',
17+
useShadowRoot: true,
18+
},
19+
];
20+
// Wrap simulant's created event to add composed: true, which is the default
21+
// for most events.
22+
const fire = (node, event, params) => {
23+
const simulatedEvent = simulant(event, params);
24+
const fixedEvent = new simulatedEvent.constructor(simulatedEvent.type, {
25+
bubbles: simulatedEvent.bubbles,
26+
button: simulatedEvent.button,
27+
cancelable: simulatedEvent.cancelable,
28+
composed: true,
29+
});
30+
fixedEvent.keyCode = simulatedEvent.keyCode;
31+
node.dispatchEvent(fixedEvent);
32+
return fixedEvent;
33+
};
34+
35+
configs.map((config) =>
36+
// eslint-disable-next-line mocha/no-setup-in-describe
37+
describe(`useRootClose ${config.description}`, () => {
38+
let attachTo, renderRoot, myDiv;
39+
40+
beforeEach(() => {
41+
renderRoot = document.createElement('div');
42+
if (config.useShadowRoot) {
43+
renderRoot.attachShadow({ mode: 'open' });
44+
}
45+
document.body.appendChild(renderRoot);
46+
attachTo = config.useShadowRoot ? renderRoot.shadowRoot : renderRoot;
47+
myDiv = () => attachTo.querySelector('#my-div');
48+
});
1049

11-
describe('useRootClose', () => {
12-
let attachTo;
50+
afterEach(() => {
51+
ReactDOM.unmountComponentAtNode(renderRoot);
52+
document.body.removeChild(renderRoot);
53+
});
1354

14-
beforeEach(() => {
15-
attachTo = document.createElement('div');
16-
document.body.appendChild(attachTo);
17-
});
55+
describe('using default event', () => {
56+
// eslint-disable-next-line mocha/no-setup-in-describe
57+
shouldCloseOn(undefined, 'click');
58+
});
1859

19-
afterEach(() => {
20-
ReactDOM.unmountComponentAtNode(attachTo);
21-
document.body.removeChild(attachTo);
22-
});
60+
describe('using click event', () => {
61+
// eslint-disable-next-line mocha/no-setup-in-describe
62+
shouldCloseOn('click', 'click');
63+
});
2364

24-
describe('using default event', () => {
25-
// eslint-disable-next-line mocha/no-setup-in-describe
26-
shouldCloseOn(undefined, 'click');
27-
});
65+
describe('using mousedown event', () => {
66+
// eslint-disable-next-line mocha/no-setup-in-describe
67+
shouldCloseOn('mousedown', 'mousedown');
68+
});
2869

29-
describe('using click event', () => {
30-
// eslint-disable-next-line mocha/no-setup-in-describe
31-
shouldCloseOn('click', 'click');
32-
});
70+
function shouldCloseOn(clickTrigger, eventName) {
71+
function Wrapper({ onRootClose, disabled }) {
72+
const ref = useRef();
73+
useRootClose(ref, onRootClose, {
74+
disabled,
75+
clickTrigger,
76+
});
3377

34-
describe('using mousedown event', () => {
35-
// eslint-disable-next-line mocha/no-setup-in-describe
36-
shouldCloseOn('mousedown', 'mousedown');
37-
});
78+
return (
79+
<div ref={ref} id="my-div">
80+
hello there
81+
</div>
82+
);
83+
}
84+
85+
it('should close when clicked outside', () => {
86+
let spy = sinon.spy();
87+
88+
mount(<Wrapper onRootClose={spy} />, { attachTo });
3889

39-
function shouldCloseOn(clickTrigger, eventName) {
40-
function Wrapper({ onRootClose, disabled }) {
41-
const ref = useRef();
42-
useRootClose(ref, onRootClose, {
43-
disabled,
44-
clickTrigger,
90+
fire(myDiv(), eventName);
91+
92+
expect(spy).to.not.have.been.called;
93+
94+
fire(document.body, eventName);
95+
96+
expect(spy).to.have.been.calledOnce;
97+
98+
expect(spy.getCall(0).args[0].type).to.be.oneOf(['click', 'mousedown']);
4599
});
46100

47-
return (
48-
<div ref={ref} id="my-div">
49-
hello there
50-
</div>
51-
);
52-
}
101+
it('should not close when right-clicked outside', () => {
102+
let spy = sinon.spy();
103+
mount(<Wrapper onRootClose={spy} />, { attachTo });
53104

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

57-
mount(<Wrapper onRootClose={spy} />, { attachTo });
107+
expect(spy).to.not.have.been.called;
58108

59-
simulant.fire(document.getElementById('my-div'), eventName);
109+
fire(document.body, eventName, { button: 1 });
60110

61-
expect(spy).to.not.have.been.called;
111+
expect(spy).to.not.have.been.called;
112+
});
62113

63-
simulant.fire(document.body, eventName);
114+
it('should not close when disabled', () => {
115+
let spy = sinon.spy();
116+
mount(<Wrapper onRootClose={spy} disabled />, { attachTo });
64117

65-
expect(spy).to.have.been.calledOnce;
118+
fire(myDiv(), eventName);
66119

67-
expect(spy.getCall(0).args[0].type).to.be.oneOf(['click', 'mousedown']);
68-
});
120+
expect(spy).to.not.have.been.called;
69121

70-
it('should not close when right-clicked outside', () => {
71-
let spy = sinon.spy();
72-
mount(<Wrapper onRootClose={spy} />, { attachTo });
122+
fire(document.body, eventName);
73123

74-
simulant.fire(document.getElementById('my-div'), eventName, {
75-
button: 1,
124+
expect(spy).to.not.have.been.called;
76125
});
77126

78-
expect(spy).to.not.have.been.called;
127+
it('should close when inside another RootCloseWrapper', () => {
128+
let outerSpy = sinon.spy();
129+
let innerSpy = sinon.spy();
79130

80-
simulant.fire(document.body, eventName, { button: 1 });
131+
function Inner() {
132+
const ref = useRef();
133+
useRootClose(ref, innerSpy, { clickTrigger });
81134

82-
expect(spy).to.not.have.been.called;
83-
});
135+
return (
136+
<div ref={ref} id="my-other-div">
137+
hello there
138+
</div>
139+
);
140+
}
84141

85-
it('should not close when disabled', () => {
86-
let spy = sinon.spy();
87-
mount(<Wrapper onRootClose={spy} disabled />, { attachTo });
142+
function Outer() {
143+
const ref = useRef();
144+
useRootClose(ref, outerSpy, { clickTrigger });
88145

89-
simulant.fire(document.getElementById('my-div'), eventName);
146+
return (
147+
<div ref={ref}>
148+
<div id="my-div">hello there</div>
149+
<Inner />
150+
</div>
151+
);
152+
}
90153

91-
expect(spy).to.not.have.been.called;
154+
mount(<Outer />, { attachTo });
92155

93-
simulant.fire(document.body, eventName);
156+
fire(myDiv(), eventName);
94157

95-
expect(spy).to.not.have.been.called;
96-
});
158+
expect(outerSpy).to.have.not.been.called;
159+
expect(innerSpy).to.have.been.calledOnce;
97160

98-
it('should close when inside another RootCloseWrapper', () => {
99-
let outerSpy = sinon.spy();
100-
let innerSpy = sinon.spy();
161+
expect(innerSpy.getCall(0).args[0].type).to.be.oneOf([
162+
'click',
163+
'mousedown',
164+
]);
165+
});
166+
}
101167

102-
function Inner() {
168+
describe('using keyup event', () => {
169+
function Wrapper({ children, onRootClose, event: clickTrigger }) {
103170
const ref = useRef();
104-
useRootClose(ref, innerSpy, { clickTrigger });
171+
useRootClose(ref, onRootClose, { clickTrigger });
105172

106173
return (
107-
<div ref={ref} id="my-other-div">
108-
hello there
174+
<div ref={ref} id="my-div">
175+
{children}
109176
</div>
110177
);
111178
}
112179

113-
function Outer() {
114-
const ref = useRef();
115-
useRootClose(ref, outerSpy, { clickTrigger });
116-
117-
return (
118-
<div ref={ref}>
180+
it('should close when escape keyup', () => {
181+
let spy = sinon.spy();
182+
mount(
183+
<Wrapper onRootClose={spy}>
119184
<div id="my-div">hello there</div>
120-
<Inner />
121-
</div>
185+
</Wrapper>,
122186
);
123-
}
124-
125-
mount(<Outer />, { attachTo });
126-
127-
simulant.fire(document.getElementById('my-div'), eventName);
128187

129-
expect(outerSpy).to.have.not.been.called;
130-
expect(innerSpy).to.have.been.calledOnce;
188+
expect(spy).to.not.have.been.called;
131189

132-
expect(innerSpy.getCall(0).args[0].type).to.be.oneOf([
133-
'click',
134-
'mousedown',
135-
]);
136-
});
137-
}
138-
139-
describe('using keyup event', () => {
140-
function Wrapper({ children, onRootClose, event: clickTrigger }) {
141-
const ref = useRef();
142-
useRootClose(ref, onRootClose, { clickTrigger });
143-
144-
return (
145-
<div ref={ref} id="my-div">
146-
{children}
147-
</div>
148-
);
149-
}
150-
151-
it('should close when escape keyup', () => {
152-
let spy = sinon.spy();
153-
mount(
154-
<Wrapper onRootClose={spy}>
155-
<div id="my-div">hello there</div>
156-
</Wrapper>,
157-
);
158-
159-
expect(spy).to.not.have.been.called;
190+
fire(document.body, 'keyup', { keyCode: escapeKeyCode });
160191

161-
simulant.fire(document.body, 'keyup', { keyCode: escapeKeyCode });
162-
163-
expect(spy).to.have.been.calledOnce;
164-
165-
expect(spy.getCall(0).args.length).to.be.equal(1);
166-
expect(spy.getCall(0).args[0].keyCode).to.be.equal(escapeKeyCode);
167-
expect(spy.getCall(0).args[0].type).to.be.equal('keyup');
168-
});
192+
expect(spy).to.have.been.calledOnce;
169193

170-
it('should close when inside another RootCloseWrapper', () => {
171-
let outerSpy = sinon.spy();
172-
let innerSpy = sinon.spy();
194+
expect(spy.getCall(0).args.length).to.be.equal(1);
195+
expect(spy.getCall(0).args[0].keyCode).to.be.equal(escapeKeyCode);
196+
expect(spy.getCall(0).args[0].type).to.be.equal('keyup');
197+
});
173198

174-
mount(
175-
<Wrapper onRootClose={outerSpy}>
176-
<div>
177-
<div id="my-div">hello there</div>
178-
<Wrapper onRootClose={innerSpy}>
179-
<div id="my-other-div">hello there</div>
180-
</Wrapper>
181-
</div>
182-
</Wrapper>,
183-
);
199+
it('should close when inside another RootCloseWrapper', () => {
200+
let outerSpy = sinon.spy();
201+
let innerSpy = sinon.spy();
202+
203+
mount(
204+
<Wrapper onRootClose={outerSpy}>
205+
<div>
206+
<div id="my-div">hello there</div>
207+
<Wrapper onRootClose={innerSpy}>
208+
<div id="my-other-div">hello there</div>
209+
</Wrapper>
210+
</div>
211+
</Wrapper>,
212+
);
184213

185-
simulant.fire(document.body, 'keyup', { keyCode: escapeKeyCode });
214+
fire(document.body, 'keyup', { keyCode: escapeKeyCode });
186215

187-
// TODO: Update to match expectations.
188-
// expect(outerSpy).to.have.not.been.called;
189-
expect(innerSpy).to.have.been.calledOnce;
216+
// TODO: Update to match expectations.
217+
// expect(outerSpy).to.have.not.been.called;
218+
expect(innerSpy).to.have.been.calledOnce;
190219

191-
expect(innerSpy.getCall(0).args.length).to.be.equal(1);
192-
expect(innerSpy.getCall(0).args[0].keyCode).to.be.equal(escapeKeyCode);
193-
expect(innerSpy.getCall(0).args[0].type).to.be.equal('keyup');
220+
expect(innerSpy.getCall(0).args.length).to.be.equal(1);
221+
expect(innerSpy.getCall(0).args[0].keyCode).to.be.equal(escapeKeyCode);
222+
expect(innerSpy.getCall(0).args[0].type).to.be.equal('keyup');
223+
});
194224
});
195-
});
196-
});
225+
}),
226+
);

0 commit comments

Comments
 (0)