diff --git a/.eslintrc.js b/.eslintrc similarity index 60% rename from .eslintrc.js rename to .eslintrc index 91f52e05..bdf61680 100644 --- a/.eslintrc.js +++ b/.eslintrc @@ -1,4 +1,4 @@ -module.exports = { +{ "env": { "es6": true, "browser": true @@ -13,13 +13,20 @@ module.exports = { "sourceType": "module" }, "parser": "babel-eslint", - rules: { + "rules": { + "quotes": [0], "comma-dangle": [2, "only-multiline"], - "max-len": [1, {"code": 140}], + "max-len": [1, {"code": 80}], + "no-unused-expressions": [0], "no-continue": [0], "no-plusplus": [0], - "space-before-function-paren": [2, "always"], + "func-names": [0], + "arrow-parens": [0], + "space-before-function-paren": [0], "import/no-extraneous-dependencies": [2, {"devDependencies": true}], + "jsx-a11y/no-static-element-interactions": [0], + "react/no-find-dom-node": [0], + "react/jsx-closing-bracket-location": [0], "react/jsx-filename-extension": ["error", {"extensions": [".js"]}] - }, -}; + } +} diff --git a/lib/components/Modal.js b/lib/components/Modal.js index 33d613fd..ee51a588 100644 --- a/lib/components/Modal.js +++ b/lib/components/Modal.js @@ -2,31 +2,37 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import ExecutionEnvironment from 'exenv'; -import ModalPortal from './ModalPortal'; import elementClass from 'element-class'; +import ModalPortal from './ModalPortal'; import * as ariaAppHider from '../helpers/ariaAppHider'; import * as refCount from '../helpers/refCount'; +const EE = ExecutionEnvironment; const renderSubtreeIntoContainer = ReactDOM.unstable_renderSubtreeIntoContainer; -const SafeHTMLElement = ExecutionEnvironment.canUseDOM ? window.HTMLElement : {}; -let AppElement = ExecutionEnvironment.canUseDOM ? document.body : {appendChild: function() {}}; +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 = function(element) { - AppElement = ariaAppHider.setElement(element); - }; + static setAppElement(element) { + ariaAppHider.setElement(element || AppElement); + } - static injectCSS = function() { - "production" !== process.env.NODE_ENV - && console.warn('React-Modal: injectCSS has been deprecated ' + - 'and no longer has any effect. It will be removed in a later version'); - }; + /* eslint-disable no-console */ + static injectCSS() { + (process.env.NODE_ENV !== "production") + && console.warn( + 'React-Modal: injectCSS has been deprecated ' + + 'and no longer has any effect. It will be removed in a later version' + ); + } + /* eslint-enable no-console */ + /* eslint-disable react/no-unused-prop-types */ static propTypes = { isOpen: PropTypes.bool.isRequired, style: PropTypes.shape({ @@ -35,6 +41,14 @@ export default class Modal extends Component { }), portalClassName: PropTypes.string, bodyOpenClassName: PropTypes.string, + className: PropTypes.oneOfType([ + PropTypes.String, + PropTypes.object + ]), + overlayClassName: PropTypes.oneOfType([ + PropTypes.String, + PropTypes.object + ]), appElement: PropTypes.instanceOf(SafeHTMLElement), onAfterOpen: PropTypes.func, onRequestClose: PropTypes.func, @@ -45,6 +59,7 @@ export default class Modal extends Component { role: PropTypes.string, contentLabel: PropTypes.string.isRequired }; + /* eslint-enable react/no-unused-prop-types */ static defaultProps = { isOpen: false, @@ -53,31 +68,31 @@ export default class Modal extends Component { ariaHideApp: true, closeTimeoutMS: 0, shouldCloseOnOverlayClick: true, - parentSelector: function () { return document.body; } + parentSelector() { return document.body; } }; static defaultStyles = { overlay: { - position : 'fixed', - top : 0, - left : 0, - right : 0, - bottom : 0, - backgroundColor : 'rgba(255, 255, 255, 0.75)' + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(255, 255, 255, 0.75)' }, content: { - position : 'absolute', - top : '40px', - left : '40px', - right : '40px', - bottom : '40px', - border : '1px solid #ccc', - background : '#fff', - overflow : 'auto', - WebkitOverflowScrolling : 'touch', - borderRadius : '4px', - outline : 'none', - padding : '20px' + position: 'absolute', + top: '40px', + left: '40px', + right: '40px', + bottom: '40px', + border: '1px solid #ccc', + background: '#fff', + overflow: 'auto', + WebkitOverflowScrolling: 'touch', + borderRadius: '4px', + outline: 'none', + padding: '20px' } }; @@ -92,12 +107,6 @@ export default class Modal extends Component { this.renderPortal(this.props); } - componentWillUpdate(newProps) { - if(newProps.portalClassName !== this.props.portalClassName) { - this.node.className = newProps.portalClassName; - } - } - componentWillReceiveProps(newProps) { if (newProps.isOpen) refCount.add(this); if (!newProps.isOpen) refCount.remove(this); @@ -112,6 +121,12 @@ export default class Modal extends Component { this.renderPortal(newProps); } + componentWillUpdate(newProps) { + if (newProps.portalClassName !== this.props.portalClassName) { + this.node.className = newProps.portalClassName; + } + } + componentWillUnmount() { if (!this.node) return; @@ -132,8 +147,7 @@ export default class Modal extends Component { this.portal.closeWithTimeout(); } - const that = this; - setTimeout(function() { that.removePortal(); }, closesAt - now); + setTimeout(() => this.removePortal, closesAt - now); } else { this.removePortal(); } diff --git a/lib/components/ModalPortal.js b/lib/components/ModalPortal.js index 19f2bed6..3c22c0ea 100644 --- a/lib/components/ModalPortal.js +++ b/lib/components/ModalPortal.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; +import { PropTypes } from 'prop-types'; import * as focusManager from '../helpers/focusManager'; -import { scopeTab } from '../helpers/scopeTab'; +import scopeTab from '../helpers/scopeTab'; // so that our CSS is statically analyzable const CLASS_NAMES = { @@ -19,6 +20,33 @@ export default class ModalPortal extends Component { } }; + static propTypes = { + isOpen: PropTypes.bool.isRequired, + defaultStyles: PropTypes.shape({ + content: PropTypes.object, + overlay: PropTypes.object + }), + style: PropTypes.shape({ + content: PropTypes.object, + overlay: PropTypes.object + }), + className: PropTypes.oneOfType([ + PropTypes.String, + PropTypes.object + ]), + overlayClassName: PropTypes.oneOfType([ + PropTypes.String, + PropTypes.object + ]), + onAfterOpen: PropTypes.func, + onRequestClose: PropTypes.func, + closeTimeoutMS: PropTypes.number, + shouldCloseOnOverlayClick: PropTypes.bool, + role: PropTypes.string, + contentLabel: PropTypes.string, + children: PropTypes.node + }; + constructor(props) { super(props); @@ -38,10 +66,6 @@ export default class ModalPortal extends Component { } } - componentWillUnmount() { - clearTimeout(this.closeTimer); - } - componentWillReceiveProps(newProps) { // Focus only needs to be set once when the modal is being opened if (!this.props.isOpen && newProps.isOpen) { @@ -59,6 +83,10 @@ export default class ModalPortal extends Component { } } + componentWillUnmount() { + clearTimeout(this.closeTimer); + } + setFocusAfterRender = focus => { this.focusAfterRender = focus; } @@ -93,17 +121,16 @@ export default class ModalPortal extends Component { } } - focusContent = () => { - // Don't steal focus from inner elements - if (!this.contentHasFocus()) { - this.refs.content.focus(); - } - } + // Don't steal focus from inner elements + focusContent = () => (!this.contentHasFocus()) && this.content.focus(); closeWithTimeout = () => { const closesAt = Date.now() + this.props.closeTimeoutMS; - this.setState({ beforeClose: true, closesAt: closesAt }, () => { - this.closeTimer = setTimeout(this.closeWithoutTimeout, this.state.closesAt - Date.now()); + this.setState({ beforeClose: true, closesAt }, () => { + this.closeTimer = setTimeout( + this.closeWithoutTimeout, + this.state.closesAt - Date.now() + ); }); } @@ -117,10 +144,10 @@ export default class ModalPortal extends Component { } handleKeyDown = event => { - if (event.keyCode == TAB_KEY) { - scopeTab(this.refs.content, event); + if (event.keyCode === TAB_KEY) { + scopeTab(this.content, event); } - if (event.keyCode == ESC_KEY) { + if (event.keyCode === ESC_KEY) { event.preventDefault(); this.requestClose(event); } @@ -145,53 +172,56 @@ export default class ModalPortal extends Component { this.shouldClose = false; } - requestClose = event => { - if (this.ownerHandlesClose()) { - this.props.onRequestClose(event); - } - } + requestClose = event => + this.ownerHandlesClose() && this.props.onRequestClose(event); - ownerHandlesClose = () => { - return this.props.onRequestClose; - } + ownerHandlesClose = () => + this.props.onRequestClose; - shouldBeClosed = () => { - return !this.state.isOpen && !this.state.beforeClose; - } + shouldBeClosed = () => + !this.state.isOpen && !this.state.beforeClose; - contentHasFocus = () => { - return document.activeElement === this.refs.content || this.refs.content.contains(document.activeElement); - } + contentHasFocus = () => + document.activeElement === this.content || + this.content.contains(document.activeElement); buildClassName = (which, additional) => { const classNames = (typeof additional === 'object') ? additional : { base: CLASS_NAMES[which], - afterOpen: CLASS_NAMES[which] + "--after-open", - beforeClose: CLASS_NAMES[which] + "--before-close" + afterOpen: `${CLASS_NAMES[which]}--after-open`, + beforeClose: `${CLASS_NAMES[which]}--before-close` }; let className = classNames.base; - if (this.state.afterOpen) { className += " " + classNames.afterOpen; } - if (this.state.beforeClose) { className += " " + classNames.beforeClose; } - return (typeof additional === 'string' && additional) ? [className, additional].join(" ") : className; + if (this.state.afterOpen) { + className = `${className} ${classNames.afterOpen}`; + } + if (this.state.beforeClose) { + className = `${className} ${classNames.beforeClose}`; + } + return (typeof additional === 'string' && additional) ? + `${className} ${additional}` : className; } render() { - const contentStyles = this.props.className ? {} : this.props.defaultStyles.content; - const overlayStyles = this.props.overlayClassName ? {} : this.props.defaultStyles.overlay; + const { className, overlayClassName, defaultStyles } = this.props; + const contentStyles = className ? {} : defaultStyles.content; + const overlayStyles = overlayClassName ? {} : defaultStyles.overlay; return this.shouldBeClosed() ?
: ( -
-
+
{ this.overlay = overlay; }} + className={this.buildClassName('overlay', overlayClassName)} + style={{ ...overlayStyles, ...this.props.style.overlay }} + onClick={this.handleOverlayOnClick}> +
{ this.content = content; }} + style={{ ...contentStyles, ...this.props.style.content }} + className={this.buildClassName('content', className)} + tabIndex="-1" + onKeyDown={this.handleKeyDown} + onClick={this.handleContentOnClick} + role={this.props.role} + aria-label={this.props.contentLabel}> {this.props.children}
diff --git a/lib/helpers/ariaAppHider.js b/lib/helpers/ariaAppHider.js index 8ade7473..9bcf4223 100644 --- a/lib/helpers/ariaAppHider.js +++ b/lib/helpers/ariaAppHider.js @@ -1,22 +1,32 @@ -let _element = typeof document !== 'undefined' ? document.body : null; +let globalElement = typeof document !== 'undefined' ? document.body : null; export function setElement(element) { - if (typeof element === 'string') { - const el = document.querySelectorAll(element); - element = 'length' in el ? el[0] : el; + let useElement = element; + if (typeof useElement === 'string') { + const el = document.querySelectorAll(useElement); + useElement = 'length' in el ? el[0] : el; + } + globalElement = useElement || globalElement; + return globalElement; +} + +export function validateElement(appElement) { + if (!appElement && !globalElement) { + throw new Error([ + 'react-modal: You must set an element with', + '`Modal.setAppElement(el)` to make this accessible' + ]); } - _element = element || _element; - return _element; } export function hide(appElement) { validateElement(appElement); - (appElement || _element).setAttribute('aria-hidden', 'true'); + (appElement || globalElement).setAttribute('aria-hidden', 'true'); } export function show(appElement) { validateElement(appElement); - (appElement || _element).removeAttribute('aria-hidden'); + (appElement || globalElement).removeAttribute('aria-hidden'); } export function toggle(shouldHide, appElement) { @@ -24,14 +34,6 @@ export function toggle(shouldHide, appElement) { apply(appElement); } -export function validateElement(appElement) { - if (!appElement && !_element) { - throw new Error('react-modal: You must set an element with `Modal.setAppElement(el)` to make this accessible'); - } -} - export function resetForTesting() { - _element = document.body; + globalElement = document.body; } - - diff --git a/lib/helpers/focusManager.js b/lib/helpers/focusManager.js index 3e8a2564..f4ff69fc 100644 --- a/lib/helpers/focusManager.js +++ b/lib/helpers/focusManager.js @@ -1,14 +1,14 @@ import findTabbable from '../helpers/tabbable'; -let focusLaterElements = []; +const focusLaterElements = []; let modalElement = null; let needToFocus = false; -export function handleBlur(event) { +export function handleBlur() { needToFocus = true; } -export function handleFocus(event) { +export function handleFocus() { if (needToFocus) { needToFocus = false; if (!modalElement) { @@ -19,7 +19,7 @@ export function handleFocus(event) { // the element outside of a setTimeout. Side-effect of this implementation // is that the document.body gets focus, and then we focus our element right // after, seems fine. - setTimeout(function() { + setTimeout(() => { if (modalElement.contains(document.activeElement)) { return; } @@ -33,17 +33,22 @@ export function markForFocusLater() { focusLaterElements.push(document.activeElement); } +/* eslint-disable no-console */ export function returnFocus() { let toFocus = null; try { toFocus = focusLaterElements.pop(); toFocus.focus(); return; - } - catch (e) { - console.warn('You tried to return focus to '+toFocus+' but it is not in the DOM anymore'); + } catch (e) { + console.warn([ + 'You tried to return focus to', + toFocus, + 'but it is not in the DOM anymore' + ].join(" ")); } } +/* eslint-enable no-console */ export function setupScopedFocus(element) { modalElement = element; diff --git a/lib/helpers/refCount.js b/lib/helpers/refCount.js index 21df32b4..5066dfa7 100644 --- a/lib/helpers/refCount.js +++ b/lib/helpers/refCount.js @@ -1,4 +1,4 @@ -let modals = []; +const modals = []; export function add(element) { if (modals.indexOf(element) === -1) { diff --git a/lib/helpers/scopeTab.js b/lib/helpers/scopeTab.js index e4b21d6f..9e5f180f 100644 --- a/lib/helpers/scopeTab.js +++ b/lib/helpers/scopeTab.js @@ -1,16 +1,16 @@ import findTabbable from './tabbable'; -export function scopeTab(node, event) { +export default function scopeTab(node, event) { const tabbable = findTabbable(node); if (!tabbable.length) { - event.preventDefault(); - return; + event.preventDefault(); + return; } const finalTabbable = tabbable[event.shiftKey ? 0 : tabbable.length - 1]; const leavingFinalTabbable = ( finalTabbable === document.activeElement || - // handle immediate shift+tab after opening with mouse - node === document.activeElement + // handle immediate shift+tab after opening with mouse + node === document.activeElement ); if (!leavingFinalTabbable) return; event.preventDefault(); diff --git a/lib/helpers/tabbable.js b/lib/helpers/tabbable.js index e10d0d75..905d17b4 100644 --- a/lib/helpers/tabbable.js +++ b/lib/helpers/tabbable.js @@ -10,14 +10,7 @@ * http://api.jqueryui.com/category/ui-core/ */ -function focusable(element, isTabIndexNotNaN) { - const nodeName = element.nodeName.toLowerCase(); - return (/input|select|textarea|button|object/.test(nodeName) ? - !element.disabled : - "a" === nodeName ? - element.href || isTabIndexNotNaN : - isTabIndexNotNaN) && visible(element); -} +const tabbableNode = /input|select|textarea|button|object/; function hidden(el) { return (el.offsetWidth <= 0 && el.offsetHeight <= 0) || @@ -25,14 +18,22 @@ function hidden(el) { } function visible(element) { - while (element) { - if (element === document.body) break; - if (hidden(element)) return false; - element = element.parentNode; + let parentElement = element; + while (parentElement) { + if (parentElement === document.body) break; + if (hidden(parentElement)) return false; + parentElement = parentElement.parentNode; } return true; } +function focusable(element, isTabIndexNotNaN) { + const nodeName = element.nodeName.toLowerCase(); + const res = ((tabbableNode.test(nodeName)) && !element.disabled) || + (nodeName === "a" ? element.href || isTabIndexNotNaN : isTabIndexNotNaN); + return res && visible(element); +} + function tabbable(element) { let tabIndex = element.getAttribute('tabindex'); if (tabIndex === null) tabIndex = undefined; @@ -41,9 +42,7 @@ function tabbable(element) { } export default function findTabbableDescendants(element) { - return [].slice.call(element.querySelectorAll('*'), 0).filter(function(el) { - return tabbable(el); - }); + return [].slice.call( + element.querySelectorAll('*'), 0 + ).filter(tabbable); } - - diff --git a/package.json b/package.json index 4bd50f8d..eb3faa5b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "start": "./node_modules/.bin/webpack-dev-server --inline --host 127.0.0.1 --content-base examples/", "test": "cross-env NODE_ENV=test karma start", "test:full": "npm-run-all -p 'test -- --single-run' lint", - "lint": "eslint lib/ specs/" + "lint": "eslint lib/" }, "authors": [ "Ryan Florence" diff --git a/specs/Modal.events.spec.js b/specs/Modal.events.spec.js index bca42ed4..63608be3 100644 --- a/specs/Modal.events.spec.js +++ b/specs/Modal.events.spec.js @@ -1,48 +1,46 @@ +/* eslint-env mocha */ import sinon from 'sinon'; import expect from 'expect'; import React from 'react'; import ReactDOM from 'react-dom'; import TestUtils from 'react-addons-test-utils'; import Modal from '../lib/components/Modal'; -import * as ariaAppHider from '../lib/helpers/ariaAppHider'; import { - isBodyWithReactModalOpenClass, findDOMWithClass, - contentAttribute, overlayAttribute, moverlay, mcontent, clickAt, mouseDownAt, mouseUpAt, escKeyDown, tabKeyDown, - renderModal, unmountModal, emptyDOM + renderModal, emptyDOM } from './helper'; describe('Events', () => { afterEach('Unmount modal', emptyDOM); it('should trigger the onAfterOpen callback', () => { - var afterOpenCallback = sinon.spy(); + const afterOpenCallback = sinon.spy(); renderModal({ isOpen: true, onAfterOpen: afterOpenCallback }); expect(afterOpenCallback.called).toBeTruthy(); }); it('keeps focus inside the modal when child has no tabbable elements', () => { - var tabPrevented = false; - var modal = renderModal({ isOpen: true }, 'hello'); + let tabPrevented = false; + const modal = renderModal({ isOpen: true }, 'hello'); const content = mcontent(modal); expect(document.activeElement).toEqual(content); tabKeyDown(content, { - preventDefault: function() { tabPrevented = true; } + preventDefault() { tabPrevented = true; } }); expect(tabPrevented).toEqual(true); }); it('handles case when child has no tabbable elements', () => { - var modal = renderModal({ isOpen: true }, 'hello'); + const modal = renderModal({ isOpen: true }, 'hello'); const content = mcontent(modal); tabKeyDown(content); expect(document.activeElement).toEqual(content); }); it('should close on Esc key event', () => { - var requestCloseCallback = sinon.spy(); - var modal = renderModal({ + const requestCloseCallback = sinon.spy(); + const modal = renderModal({ isOpen: true, shouldCloseOnOverlayClick: true, onRequestClose: requestCloseCallback @@ -50,63 +48,61 @@ describe('Events', () => { escKeyDown(mcontent(modal)); expect(requestCloseCallback.called).toBeTruthy(); // Check if event is passed to onRequestClose callback. - var event = requestCloseCallback.getCall(0).args[0]; + const event = requestCloseCallback.getCall(0).args[0]; expect(event).toExist(); }); - it('verify overlay click when shouldCloseOnOverlayClick sets to false', () => { - const requestCloseCallback = sinon.spy(); - const modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: false + describe('shouldCloseOnoverlayClick', () => { + it('when false, click on overlay should not close', () => { + const requestCloseCallback = sinon.spy(); + const modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: false + }); + const overlay = moverlay(modal); + clickAt(overlay); + expect(!requestCloseCallback.called).toBeTruthy(); }); - var overlay = moverlay(modal); - clickAt(overlay); - expect(!requestCloseCallback.called).toBeTruthy(); - }); - it('verify overlay click when shouldCloseOnOverlayClick sets to true', () => { - var requestCloseCallback = sinon.spy(); - var modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: true, - onRequestClose: function() { - requestCloseCallback(); - } + it('when true, click on overlay must close', () => { + const requestCloseCallback = sinon.spy(); + const modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: true, + onRequestClose: requestCloseCallback + }); + clickAt(moverlay(modal)); + expect(requestCloseCallback.called).toBeTruthy(); }); - clickAt(moverlay(modal)); - expect(requestCloseCallback.called).toBeTruthy(); - }); - it('verify overlay mouse down and content mouse up when shouldCloseOnOverlayClick sets to true', () => { - const requestCloseCallback = sinon.spy(); - const modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: true, - onRequestClose: requestCloseCallback + it('overlay mouse down and content mouse up, should not close', () => { + const requestCloseCallback = sinon.spy(); + const modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: true, + onRequestClose: requestCloseCallback + }); + mouseDownAt(moverlay(modal)); + mouseUpAt(mcontent(modal)); + expect(!requestCloseCallback.called).toBeTruthy(); }); - mouseDownAt(moverlay(modal)); - mouseUpAt(mcontent(modal)); - expect(!requestCloseCallback.called).toBeTruthy(); - }); - it('verify content mouse down and overlay mouse up when shouldCloseOnOverlayClick sets to true', () => { - var requestCloseCallback = sinon.spy(); - var modal = renderModal({ - isOpen: true, - shouldCloseOnOverlayClick: true, - onRequestClose: function() { - requestCloseCallback(); - } + it('content mouse down and overlay mouse up, should not close', () => { + const requestCloseCallback = sinon.spy(); + const modal = renderModal({ + isOpen: true, + shouldCloseOnOverlayClick: true, + onRequestClose: requestCloseCallback + }); + mouseDownAt(mcontent(modal)); + mouseUpAt(moverlay(modal)); + expect(!requestCloseCallback.called).toBeTruthy(); }); - mouseDownAt(mcontent(modal)); - mouseUpAt(moverlay(modal)); - expect(!requestCloseCallback.called).toBeTruthy(); }); it('should not stop event propagation', () => { - var hasPropagated = false; - var modal = renderModal({ + let hasPropagated = false; + const modal = renderModal({ isOpen: true, shouldCloseOnOverlayClick: true }); @@ -118,8 +114,8 @@ describe('Events', () => { }); it('verify event passing on overlay click', () => { - var requestCloseCallback = sinon.spy(); - var modal = renderModal({ + const requestCloseCallback = sinon.spy(); + const modal = renderModal({ isOpen: true, shouldCloseOnOverlayClick: true, onRequestClose: requestCloseCallback @@ -131,7 +127,7 @@ describe('Events', () => { }); expect(requestCloseCallback.called).toBeTruthy(); // Check if event is passed to onRequestClose callback. - var event = requestCloseCallback.getCall(0).args[0]; + const event = requestCloseCallback.getCall(0).args[0]; expect(event).toExist(); }); }); diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js index bbf13e72..8e5ffd66 100644 --- a/specs/Modal.spec.js +++ b/specs/Modal.spec.js @@ -1,24 +1,21 @@ /* eslint-env mocha */ -import sinon from 'sinon'; import expect from 'expect'; import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import TestUtils from 'react-addons-test-utils'; -import Modal from '../lib/components/Modal'; import createReactClass from 'create-react-class'; +import Modal from '../lib/components/Modal'; import * as ariaAppHider from '../lib/helpers/ariaAppHider'; import { - isBodyWithReactModalOpenClass, findDOMWithClass, - contentAttribute, overlayAttribute, + isBodyWithReactModalOpenClass, + contentAttribute, mcontent, moverlay, - clickAt, mouseDownAt, mouseUpAt, escKeyDown, tabKeyDown, + escKeyDown, renderModal, unmountModal, emptyDOM } from './helper'; -import * as events from './Modal.events.spec'; -import * as styles from './Modal.style.spec'; - -const Simulate = TestUtils.Simulate; +import './Modal.events.spec'; +import './Modal.style.spec'; describe('State', () => { afterEach('check if test cleaned up rendered modals', emptyDOM); @@ -33,15 +30,15 @@ describe('State', () => { }); it('can be closed initially', () => { - var modal = renderModal({}, 'hello'); + const modal = renderModal({}, 'hello'); expect(ReactDOM.findDOMNode(mcontent(modal))).toNotExist(); }); it('has default props', () => { - var node = document.createElement('div'); + const node = document.createElement('div'); Modal.setAppElement(document.createElement('div')); - var modal = ReactDOM.render(, node); - var props = modal.props; + const modal = ReactDOM.render(, node); + const props = modal.props; expect(props.isOpen).toBe(false); expect(props.ariaHideApp).toBe(true); expect(props.closeTimeoutMS).toBe(0); @@ -52,8 +49,8 @@ describe('State', () => { }); it('accepts appElement as a prop', () => { - var el = document.createElement('div'); - var node = document.createElement('div'); + const el = document.createElement('div'); + const node = document.createElement('div'); ReactDOM.render(( ), node); @@ -62,12 +59,12 @@ describe('State', () => { }); it('renders into the body, not in context', () => { - var node = document.createElement('div'); - var App = createReactClass({ + const node = document.createElement('div'); + const App = createReactClass({ render() { return (
- + hello
@@ -85,19 +82,24 @@ describe('State', () => { }); it('renders the modal content with a dialog aria role when provided ', () => { - var child = 'I am a child of Modal, and he has sent me here...'; - var modal = renderModal({ isOpen: true, role: 'dialog' }, child); + const child = 'I am a child of Modal, and he has sent me here...'; + const modal = renderModal({ isOpen: true, role: 'dialog' }, child); expect(contentAttribute(modal, 'role')).toEqual('dialog'); }); - it('renders the modal with a aria-label based on the contentLabel prop', () => { - var child = 'I am a child of Modal, and he has sent me here...'; - var modal = renderModal({ isOpen: true, contentLabel: 'Special Modal' }, child); - expect(contentAttribute(modal, 'aria-label')).toEqual('Special Modal'); + it('set aria-label based on the contentLabel prop', () => { + const child = 'I am a child of Modal, and he has sent me here...'; + const modal = renderModal({ + isOpen: true, + contentLabel: 'Special Modal' + }, child); + expect( + contentAttribute(modal, 'aria-label') + ).toEqual('Special Modal'); }); it('removes the portal node', () => { - var modal = renderModal({ isOpen: true }, 'hello'); + const modal = renderModal({ isOpen: true }, 'hello'); unmountModal(); expect(document.querySelector('.ReactModalPortal')).toNotExist(); }); @@ -115,9 +117,7 @@ describe('State', () => { const modalA = renderModal({ isOpen: true, className: 'modal-a', - onRequestClose () { - cleanup(); - } + onRequestClose: cleanup }, null); const modalContent = mcontent(modalA); @@ -126,7 +126,7 @@ describe('State', () => { const modalB = renderModal({ isOpen: true, className: 'modal-b', - onRequestClose: function() { + onRequestClose() { const modalContent = mcontent(modalB); expect(document.activeElement).toEqual(mcontent(modalA)); escKeyDown(modalContent); @@ -138,17 +138,17 @@ describe('State', () => { }); it('does not steel focus when a descendent is already focused', () => { - var content; - var input = ( - { el && el.focus(); content = el; }} /> + let content; + const input = ( + { el && el.focus(); content = el; }} /> ); - renderModal({ isOpen: true }, input, function () { + renderModal({ isOpen: true }, input, () => { expect(document.activeElement).toEqual(content); }); }); it('supports portalClassName', () => { - var modal = renderModal({ + const modal = renderModal({ isOpen: true, portalClassName: 'myPortalClass' }); @@ -172,7 +172,7 @@ describe('State', () => { ).toBeTruthy(); }); - it('overrides the default content classes when a custom object className is used', () => { + it('overrides content classes with custom object className', () => { const modal = renderModal({ isOpen: true, className: { @@ -181,11 +181,15 @@ describe('State', () => { beforeClose: 'myClass_before-close' } }); - expect(mcontent(modal).className).toEqual('myClass myClass_after-open'); + expect( + mcontent(modal).className + ).toEqual( + 'myClass myClass_after-open' + ); unmountModal(); }); - it('overrides the default overlay classes when a custom object overlayClassName is used', () => { + it('overrides overlay classes with custom object overlayClassName', () => { const modal = renderModal({ isOpen: true, overlayClassName: { @@ -194,13 +198,22 @@ describe('State', () => { beforeClose: 'myOverlayClass_before-close' } }); - expect(moverlay(modal).className).toEqual('myOverlayClass myOverlayClass_after-open'); + expect( + moverlay(modal).className + ).toEqual( + 'myOverlayClass myOverlayClass_after-open' + ); unmountModal(); }); it('supports overriding react modal open class in document.body.', () => { - const modal = renderModal({ isOpen: true, bodyOpenClassName: 'custom-modal-open' }); - expect(document.body.className.indexOf('custom-modal-open') !== -1).toBeTruthy(); + const modal = renderModal({ + isOpen: true, + bodyOpenClassName: 'custom-modal-open' + }); + expect( + document.body.className.indexOf('custom-modal-open') > -1 + ).toBeTruthy(); }); it('don\'t append class to document.body if modal is not open', () => { @@ -231,11 +244,11 @@ describe('State', () => { expect(!isBodyWithReactModalOpenClass()).toBeTruthy(); }); - it('removes aria-hidden from appElement when unmounted without closing', () => { - var el = document.createElement('div'); - var node = document.createElement('div'); + it('removes aria-hidden from appElement when unmounted w/o closing', () => { + const el = document.createElement('div'); + const node = document.createElement('div'); ReactDOM.render(( - + ), node); expect(el.getAttribute('aria-hidden')).toEqual('true'); ReactDOM.unmountComponentAtNode(node); @@ -244,7 +257,7 @@ describe('State', () => { it('adds --after-open for animations', () => { const modal = renderModal({ isOpen: true }); - var rg = /--after-open/i; + const rg = /--after-open/i; expect(rg.test(mcontent(modal).className)).toBeTruthy(); expect(rg.test(moverlay(modal).className)).toBeTruthy(); }); @@ -264,11 +277,11 @@ describe('State', () => { modal.portal.closeWithoutTimeout(); }); - it('check the state of the modal after close with time out and reopen it', () => { - var modal = renderModal({ + it('should not be open after close with time out and reopen it', () => { + const modal = renderModal({ isOpen: true, closeTimeoutMS: 2000, - onRequestClose: function() {} + onRequestClose() {} }); modal.portal.closeWithTimeout(); modal.portal.open(); @@ -282,8 +295,8 @@ describe('State', () => { }); it('verify prop of shouldCloseOnOverlayClick', () => { - var modalOpts = { isOpen: true, shouldCloseOnOverlayClick: false }; - var modal = renderModal(modalOpts); + const modalOpts = { isOpen: true, shouldCloseOnOverlayClick: false }; + const modal = renderModal(modalOpts); expect(!modal.props.shouldCloseOnOverlayClick).toBeTruthy(); }); @@ -344,15 +357,15 @@ describe('State', () => { }); it('verify that portalClassName is refreshed on component update', () => { - var node = document.createElement('div'); - var modal = null; + const node = document.createElement('div'); + let modal = null; - var App = createReactClass({ - getInitialState: function () { + const App = createReactClass({ + getInitialState() { return { testHasChanged: false }; }, - componentDidMount: function() { + componentDidMount() { expect(modal.node.className).toEqual('myPortalClass'); this.setState({ @@ -360,17 +373,21 @@ describe('State', () => { }); }, - componentDidUpdate: function() { + componentDidUpdate() { expect(modal.node.className).toEqual('myPortalClass-modifier'); }, - render: function() { + render() { + const portalClassName = this.state.testHasChanged === true ? + 'myPortalClass-modifier' : 'myPortalClass'; + return (
{ modal = modalComponent; }} - isOpen={true} - portalClassName={this.state.testHasChanged === true ? 'myPortalClass-modifier' : 'myPortalClass'}> + isOpen + portalClassName={portalClassName}> + Test
); diff --git a/specs/Modal.style.spec.js b/specs/Modal.style.spec.js index cb8a3b45..53a361e3 100644 --- a/specs/Modal.style.spec.js +++ b/specs/Modal.style.spec.js @@ -1,4 +1,4 @@ -import sinon from 'sinon'; +/* eslint-env mocha */ import expect from 'expect'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -6,11 +6,8 @@ import TestUtils from 'react-addons-test-utils'; import Modal from '../lib/components/Modal'; import * as ariaAppHider from '../lib/helpers/ariaAppHider'; import { - isBodyWithReactModalOpenClass, findDOMWithClass, - contentAttribute, overlayAttribute, mcontent, moverlay, - clickAt, mouseDownAt, mouseUpAt, escKeyDown, tabKeyDown, - renderModal, unmountModal, emptyDOM + renderModal, emptyDOM } from './helper'; describe('Style', () => { @@ -21,45 +18,48 @@ describe('Style', () => { expect(mcontent(modal).style.top).toEqual(''); }); - it('overrides the default styles when a custom overlayClassName is used', () => { - const modal = renderModal({ - isOpen: true, - overlayClassName: 'myOverlayClass' - }); - expect(moverlay(modal).style.backgroundColor).toEqual(''); - }); + it('overrides the default styles when a custom overlayClassName is used', + () => { + const modal = renderModal({ + isOpen: true, + overlayClassName: 'myOverlayClass' + }); + expect(moverlay(modal).style.backgroundColor).toEqual(''); + } + ); it('supports adding style to the modal contents', () => { const style = { content: { width: '20px' } }; - const modal = renderModal({ isOpen: true, style: style }); + const modal = renderModal({ isOpen: true, style }); expect(mcontent(modal).style.width).toEqual('20px'); }); it('supports overriding style on the modal contents', () => { const style = { content: { position: 'static' } }; - const modal = renderModal({ isOpen: true, style: style }); + const modal = renderModal({ isOpen: true, style }); expect(mcontent(modal).style.position).toEqual('static'); }); it('supports adding style on the modal overlay', () => { const style = { overlay: { width: '75px' } }; - const modal = renderModal({ isOpen: true, style: style }); + const modal = renderModal({ isOpen: true, style }); expect(moverlay(modal).style.width).toEqual('75px'); }); it('supports overriding style on the modal overlay', () => { const style = { overlay: { position: 'static' } }; - const modal = renderModal({ isOpen: true, style: style }); + const modal = renderModal({ isOpen: true, style }); expect(moverlay(modal).style.position).toEqual('static'); }); it('supports overriding the default styles', () => { const previousStyle = Modal.defaultStyles.content.position; - // Just in case the default style is already relative, check that we can change it + // 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 }); - expect(modal.portal.refs.content.style.position).toEqual(newStyle); + expect(modal.portal.content.style.position).toEqual(newStyle); Modal.defaultStyles.content.position = previousStyle; }); }); diff --git a/specs/helper.js b/specs/helper.js index e34bf37a..27803ebb 100644 --- a/specs/helper.js +++ b/specs/helper.js @@ -44,7 +44,7 @@ const getModalAttribute = component => (instance, attr) => * @return {DOMElement} */ const modalComponent = component => instance => - instance.portal.refs[component]; + instance.portal[component]; /** * Returns the modal content.