diff --git a/README.md b/README.md
index 98fca4fc..be6e0e9b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,90 @@
React Modal
===========
-WIP
+Accessible React Modal Dialog Component
+
+Usage
+-----
+
+```xml
+
+ Modal Content
+ Etc.
+
+```
+
+Accessibility Notes
+-------------------
+
+
+
+Inside the app:
+
+```js
+/** @jsx React.DOM */
+
+var React = require('react');
+var Modal = require('react-modal');
+
+var appElement = document.getElementById('your-app-element');
+
+Modal.setAppElement(appElement);
+Modal.injectCSS();
+
+var App = React.createClass({
+
+ getInitialState: function() {
+ return { modalIsOpen: false };
+ },
+
+ openModal: function() {
+ this.setState({modalIsOpen: true});
+ },
+
+ closeModal: function() {
+ this.setState({modalIsOpen: false});
+ },
+
+ handleModalCloseRequest: function() {
+ // opportunity to validate something and keep the modal open even if it
+ // requested to be closed
+ this.setState({modalIsOpen: false});
+ },
+
+ render: function() {
+ return (
+
+
+
+ Hello
+
+ I am a modal
+
+
+
+ );
+ }
+});
+
+React.renderComponent(, appElement);
+```
diff --git a/examples/README.md b/examples/README.md
deleted file mode 100644
index 8f0989f6..00000000
--- a/examples/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-### React Router Examples
-
-In order to try out the examples, you need to follow these steps:
-
-1. Clone this repo
-1. Run `npm -g install webpack`, if you don't have it installed already
-1. Run `npm install` from the repo's root directory
-1. Run `./script/build-examples` from the repo's root directory
-1. Point your browser to the `index.html` location in this directory
diff --git a/examples/basic/app.css b/examples/basic/app.css
index 086477e3..0815fd74 100644
--- a/examples/basic/app.css
+++ b/examples/basic/app.css
@@ -4,14 +4,6 @@ body {
background: #ccc;
}
-a {
- color: hsl(200, 50%, 50%);
-}
-
-a.active {
- color: hsl(20, 50%, 50%);
-}
-
.ReactModal__Overlay {
-webkit-perspective: 600;
perspective: 600;
@@ -41,5 +33,3 @@ a.active {
transition: all 150ms ease-in;
}
-
-
diff --git a/examples/basic/index.html b/examples/basic/index.html
index 5c774816..b6b5dc97 100644
--- a/examples/basic/index.html
+++ b/examples/basic/index.html
@@ -1,7 +1,6 @@
Master Detail Example
-
diff --git a/examples/global.css b/examples/global.css
deleted file mode 100644
index b2fdc81a..00000000
--- a/examples/global.css
+++ /dev/null
@@ -1,20 +0,0 @@
-body {
- font-family: "Helvetica Neue", Arial;
- font-weight: 200;
-}
-
-h1, h2, h3 {
- font-weight: 100;
-}
-
-a {
- color: hsl(200, 50%, 50%);
-}
-
-a.active {
- color: hsl(20, 50%, 50%);
-}
-
-.breadcrumbs a {
- text-decoration: none;
-}
diff --git a/examples/index.html b/examples/index.html
deleted file mode 100644
index 1a573076..00000000
--- a/examples/index.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
-React Router Examples
-
-
-React Router Examples
-
diff --git a/lib/components/ModalPortal.js b/lib/components/ModalPortal.js
index ff53844f..603d4204 100644
--- a/lib/components/ModalPortal.js
+++ b/lib/components/ModalPortal.js
@@ -1,6 +1,5 @@
-/** @jsx React.DOM */
-
var React = require('react');
+var div = React.DOM.div;
var focusManager = require('../helpers/focusManager');
var scopeTab = require('../helpers/scopeTab');
@@ -98,8 +97,8 @@ var ModalPortal = module.exports = React.createClass({
},
handleKeyDown: function(event) {
- if (event.keyCode == 9 /*tab*/) scopeTab(this.getDOMNode(), event);
- if (event.keyCode == 27 /*esc*/) this.requestClose();
+ if (event.key == 9 /*tab*/) scopeTab(this.getDOMNode(), event);
+ if (event.key == 27 /*esc*/) this.requestClose();
},
handleOverlayClick: function() {
@@ -134,27 +133,23 @@ var ModalPortal = module.exports = React.createClass({
},
render: function() {
- if (this.shouldBeClosed())
- return ;
-
- return (
-
-
- {this.props.children}
-
-
+ return this.shouldBeClosed() ? div() : (
+ div({
+ className: this.buildClassName('overlay'),
+ style: this.overlayStyles,
+ onClick: this.handleOverlayClick
+ },
+ div({
+ ref: "content",
+ className: this.buildClassName('content'),
+ tabIndex: "-1",
+ onClick: stopPropagation,
+ onKeyDown: this.handleKeyDown
+ },
+ this.props.children
+ )
+ )
);
}
-
});
diff --git a/lib/helpers/ariaAppHider.js b/lib/helpers/ariaAppHider.js
index 8f5c27a8..886ab3d1 100644
--- a/lib/helpers/ariaAppHider.js
+++ b/lib/helpers/ariaAppHider.js
@@ -5,26 +5,34 @@ function setElement(element) {
}
function hide(appElement) {
- validateElement();
+ validateElement(appElement);
(appElement || _element).setAttribute('aria-hidden', 'true');
}
function show(appElement) {
- validateElement();
+ validateElement(appElement);
(appElement || _element).removeAttribute('aria-hidden');
}
function toggle(shouldHide, appElement) {
- if (shouldHide) hide(appElement); else show(appElement);
+ if (shouldHide)
+ hide(appElement);
+ else
+ show(appElement);
}
-function validateElement() {
- if (!_element)
+function validateElement(appElement) {
+ if (!appElement && !_element)
throw new Error('react-modal: You must set an element with `Modal.setAppElement(el)` to make this accessible');
}
+function resetForTesting() {
+ _element = null;
+}
+
exports.toggle = toggle;
exports.setElement = setElement;
exports.show = show;
exports.hide = hide;
+exports.resetForTesting = resetForTesting;
diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js
index f2603489..39cf993e 100644
--- a/specs/Modal.spec.js
+++ b/specs/Modal.spec.js
@@ -1,21 +1,132 @@
require('./helper');
var Modal = require('../lib/components/Modal');
+var React = require('react/addons');
+var Simulate = React.addons.TestUtils.Simulate;
+var ariaAppHider = require('../lib/helpers/ariaAppHider');
+var button = React.DOM.button;
describe('Modal', function () {
- it('throws without an appElement');
- it('uses the global appElement');
- it('accepts appElement as a prop');
- it('opens');
- it('closes');
- it('renders into the body, not in context');
- it('renders children');
- it('has default props');
- it('removes the portal node');
+
it('scopes tab navigation to the modal');
- it('focuses the modal');
it('focuses the last focused element when tabbing in from browser chrome');
- it('adds --after-open for animations');
- it('adds --before-close for animations');
- it('does not freak out when you hand it a ref');
+
+
+ it('can be open initially', function() {
+ var component = renderModal({isOpen: true}, 'hello');
+ equal(component.portal.refs.content.getDOMNode().innerHTML.trim(), 'hello');
+ unmountModal();
+ });
+
+ it('can be closed initially', function() {
+ var component = renderModal({}, 'hello');
+ equal(component.portal.getDOMNode().innerHTML.trim(), '');
+ unmountModal();
+ });
+
+ it('throws without an appElement', function() {
+ var node = document.createElement('div');
+ throws(function() {
+ React.renderComponent(Modal({isOpen: true}), node);
+ });
+ React.unmountComponentAtNode(node);
+ });
+
+ it('uses the global appElement', function() {
+ var app = document.createElement('div');
+ var node = document.createElement('div');
+ Modal.setAppElement(app);
+ React.renderComponent(Modal({isOpen: true}), node);
+ equal(app.getAttribute('aria-hidden'), 'true');
+ ariaAppHider.resetForTesting();
+ React.unmountComponentAtNode(node);
+ });
+
+ it('accepts appElement as a prop', function() {
+ var el = document.createElement('div');
+ var node = document.createElement('div');
+ React.renderComponent(Modal({
+ isOpen: true,
+ appElement: el
+ }), node);
+ equal(el.getAttribute('aria-hidden'), 'true');
+ React.unmountComponentAtNode(node);
+ });
+
+ it('renders into the body, not in context', function() {
+ var node = document.createElement('div');
+ var App = React.createClass({
+ render: function() {
+ return React.DOM.div({}, Modal({isOpen: true, ariaHideApp: false}, 'hello'));
+ }
+ });
+ React.renderComponent(App(), node);
+ var modalParent = document.body.querySelector('.ReactModalPortal').parentNode;
+ equal(modalParent, document.body);
+ React.unmountComponentAtNode(node);
+ });
+
+ it('renders children', function() {
+ var child = 'I am a child of Modal, and he has sent me here...';
+ var component = renderModal({isOpen: true}, child);
+ equal(component.portal.refs.content.getDOMNode().innerHTML, child);
+ unmountModal();
+ });
+
+ it('has default props', function() {
+ var node = document.createElement('div');
+ Modal.setAppElement(document.createElement('div'));
+ var component = React.renderComponent(Modal(), node);
+ var props = component.props;
+ equal(props.isOpen, false);
+ equal(props.ariaHideApp, true);
+ equal(props.closeTimeoutMS, 0);
+ React.unmountComponentAtNode(node);
+ ariaAppHider.resetForTesting();
+ });
+
+ it('removes the portal node', function() {
+ var component = renderModal({isOpen: true}, 'hello');
+ equal(component.portal.refs.content.getDOMNode().innerHTML.trim(), 'hello');
+ unmountModal();
+ ok(!document.querySelector('.ReactModalPortal'));
+ });
+
+ it('focuses the modal content', function() {
+ var modal = renderModal({isOpen: true});
+ strictEqual(document.activeElement, modal.portal.refs.content.getDOMNode());
+ unmountModal();
+ });
+
+ it('adds --after-open for animations', function() {
+ var modal = renderModal({isOpen: true});
+ var overlay = document.querySelector('.ReactModal__Overlay');
+ var content = document.querySelector('.ReactModal__Content');
+ ok(overlay.className.match(/ReactModal__Overlay--after-open/));
+ ok(content.className.match(/ReactModal__Content--after-open/));
+ unmountModal();
+ });
+
+ //it('adds --before-close for animations', function() {
+ //var node = document.createElement('div');
+
+ //var component = React.renderComponent(Modal({
+ //isOpen: true,
+ //ariaHideApp: false,
+ //closeTimeoutMS: 50,
+ //}), node);
+
+ //component = React.renderComponent(Modal({
+ //isOpen: false,
+ //ariaHideApp: false,
+ //closeTimeoutMS: 50,
+ //}), node);
+
+ // It can't find these nodes, I didn't spend much time on this
+ //var overlay = document.querySelector('.ReactModal__Overlay');
+ //var content = document.querySelector('.ReactModal__Content');
+ //ok(overlay.className.match(/ReactModal__Overlay--before-close/));
+ //ok(content.className.match(/ReactModal__Content--before-close/));
+ //unmountModal();
+ //});
});
diff --git a/specs/helper.js b/specs/helper.js
index ec4015f8..7eb4b260 100644
--- a/specs/helper.js
+++ b/specs/helper.js
@@ -1,5 +1,25 @@
assert = require('assert');
-expect = require('expect');
React = require('react/addons');
+var Modal = require('../lib/components/Modal');
+
ReactTestUtils = React.addons.TestUtils;
+ok = assert.ok;
+equal = assert.equal;
+strictEqual = assert.strictEqual;
+throws = assert.throws;
+
+var _currentDiv = null;
+
+renderModal = function(def) {
+ def.ariaHideApp = false;
+ _currentDiv = document.createElement('div');
+ document.body.appendChild(_currentDiv);
+ return React.renderComponent(Modal.apply(Modal, arguments), _currentDiv);
+};
+
+unmountModal = function() {
+ React.unmountComponentAtNode(_currentDiv);
+ document.body.removeChild(_currentDiv);
+ _currentDiv = null;
+};
diff --git a/specs/main.js b/specs/main.js
index c5865e73..16266bc4 100644
--- a/specs/main.js
+++ b/specs/main.js
@@ -1,9 +1 @@
-require('./ActiveDelegate.spec.js');
-require('./AsyncState.spec.js');
-require('./DefaultRoute.spec.js');
-require('./NotFoundRoute.spec.js');
-require('./Path.spec.js');
-require('./PathStore.spec.js');
-require('./Route.spec.js');
-require('./Routes.spec.js');
-require('./RouteStore.spec.js');
+require('./Modal.spec.js');