Skip to content

Commit

Permalink
[fixed] improvements on setAppElement...
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
diasbruno committed Jun 25, 2017
1 parent 5641f40 commit f8edc2b
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 20 deletions.
43 changes: 28 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ Example:
</Modal>
```

### App Element

The app element allows you to specify the portion
of your app that should be hidden (via aria-hidden)
to prevent assistive technologies such as screenreaders
from reading content outside of the content of
your modal.

It's optional and if not specified it will try to use
`document.body` as your app element.

If your are doing server-side rendering, you should use
this property.

It can be specified in the following ways:

- DOMElement

```js
Modal.setAppElement(appElement);
```

- query selector - uses the first element found if you pass in a class.

```js
Modal.setAppElement('#your-app-element');
```

## Styles

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


/*
The app element allows you to specify the portion of your app that should be hidden (via aria-hidden)
to prevent assistive technologies such as screenreaders from reading content outside of the content of
your modal. It can be specified in the following ways:
* element
Modal.setAppElement(appElement);
* query selector - uses the first element found if you pass in a class.
Modal.setAppElement('#your-app-element');
*/
const appElement = document.getElementById('your-app-element');

const customStyles = {
content : {
top : '50%',
Expand Down
15 changes: 15 additions & 0 deletions specs/Modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,21 @@ describe('State', () => {
expect(!isBodyWithReactModalOpenClass()).toBeTruthy();
});

it('adding/removing aria-hidden without an appElement will try to fallback to document.body', () => {
ariaAppHider.documentNotReadyOrSSRTesting();
const node = document.createElement('div');
ReactDOM.render((
<Modal isOpen />
), node);
expect(document.body.getAttribute('aria-hidden')).toEqual('true');
ReactDOM.unmountComponentAtNode(node);
expect(document.body.getAttribute('aria-hidden')).toEqual(null);
});

it('raise an exception if appElement is a selector and no elements were found.', () => {
expect(() => ariaAppHider.setElement('.test')).toThrow();
});

it('removes aria-hidden from appElement when unmounted w/o closing', () => {
const el = document.createElement('div');
const node = document.createElement('div');
Expand Down
3 changes: 1 addition & 2 deletions src/components/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ const EE = ExecutionEnvironment;
const renderSubtreeIntoContainer = ReactDOM.unstable_renderSubtreeIntoContainer;

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

function getParentElement(parentSelector) {
return parentSelector();
}

export default class Modal extends Component {
static setAppElement(element) {
ariaAppHider.setElement(element || AppElement);
ariaAppHider.setElement(element);
}

/* eslint-disable no-console */
Expand Down
29 changes: 26 additions & 3 deletions src/helpers/ariaAppHider.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
let globalElement = typeof document !== 'undefined' ? document.body : null;
let globalElement = null;

export function assertNodeList(nodeList, selector) {
if (!nodeList || !nodeList.length) {
throw new Error(
`react-modal: No elements were found for selector ${selector}.`
);
}
}

export function setElement(element) {
let useElement = element;
if (typeof useElement === 'string') {
const el = document.querySelectorAll(useElement);
assertNodeList(el, useElement);
useElement = 'length' in el ? el[0] : el;
}
globalElement = useElement || globalElement;
return globalElement;
}

export function tryForceFallback() {
if (document && document.body) {
// force fallback to document.body
setElement(document.body);
return true;
}
return false;
}

export function validateElement(appElement) {
if (!appElement && !globalElement) {
if (!appElement && !globalElement && !tryForceFallback()) {
throw new Error([
'react-modal: You must set an element with',
'react-modal: Cannot fallback to `document.body`, because it\'s not ready or available.',
'If you are doing server-side rendering, use this function to defined an element.',
'`Modal.setAppElement(el)` to make this accessible'
]);
}
Expand All @@ -34,6 +53,10 @@ export function toggle(shouldHide, appElement) {
apply(appElement);
}

export function documentNotReadyOrSSRTesting() {
globalElement = null;
}

export function resetForTesting() {
globalElement = document.body;
}

0 comments on commit f8edc2b

Please sign in to comment.