Skip to content

Commit 659d44e

Browse files
authored
Add shadow DOM support with ft v6.8.1 (#652)
Adds a demo that uses open and closed shadows. Does NOT add a test for this demo because of lack of support for Shadow DOM in Cypress (see comment in focus-trap-demo.spec.js). Adds a warning to the README about testing in JSDom, and fixes all the JSDom-based tests to use `displayCheck='none'` to get around JSDom limitations with new APIs used by tabbable in v5.3.0.
1 parent c3ef438 commit 659d44e

10 files changed

+228
-41
lines changed

.changeset/olive-bears-relate.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'focus-trap-react': minor
3+
---
4+
5+
<<<<<<< HEAD
6+
Bumps focus-trap to v6.8.0. The big new feature is opt-in Shadow DOM support in focus-trap (in tabbable), and a new `getShadowRoot` tabbable option exposed in a new `focusTrapOptions.tabbableOptions` configuration option.
7+
=======
8+
Bumps focus-trap to v6.8.1. The big new feature is opt-in Shadow DOM support in focus-trap (in tabbable), and new tabbable options exposed in a new `focusTrapOptions.tabbableOptions` configuration option.
9+
>>>>>>> 57d9caa (Add shadow DOM support with ft v6.8.1)

README.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ ReactDOM.render(<Demo />, document.getElementById('root'));
141141

142142
Type: `Object`, optional
143143

144-
Pass any of the options available in [`focus-trap`'s `createOptions`](https://github.com/focus-trap/focus-trap#focustrap--createfocustrapelement-createoptions).
144+
Pass any of the options available in focus-trap's [createOptions](https://github.com/focus-trap/focus-trap#createoptions).
145+
146+
> ⚠️ See notes about __[testing in JSDom](#testing-in-jsdom)__ (e.g. using Jest) if that's what you currently use.
145147
146148
#### active
147149

@@ -169,6 +171,20 @@ If `containerElements` is subsequently updated (i.e. after the trap has been cre
169171

170172
Using `containerElements` does require the use of React refs which, by nature, will require at least one state update in order to get the resolved elements into the prop, resulting in at least one additional render. In the normal case, this is likely more than acceptable, but if you really want to optimize things, then you could consider [using focus-trap directly](https://codesandbox.io/s/focus-trapreact-containerelements-demos-v5ydi) (see `Trap2.js`).
171173

174+
## Help
175+
176+
### Testing in JSDom
177+
178+
> ⚠️ JSDom is not officially supported. Your mileage may vary, and tests may break from one release to the next (even a patch or minor release).
179+
>
180+
> This topic is just here to help with what we know may affect your tests.
181+
182+
In general, a focus trap is best tested in a full browser environment such as Cypress, Playwright, or Nightwatch where a full DOM is available.
183+
184+
Sometimes, that's not entirely desirable, and depending on what you're testing, you may be able to get away with using JSDom (e.g. via Jest), but you'll have to configure your traps using the `focusTrapOptions.tabbableOptions.displayCheck: 'none'` option.
185+
186+
See [Testing focus-trap in JSDom](https://github.com/focus-trap/focus-trap#testing-in-jsdom) for more details.
187+
172188
## Contributing
173189

174190
See [CONTRIBUTING](CONTRIBUTING.md).

cypress/integration/focus-trap-demo.spec.js

+12
Original file line numberDiff line numberDiff line change
@@ -375,4 +375,16 @@ describe('<FocusTrap> component', () => {
375375
});
376376
});
377377
});
378+
379+
// describe('demo: with-shadow-dom', () => {
380+
// TL/DR: Unfortunately, the https://github.com/Bkucera/cypress-plugin-tab plugin doesn't
381+
// support Shadow DOM, and Cypress itself doesn't have great support for it either
382+
// (see more info below) so there's no point in writing a test for this demo at this time.
383+
// NOTE: Because of how Cypress interacts with Shadown DOMs, it sees the shadow as a black
384+
// box that has focus, so that limits what we can check for in expectations (e.g. we can't
385+
// effectively check that an element inside a shadow has focus; Cypress will always say yes
386+
// because something inside has focus, but it doesn't know what, exactly...). Also, the
387+
// cypress-plugin-tab will complain if we try to .tab() from inside the shadow host saying
388+
// it's not a tabbable element because it doesn't appear to support shadow DOM.
389+
// });
378390
});

demo/index.html

