From 58c26ae9cfe5d1fda2d774261d2d8d63d1a59e27 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 24 Sep 2014 16:19:19 -0600 Subject: [PATCH] tests and stuff --- README.md | 87 ++++++++++++++++++++- examples/README.md | 9 --- examples/basic/app.css | 10 --- examples/basic/index.html | 1 - examples/global.css | 20 ----- examples/index.html | 17 ----- lib/components/ModalPortal.js | 43 +++++------ lib/helpers/ariaAppHider.js | 18 +++-- specs/Modal.spec.js | 137 ++++++++++++++++++++++++++++++---- specs/helper.js | 22 +++++- specs/main.js | 10 +-- 11 files changed, 264 insertions(+), 110 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/global.css delete mode 100644 examples/index.html 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');