diff --git a/README.md b/README.md index 5c02394a..a711d4bd 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,34 @@ Example: ``` +### 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 @@ -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%', diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js index dea72c93..c0bcd77f 100644 --- a/specs/Modal.spec.js +++ b/specs/Modal.spec.js @@ -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(( + + ), 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'); diff --git a/src/components/Modal.js b/src/components/Modal.js index 5d7d30ff..55ff91e7 100644 --- a/src/components/Modal.js +++ b/src/components/Modal.js @@ -11,7 +11,6 @@ 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(); @@ -19,7 +18,7 @@ function getParentElement(parentSelector) { export default class Modal extends Component { static setAppElement(element) { - ariaAppHider.setElement(element || AppElement); + ariaAppHider.setElement(element); } /* eslint-disable no-console */ diff --git a/src/helpers/ariaAppHider.js b/src/helpers/ariaAppHider.js index 9bcf4223..c9c7d1d9 100644 --- a/src/helpers/ariaAppHider.js +++ b/src/helpers/ariaAppHider.js @@ -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' ]); } @@ -34,6 +53,10 @@ export function toggle(shouldHide, appElement) { apply(appElement); } +export function documentNotReadyOrSSRTesting() { + globalElement = null; +} + export function resetForTesting() { globalElement = document.body; }