+15-1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ <h2 id="setReturnFocus-heading">demo setReturnFocus option applied</h2>
131131
View demo source <span aria-hidden="true">&gt;&gt;</span>
132132
</a>
133133
</p>
134+
134135
<h2 id="iframe-heading">demo Iframe with document option applied</h2>
135136
<p>
136137
When integrated in an iframe, you may specify the document (of the said
@@ -149,7 +150,20 @@ <h2 id="iframe-heading">demo Iframe with document option applied</h2>
149150
View demo source <span aria-hidden="true">&gt;&gt;</span>
150151
</a>
151152
</p>
152-
<div id="demo-iframe" />
153+
<div id="demo-iframe"></div>
154+
155+
<h2 id="with-shadow-dom-heading">with shadow dom</h2>
156+
<p>
157+
This focus trap <em>contains</em> tabbable elements that are <strong>inside</strong>
158+
open and closed Shadow DOMs. It configures <code>tabbable</code> to look for Shadow DOM
159+
elements and provides a reference to the closed Shadow when requested.
160+
</p>
161+
<div id="demo-with-shadow-dom"></div>
162+
<p>
163+
<a href="https://github.com/focus-trap/focus-trap-react/blob/master/demo/js/demo-defaults.js" aria-describedby="defaults-heading">
164+
View demo source <span aria-hidden="true">&gt;&gt;</span>
165+
</a>
166+
</p>
153167

154168
<p>
155169
<span aria-hidden="true" style="font-size:2em;vertical-align:middle;"></span>

demo/js/demo-with-shadow-dom.js

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const React = require('react');
2+
const ReactDOM = require('react-dom');
3+
const FocusTrap = require('../../dist/focus-trap-react');
4+
5+
const createShadow = function (hostEl, isOpen) {
6+
const containerEl = document.createElement('div');
7+
containerEl.id = 'with-shadow-dom-closed-container';
8+
containerEl.style = `border: 1px dotted black; margin-top: 10px; padding: 10px; background-color: ${
9+
isOpen ? 'transparent' : 'rgba(0, 0, 0, 0.05)'
10+
};`;
11+
containerEl.innerHTML = `
12+
<p style="margin-top: 0; padding-top: 0;">
13+
This field is inside a <strong>${
14+
isOpen ? 'opened' : 'closed'
15+
}</strong> Shadow DOM:
16+
</p>
17+
<input id="text-input" type="text" />
18+
`;
19+
20+
// use same styles as host
21+
const styleLinkEl = document.createElement('link');
22+
styleLinkEl.setAttribute('rel', 'stylesheet');
23+
styleLinkEl.setAttribute('href', 'style.css');
24+
25+
const shadowEl = hostEl.attachShadow({ mode: isOpen ? 'open' : 'closed' });
26+
shadowEl.appendChild(styleLinkEl);
27+
shadowEl.appendChild(containerEl);
28+
29+
return shadowEl;
30+
};
31+
32+
const DemoWithShadowDom = function () {
33+
const [active, setActive] = React.useState(false);
34+
const openedShadowHostRef = React.useRef(null);
35+
const openedShadowRef = React.useRef(null);
36+
const closedShadowHostRef = React.useRef(null);
37+
const closedShadowRef = React.useRef(null);
38+
39+
const handleTrapActivate = React.useCallback(function () {
40+
setActive(true);
41+
}, []);
42+
43+
const handleTrapDeactivate = React.useCallback(function () {
44+
setActive(false);
45+
}, []);
46+
47+
React.useEffect(function () {
48+
if (openedShadowHostRef.current && !openedShadowRef.current) {
49+
openedShadowRef.current = createShadow(openedShadowHostRef.current, true);
50+
}
51+
52+
if (closedShadowHostRef.current && !closedShadowRef.current) {
53+
closedShadowRef.current = createShadow(
54+
closedShadowHostRef.current,
55+
false
56+
);
57+
}
58+
}, []);
59+
60+
return (
61+
<div>
62+
<p>
63+
<button
64+
onClick={handleTrapActivate}
65+
aria-describedby="with-shadow-dom-heading"
66+
>
67+
activate trap
68+
</button>
69+
</p>
70+
<FocusTrap
71+
active={active}
72+
focusTrapOptions={{
73+
onDeactivate: handleTrapDeactivate,
74+
tabbableOptions: {
75+
getShadowRoot(node) {
76+
if (node === closedShadowHostRef.current) {
77+
return closedShadowHostRef.current;
78+
}
79+
},
80+
},
81+
}}
82+
>
83+
<div className={`trap ${active ? 'is-active' : ''}`}>
84+
<p>
85+
Here is a focus trap <a href="#">with</a> <a href="#">some</a>{' '}
86+
<a href="#">focusable</a> parts.
87+
</p>
88+
<div id="with-shadow-dom-opened-host" ref={openedShadowHostRef}></div>
89+
<div id="with-shadow-dom-closed-host" ref={closedShadowHostRef}></div>
90+
<p>
91+
<button
92+
onClick={handleTrapDeactivate}
93+
aria-describedby="with-shadow-dom-heading"
94+
>
95+
deactivate trap
96+
</button>
97+
</p>
98+
</div>
99+
</FocusTrap>
100+
</div>
101+
);
102+
};
103+
104+
ReactDOM.render(
105+
<DemoWithShadowDom />,
106+
document.getElementById('demo-with-shadow-dom')
107+
);

demo/js/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ require('./demo-containerelements');
88
require('./demo-containerelements-childless');
99
require('./demo-setReturnFocus');
1010
require('./demo-iframe');
11+
require('./demo-with-shadow-dom'); // TEST MANUALLY (Cypress doesn't support Shadow DOM well)

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
"typescript": "^4.6.3"
9393
},
9494
"dependencies": {
95-
"focus-trap": "^6.7.3"
95+
"focus-trap": "^6.8.1"
9696
},
9797
"peerDependencies": {
9898
"prop-types": "^15.8.1",

src/focus-trap-react.js

+4
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ FocusTrap.propTypes = {
311311
]),
312312
allowOutsideClick: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
313313
preventScroll: PropTypes.bool,
314+
tabbableOptions: PropTypes.shape({
315+
displayCheck: PropTypes.oneOf(['full', 'non-zero-area', 'none']),
316+
getShadowRoot: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
317+
}),
314318
}),
315319
containerElements: PropTypes.arrayOf(PropTypes.instanceOf(ElementType)),
316320
children: PropTypes.oneOfType([

0 commit comments

Comments
 (0)