diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js
index c801fff3..c4a61dda 100644
--- a/specs/Modal.spec.js
+++ b/specs/Modal.spec.js
@@ -11,11 +11,18 @@ import sinon from 'sinon';
import expect from 'expect';
import ReactDOM from 'react-dom';
import Modal from '../src/components/Modal';
-import * as ariaAppHider from '../src/helpers/ariaAppHider';
import { renderModal, unmountModal, emptyDOM } from './helper';
const Simulate = TestUtils.Simulate;
+function getDefaultProps () {
+ return {
+ getAppElement () {},
+ contentLabel: 'Test Modal',
+ isOpen: true
+ };
+}
+
describe('Modal', () => {
afterEach('check if test cleaned up rendered modals', emptyDOM);
@@ -24,38 +31,28 @@ describe('Modal', () => {
it('focuses the last focused element when tabbing in from browser chrome');
it('can be open initially', () => {
- const component = renderModal({ isOpen: true }, 'hello');
+ const component = renderModal(getDefaultProps(), 'hello');
expect(component.portal.content.innerHTML.trim()).toEqual('hello');
});
it('can be closed initially', () => {
- const component = renderModal({}, 'hello');
+ const props = {
+ ...getDefaultProps(),
+ isOpen: false
+ };
+ const component = renderModal(props, 'hello');
expect(ReactDOM.findDOMNode(component.portal).innerHTML.trim()).toEqual('');
});
- it('accepts appElement as a prop', () => {
- const el = document.createElement('div');
- const node = document.createElement('div');
- ReactDOM.render(
-
- , node);
- expect(el.getAttribute('aria-hidden')).toEqual('true');
- ReactDOM.unmountComponentAtNode(node);
- });
-
it('renders into the body, not in context', () => {
const node = document.createElement('div');
const App = () => (
-
+ node}>
hello
);
- Modal.setAppElement(node);
ReactDOM.render(, node);
const modalParent = document.body.querySelector('.ReactModalPortal').parentNode;
expect(modalParent).toEqual(document.body);
@@ -64,69 +61,62 @@ describe('Modal', () => {
it('renders children', () => {
const child = 'I am a child of Modal, and he has sent me here...';
- const component = renderModal({ isOpen: true }, child);
+ const component = renderModal(getDefaultProps(), child);
expect(component.portal.content.innerHTML).toEqual(child);
});
it('renders the modal content with a dialog aria role when provided ', () => {
const child = 'I am a child of Modal, and he has sent me here...';
- const component = renderModal({ isOpen: true, role: 'dialog' }, child);
+ const component = renderModal({ ...getDefaultProps(), role: 'dialog' }, child);
expect(component.portal.content.getAttribute('role')).toEqual('dialog');
});
it('renders the modal with a aria-label based on the contentLabel prop', () => {
const child = 'I am a child of Modal, and he has sent me here...';
- const component = renderModal({ isOpen: true, contentLabel: 'Special Modal' }, child);
- expect(component.portal.content.getAttribute('aria-label')).toEqual('Special Modal');
+ const component = renderModal(getDefaultProps(), child);
+ expect(component.portal.content.getAttribute('aria-label')).toEqual('Test Modal');
});
it('has default props', () => {
+ const testProps = getDefaultProps();
const node = document.createElement('div');
- Modal.setAppElement(document.createElement('div'));
- const component = ReactDOM.render(, node);
+ testProps.getAppElement = () => document.createElement('div');
+ const component = ReactDOM.render(, node);
const props = component.props;
- expect(props.isOpen).toBe(false);
expect(props.ariaHideApp).toBe(true);
expect(props.closeTimeoutMS).toBe(0);
expect(props.shouldCloseOnOverlayClick).toBe(true);
ReactDOM.unmountComponentAtNode(node);
- ariaAppHider.resetForTesting();
- Modal.setAppElement(document.body); // restore default
});
it('removes the portal node', () => {
- const component = renderModal({ isOpen: true }, 'hello');
+ const component = renderModal(getDefaultProps(), 'hello');
expect(component.portal.content.innerHTML.trim()).toEqual('hello');
unmountModal();
expect(!document.querySelector('.ReactModalPortal')).toExist();
});
it('focuses the modal content', () => {
- renderModal({ isOpen: true }, null, function checkModalContentFocus () {
+ renderModal(getDefaultProps(), null, function checkModalContentFocus () {
expect(document.activeElement).toEqual(this.portal.content);
});
});
it('give back focus to previous element or modal.', (done) => {
- const modal = renderModal({
- isOpen: true,
- onRequestClose () {
- done();
- }
- }, null, () => {});
-
- renderModal({
- isOpen: true,
- onRequestClose () {
- Simulate.keyDown(modal.portal.content, {
- // The keyCode is all that matters, so this works
- key: 'FakeKeyToTestLater',
- keyCode: 27,
- which: 27
- });
- expect(document.activeElement).toEqual(modal.portal.content);
- }
- }, null, function checkPortalFocus () {
+ const testProps = getDefaultProps();
+ testProps.onRequestClose = () => done();
+ const modal = renderModal(testProps, null, () => {});
+ const testProps2 = getDefaultProps();
+ testProps2.onRequestClose = () => {
+ Simulate.keyDown(modal.portal.content, {
+ // The keyCode is all that matters, so this works
+ key: 'FakeKeyToTestLater',
+ keyCode: 27,
+ which: 27
+ });
+ expect(document.activeElement).toEqual(modal.portal.content);
+ };
+ renderModal(testProps2, null, function checkPortalFocus () {
expect(document.activeElement).toEqual(this.portal.content);
Simulate.keyDown(this.portal.content, {
// The keyCode is all that matters, so this works
@@ -145,13 +135,13 @@ describe('Modal', () => {
/>
);
- renderModal({ isOpen: true }, input, () => {
+ renderModal(getDefaultProps(), input, () => {
expect(document.activeElement).toEqual(document.querySelector('.focus_input'));
});
});
it('handles case when child has no tabbable elements', () => {
- const component = renderModal({ isOpen: true }, 'hello');
+ const component = renderModal(getDefaultProps(), 'hello');
expect(() => {
Simulate.keyDown(component.portal.content, { key: 'Tab', keyCode: 9, which: 9 });
}).toNotThrow();
@@ -159,7 +149,7 @@ describe('Modal', () => {
it('keeps focus inside the modal when child has no tabbable elements', () => {
let tabPrevented = false;
- const modal = renderModal({ isOpen: true }, 'hello');
+ const modal = renderModal(getDefaultProps(), 'hello');
expect(document.activeElement).toEqual(modal.portal.content);
Simulate.keyDown(modal.portal.content, {
key: 'Tab',
@@ -171,47 +161,47 @@ describe('Modal', () => {
});
it('supports portalClassName', () => {
- const modal = renderModal({ isOpen: true, portalClassName: 'myPortalClass' });
+ const modal = renderModal({ ...getDefaultProps(), portalClassName: 'myPortalClass' });
expect(modal.node.className).toEqual('myPortalClass');
});
it('supports custom className', () => {
- const modal = renderModal({ isOpen: true, className: 'myClass' });
+ const modal = renderModal({ ...getDefaultProps(), className: 'myClass' });
expect(modal.portal.content.className.indexOf('myClass')).toNotEqual(-1);
});
it('supports overlayClassName', () => {
- const modal = renderModal({ isOpen: true, overlayClassName: 'myOverlayClass' });
+ const modal = renderModal({ ...getDefaultProps(), overlayClassName: 'myOverlayClass' });
expect(modal.portal.overlay.className.indexOf('myOverlayClass')).toNotEqual(-1);
});
it('overrides the default styles when a custom classname is used', () => {
- const modal = renderModal({ isOpen: true, className: 'myClass' });
+ const modal = renderModal({ ...getDefaultProps(), className: 'myClass' });
expect(modal.portal.content.style.top).toEqual('');
});
it('overrides the default styles when a custom overlayClassName is used', () => {
- const modal = renderModal({ isOpen: true, overlayClassName: 'myOverlayClass' });
+ const modal = renderModal({ ...getDefaultProps(), overlayClassName: 'myOverlayClass' });
expect(modal.portal.overlay.style.backgroundColor).toEqual('');
});
it('supports adding style to the modal contents', () => {
- const modal = renderModal({ isOpen: true, style: { content: { width: '20px' } } });
+ const modal = renderModal({ ...getDefaultProps(), style: { content: { width: '20px' } } });
expect(modal.portal.content.style.width).toEqual('20px');
});
it('supports overriding style on the modal contents', () => {
- const modal = renderModal({ isOpen: true, style: { content: { position: 'static' } } });
+ const modal = renderModal({ ...getDefaultProps(), style: { content: { position: 'static' } } });
expect(modal.portal.content.style.position).toEqual('static');
});
it('supports adding style on the modal overlay', () => {
- const modal = renderModal({ isOpen: true, style: { overlay: { width: '75px' } } });
+ const modal = renderModal({ ...getDefaultProps(), style: { overlay: { width: '75px' } } });
expect(modal.portal.overlay.style.width).toEqual('75px');
});
it('supports overriding style on the modal overlay', () => {
- const modal = renderModal({ isOpen: true, style: { overlay: { position: 'static' } } });
+ const modal = renderModal({ ...getDefaultProps(), style: { overlay: { position: 'static' } } });
expect(modal.portal.overlay.style.position).toEqual('static');
});
@@ -220,45 +210,46 @@ describe('Modal', () => {
// Just in case the default style is already relative, check that we can change it
const newStyle = previousStyle === 'relative' ? 'static' : 'relative';
Modal.defaultStyles.content.position = newStyle;
- const modal = renderModal({ isOpen: true });
+ const modal = renderModal(getDefaultProps());
expect(modal.portal.content.style.position).toEqual(newStyle);
Modal.defaultStyles.content.position = previousStyle;
});
it('adds class to body when open', () => {
- renderModal({ isOpen: false });
+ const testProps = { ...getDefaultProps(), isOpen: false };
+ renderModal(testProps);
expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(false);
unmountModal();
- renderModal({ isOpen: true });
+ renderModal(getDefaultProps());
expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(true);
unmountModal();
- renderModal({ isOpen: false });
+ renderModal(testProps);
expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(false);
});
it('removes class from body when unmounted without closing', () => {
- renderModal({ isOpen: true });
+ renderModal(getDefaultProps());
expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(true);
unmountModal();
expect(document.body.className.indexOf('ReactModal__Body--open') !== -1).toEqual(false);
});
- it('removes aria-hidden from appElement when unmounted without closing', () => {
+ it('sets aria-hidden to false on appElement when unmounted without closing', () => {
const el = document.createElement('div');
const node = document.createElement('div');
ReactDOM.render(React.createElement(Modal, {
- isOpen: true,
- appElement: el
+ ...getDefaultProps(),
+ getAppElement () { return el; }
}), node);
expect(el.getAttribute('aria-hidden')).toEqual('true');
ReactDOM.unmountComponentAtNode(node);
- expect(el.getAttribute('aria-hidden')).toEqual(null);
+ expect(el.getAttribute('aria-hidden')).toEqual('false');
});
it('adds --after-open for animations', () => {
- renderModal({ isOpen: true });
+ renderModal(getDefaultProps());
const overlay = document.querySelector('.ReactModal__Overlay');
const content = document.querySelector('.ReactModal__Content');
expect(overlay.className.match(/ReactModal__Overlay--after-open/)).toExist();
@@ -268,7 +259,7 @@ describe('Modal', () => {
it('should trigger the onAfterOpen callback', () => {
const afterOpenCallback = sinon.spy();
renderModal({
- isOpen: true,
+ ...getDefaultProps(),
onAfterOpen: afterOpenCallback
});
expect(afterOpenCallback.called).toBeTruthy();
@@ -276,7 +267,7 @@ describe('Modal', () => {
it('check the state of the modal after close with time out and reopen it', () => {
const modal = renderModal({
- isOpen: true,
+ ...getDefaultProps(),
closeTimeoutMS: 2000,
onRequestClose () {}
});
@@ -289,7 +280,7 @@ describe('Modal', () => {
it('should close on Esc key event', () => {
const requestCloseCallback = sinon.spy();
const modal = renderModal({
- isOpen: true,
+ ...getDefaultProps(),
shouldCloseOnOverlayClick: true,
onRequestClose: requestCloseCallback
});
@@ -310,6 +301,75 @@ describe('Modal', () => {
expect(event.key).toEqual('FakeKeyToTestLater');
});
+ describe('Show/Hide appElement', () => {
+ let elementArray;
+ let node;
+ beforeEach(() => {
+ const el = document.createElement('div');
+ const el2 = document.createElement('div');
+ const el3 = document.createElement('div');
+ elementArray = [el, el2, el3];
+ node = document.createElement('div');
+ });
+
+ it('hides an array of appElements', () => {
+ ReactDOM.render(
+ elementArray}
+ />
+ , node);
+ const values = elementArray.map(ae => ae.getAttribute('aria-hidden'));
+ expect(values).toEqual(['true', 'true', 'true']);
+ ReactDOM.unmountComponentAtNode(node);
+ });
+
+ it('shows an array of appElements', () => {
+ ReactDOM.render(
+ elementArray}
+ />
+ , node);
+ ReactDOM.unmountComponentAtNode(node);
+ const values = elementArray.map(ae => ae.getAttribute('aria-hidden'));
+ expect(values).toEqual(['false', 'false', 'false']);
+ });
+
+ it('hides a single appElement', () => {
+ ReactDOM.render(
+ elementArray[0]}
+ />
+ , node);
+ expect(elementArray[0].getAttribute('aria-hidden')).toEqual('true');
+ ReactDOM.unmountComponentAtNode(node);
+ });
+
+ it('shows a single appElement', () => {
+ ReactDOM.render(
+ elementArray[0]}
+ />
+ , node);
+ ReactDOM.unmountComponentAtNode(node);
+ expect(elementArray[0].getAttribute('aria-hidden')).toEqual('false');
+ });
+
+ it('throws an error if appElement is not provided', () => {
+ function renderError () {
+ ReactDOM.render(
+
+ , node);
+ }
+ expect(renderError).toThrow('react-modal: Setting an getAppElement function is required');
+ });
+ });
+
describe('should close on overlay click', () => {
afterEach('Unmount modal', emptyDOM);
@@ -317,12 +377,12 @@ describe('Modal', () => {
afterEach('Unmount modal', emptyDOM);
it('verify default prop of shouldCloseOnOverlayClick', () => {
- const modal = renderModal({ isOpen: true });
+ const modal = renderModal(getDefaultProps());
expect(modal.props.shouldCloseOnOverlayClick).toEqual(true);
});
it('verify prop of shouldCloseOnOverlayClick', () => {
- const modal = renderModal({ isOpen: true, shouldCloseOnOverlayClick: false });
+ const modal = renderModal({ ...getDefaultProps(), shouldCloseOnOverlayClick: false });
expect(modal.props.shouldCloseOnOverlayClick).toEqual(false);
});
});
@@ -333,7 +393,7 @@ describe('Modal', () => {
it('verify overlay click when shouldCloseOnOverlayClick sets to false', () => {
const requestCloseCallback = sinon.spy();
const modal = renderModal({
- isOpen: true,
+ ...getDefaultProps(),
shouldCloseOnOverlayClick: false
});
expect(modal.props.isOpen).toEqual(true);
@@ -346,7 +406,7 @@ describe('Modal', () => {
it('verify overlay click when shouldCloseOnOverlayClick sets to true', () => {
const requestCloseCallback = sinon.spy();
const modal = renderModal({
- isOpen: true,
+ ...getDefaultProps(),
shouldCloseOnOverlayClick: true,
onRequestClose () {
requestCloseCallback();
@@ -362,7 +422,7 @@ describe('Modal', () => {
it('verify overlay mouse down and content mouse up when shouldCloseOnOverlayClick sets to true', () => {
const requestCloseCallback = sinon.spy();
const modal = renderModal({
- isOpen: true,
+ ...getDefaultProps(),
shouldCloseOnOverlayClick: true,
onRequestClose: requestCloseCallback
});
@@ -379,7 +439,7 @@ describe('Modal', () => {
it('verify content mouse down and overlay mouse up when shouldCloseOnOverlayClick sets to true', () => {
const requestCloseCallback = sinon.spy();
const modal = renderModal({
- isOpen: true,
+ ...getDefaultProps(),
shouldCloseOnOverlayClick: true,
onRequestClose () {
requestCloseCallback();
@@ -398,7 +458,7 @@ describe('Modal', () => {
it('should not stop event propagation', () => {
let hasPropagated = false;
const modal = renderModal({
- isOpen: true,
+ ...getDefaultProps(),
shouldCloseOnOverlayClick: true
});
const overlay = TestUtils.scryRenderedDOMComponentsWithClass(modal.portal, 'ReactModal__Overlay');
@@ -419,7 +479,7 @@ describe('Modal', () => {
it('verify event passing on overlay click', () => {
const requestCloseCallback = sinon.spy();
const modal = renderModal({
- isOpen: true,
+ ...getDefaultProps(),
shouldCloseOnOverlayClick: true,
onRequestClose: requestCloseCallback
});
@@ -443,7 +503,7 @@ describe('Modal', () => {
it('adds --before-close for animations', () => {
const closeTimeoutMS = 50;
const modal = renderModal({
- isOpen: true,
+ ...getDefaultProps(),
closeTimeoutMS
});
@@ -463,7 +523,7 @@ describe('Modal', () => {
const closeTimeoutMS = 50;
renderModal({
- isOpen: true,
+ ...getDefaultProps(),
closeTimeoutMS
});
diff --git a/src/components/Modal.js b/src/components/Modal.js
index 2089f81e..59abe32d 100644
--- a/src/components/Modal.js
+++ b/src/components/Modal.js
@@ -1,13 +1,11 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
-import ExecutionEnvironment from 'exenv';
import elementClass from 'element-class';
import ModalPortal from './ModalPortal';
import * as ariaAppHider from '../helpers/ariaAppHider';
const renderSubtreeIntoContainer = ReactDOM.unstable_renderSubtreeIntoContainer;
-const SafeHTMLElement = ExecutionEnvironment.canUseDOM ? window.HTMLElement : {};
function getParentElement (parentSelector) {
return parentSelector();
@@ -23,7 +21,12 @@ export default class Modal extends Component {
overlay: React.PropTypes.object
}),
portalClassName: React.PropTypes.string,
- appElement: React.PropTypes.instanceOf(SafeHTMLElement),
+ /**
+ * A function that returns the appElement that will be aria-hidden
+ * when the modal is open. The function should return a DOMElement or
+ * an array of DOMElements.
+ */
+ getAppElement: React.PropTypes.func.isRequired,
onAfterOpen: React.PropTypes.func,
onRequestClose: React.PropTypes.func,
closeTimeoutMS: React.PropTypes.number,
@@ -69,10 +72,6 @@ export default class Modal extends Component {
}
};
- static setAppElement (element) {
- ariaAppHider.setElement(element);
- }
-
static injectCSS () {
return process.env.NODE_ENV !== 'production'
&& console.warn('React-Modal: injectCSS has been deprecated ' +
@@ -102,7 +101,7 @@ export default class Modal extends Component {
componentWillUnmount () {
if (this.props.ariaHideApp) {
- ariaAppHider.show(this.props.appElement);
+ ariaAppHider.show(this.props.getAppElement());
}
const state = this.portal.state;
@@ -137,7 +136,7 @@ export default class Modal extends Component {
}
if (props.ariaHideApp) {
- ariaAppHider.toggle(props.isOpen, props.appElement);
+ ariaAppHider.toggle(this.props.getAppElement(), props.isOpen);
}
this.portal = renderSubtreeIntoContainer(this,
diff --git a/src/helpers/ariaAppHider.js b/src/helpers/ariaAppHider.js
index f5ee72e9..aa4b83fe 100644
--- a/src/helpers/ariaAppHider.js
+++ b/src/helpers/ariaAppHider.js
@@ -1,37 +1,24 @@
-let globalElement = typeof document !== 'undefined' ? document.body : null;
-
function validateElement (appElement) {
- if (!appElement && !globalElement) {
- throw new Error('react-modal: You must set an element with `Modal.setAppElement(el)` to make this accessible');
+ if (!appElement) {
+ throw new Error('react-modal: Setting an getAppElement function is required');
}
}
-export function setElement (element) {
- let newElement = element;
- if (typeof newElement === 'string') {
- const el = document.querySelectorAll(element);
- newElement = 'length' in el ? el[0] : el;
+export function toggle (appElement, value) {
+ validateElement(appElement);
+ if (Array.isArray(appElement)) {
+ appElement.forEach((ae) => {
+ ae.setAttribute('aria-hidden', value);
+ });
+ } else {
+ appElement.setAttribute('aria-hidden', value);
}
- globalElement = newElement || globalElement;
- return globalElement;
}
export function hide (appElement) {
- validateElement(appElement);
- (appElement || globalElement).setAttribute('aria-hidden', 'true');
+ toggle(appElement, true);
}
export function show (appElement) {
- validateElement(appElement);
- (appElement || globalElement).removeAttribute('aria-hidden');
-}
-
-export function toggle (shouldHide, appElement) {
- if (shouldHide) {
- hide(appElement);
- } else { show(appElement); }
-}
-
-export function resetForTesting () {
- globalElement = document.body;
+ toggle(appElement, false);
}