Skip to content

Commit f8edc2b

Browse files
committed
[fixed] improvements on setAppElement...
There was some pitfalls on how `setAppElement` work. If your <script /> was in <head />, there was a change that it tries to use `document.body` that is not yet ready. Another one was using an selector string that does not find any elements, causing it to try to perform all call on `null`. This patch can also help if you want to do server-side rendering, but this was not tested and, perhaps, it's better to use this function correctly.
1 parent 5641f40 commit f8edc2b

File tree

4 files changed

+70
-20
lines changed

4 files changed

+70
-20
lines changed

README.md

+28-15
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,34 @@ Example:
4848
</Modal>
4949
```
5050

51+
### App Element
52+
53+
The app element allows you to specify the portion
54+
of your app that should be hidden (via aria-hidden)
55+
to prevent assistive technologies such as screenreaders
56+
from reading content outside of the content of
57+
your modal.
58+
59+
It's optional and if not specified it will try to use
60+
`document.body` as your app element.
61+
62+
If your are doing server-side rendering, you should use
63+
this property.
64+
65+
It can be specified in the following ways:
66+
67+
- DOMElement
68+
69+
```js
70+
Modal.setAppElement(appElement);
71+
```
72+
73+
- query selector - uses the first element found if you pass in a class.
74+
75+
```js
76+
Modal.setAppElement('#your-app-element');
77+
```
78+
5179
## Styles
5280

5381
Styles are passed as an object with 2 keys, 'overlay' and 'content' like so
@@ -160,21 +188,6 @@ import React from 'react';
160188
import ReactDOM from 'react-dom';
161189
import Modal from 'react-modal';
162190

163-
164-
/*
165-
The app element allows you to specify the portion of your app that should be hidden (via aria-hidden)
166-
to prevent assistive technologies such as screenreaders from reading content outside of the content of
167-
your modal. It can be specified in the following ways:
168-
169-
* element
170-
Modal.setAppElement(appElement);
171-
172-
* query selector - uses the first element found if you pass in a class.
173-
Modal.setAppElement('#your-app-element');
174-
175-
*/
176-
const appElement = document.getElementById('your-app-element');
177-
178191
const customStyles = {
179192
content : {
180193
top : '50%',

specs/Modal.spec.js

+15
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,21 @@ describe('State', () => {
240240
expect(!isBodyWithReactModalOpenClass()).toBeTruthy();
241241
});
242242

243+
it('adding/removing aria-hidden without an appElement will try to fallback to document.body', () => {
244+
ariaAppHider.documentNotReadyOrSSRTesting();
245+
const node = document.createElement('div');
246+
ReactDOM.render((
247+
<Modal isOpen />
248+
), node);
249+
expect(document.body.getAttribute('aria-hidden')).toEqual('true');
250+
ReactDOM.unmountComponentAtNode(node);
251+
expect(document.body.getAttribute('aria-hidden')).toEqual(null);
252+
});
253+
254+
it('raise an exception if appElement is a selector and no elements were found.', () => {
255+
expect(() => ariaAppHider.setElement('.test')).toThrow();
256+
});
257+
243258
it('removes aria-hidden from appElement when unmounted w/o closing', () => {
244259
const el = document.createElement('div');
245260
const node = document.createElement('div');

src/components/Modal.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ const EE = ExecutionEnvironment;
1111
const renderSubtreeIntoContainer = ReactDOM.unstable_renderSubtreeIntoContainer;
1212

1313
const SafeHTMLElement = EE.canUseDOM ? window.HTMLElement : {};
14-
const AppElement = EE.canUseDOM ? document.body : { appendChild() {} };
1514

1615
function getParentElement(parentSelector) {
1716
return parentSelector();
1817
}
1918

2019
export default class Modal extends Component {
2120
static setAppElement(element) {
22-
ariaAppHider.setElement(element || AppElement);
21+
ariaAppHider.setElement(element);
2322
}
2423

2524
/* eslint-disable no-console */

src/helpers/ariaAppHider.js

+26-3
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,38 @@
1-
let globalElement = typeof document !== 'undefined' ? document.body : null;
1+
let globalElement = null;
2+
3+
export function assertNodeList(nodeList, selector) {
4+
if (!nodeList || !nodeList.length) {
5+
throw new Error(
6+
`react-modal: No elements were found for selector ${selector}.`
7+
);
8+
}
9+
}
210

311
export function setElement(element) {
412
let useElement = element;
513
if (typeof useElement === 'string') {
614
const el = document.querySelectorAll(useElement);
15+
assertNodeList(el, useElement);
716
useElement = 'length' in el ? el[0] : el;
817
}
918
globalElement = useElement || globalElement;
1019
return globalElement;
1120
}
1221

22+
export function tryForceFallback() {
23+
if (document && document.body) {
24+
// force fallback to document.body
25+
setElement(document.body);
26+
return true;
27+
}
28+
return false;
29+
}
30+
1331
export function validateElement(appElement) {
14-
if (!appElement && !globalElement) {
32+
if (!appElement && !globalElement && !tryForceFallback()) {
1533
throw new Error([
16-
'react-modal: You must set an element with',
34+
'react-modal: Cannot fallback to `document.body`, because it\'s not ready or available.',
35+
'If you are doing server-side rendering, use this function to defined an element.',
1736
'`Modal.setAppElement(el)` to make this accessible'
1837
]);
1938
}
@@ -34,6 +53,10 @@ export function toggle(shouldHide, appElement) {
3453
apply(appElement);
3554
}
3655

56+
export function documentNotReadyOrSSRTesting() {
57+
globalElement = null;
58+
}
59+
3760
export function resetForTesting() {
3861
globalElement = document.body;
3962
}

0 commit comments

Comments
 (0)