From 4d25989d2708e7e6f4fd34db75164f0272979ac7 Mon Sep 17 00:00:00 2001 From: Jack Morgan Date: Wed, 29 Jul 2015 12:01:26 -0700 Subject: [PATCH] [added] Inline CSS for modal and overlay as well as props to override. [changed] injectCSS has been changed to a warning message in preperation for a future removal. lib/components/Modal.js [changed] setAppElement method is now optional. Defaults to document.body and now allows for a css selector to be passed in rather than the whole element. --- .gitignore | 1 + .npmignore | 1 + README.md | 62 +++++++++++++++++++++++++++++++++-- examples/basic/app.js | 6 ++-- examples/bootstrap/app.js | 1 - lib/components/Modal.js | 14 +++++--- lib/components/ModalPortal.js | 50 +++++++++++++++++++++++----- lib/helpers/ariaAppHider.js | 10 ++++-- lib/helpers/injectCSS.js | 48 --------------------------- package.json | 9 ++--- specs/Modal.spec.js | 35 ++++++++++++-------- 11 files changed, 148 insertions(+), 89 deletions(-) delete mode 100644 lib/helpers/injectCSS.js diff --git a/.gitignore b/.gitignore index 783821d1..27aa61d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ examples/**/*-bundle.js node_modules/ +.idea/ diff --git a/.npmignore b/.npmignore index 3c28928f..552bfc2d 100644 --- a/.npmignore +++ b/.npmignore @@ -4,3 +4,4 @@ examples karma.conf.js script specs +.idea/ diff --git a/README.md b/README.md index 4b30ca9d..3fad0cfa 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,47 @@ Accessible modal dialog component for React.JS isOpen={bool} onRequestClose={fn} closeTimeoutMS={n} -> + style={customStyle}> +

Modal Content

Etc.

``` +## Styles +Styles are passed as an object with 2 keys, 'overlay' and 'content' like so +```js +{ + overlay : { + 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' + + } +} +``` + +Styles passed to the modal are merged in with the above defaults and applied to their respective elements. +At this time, media queries will need to be handled by the consumer. + +## Examples Inside an app: ```js @@ -23,8 +58,28 @@ var Modal = require('react-modal'); var appElement = document.getElementById('your-app-element'); +/* +By default the modal is anchored to document.body. All of the following overrides are available. + +* element Modal.setAppElement(appElement); -Modal.injectCSS(); + +* query selector - uses the first element found if you pass in a class. +Modal.setAppElement('#your-app-element'); + +*/ + +const customStyles = { + content : { + top : '50%', + left : '50%', + right : 'auto', + bottom : 'auto', + marginRight : '-50%', + transform : 'translate(-50%, -50%)' + } +}; + var App = React.createClass({ @@ -47,7 +102,8 @@ var App = React.createClass({ + style={customStyles} > +

Hello

I am a modal
diff --git a/examples/basic/app.js b/examples/basic/app.js index e7da9d0d..9381eafa 100644 --- a/examples/basic/app.js +++ b/examples/basic/app.js @@ -3,8 +3,7 @@ var Modal = require('../../lib/index'); var appElement = document.getElementById('example'); -Modal.setAppElement(appElement); -Modal.injectCSS(); +Modal.setAppElement('#example'); var App = React.createClass({ @@ -37,8 +36,7 @@ var App = React.createClass({ + onRequestClose={this.handleModalCloseRequest}>

Hello

I am a modal
diff --git a/examples/bootstrap/app.js b/examples/bootstrap/app.js index a20c88de..75dff72b 100644 --- a/examples/bootstrap/app.js +++ b/examples/bootstrap/app.js @@ -4,7 +4,6 @@ var Modal = require('../../lib/index'); var appElement = document.getElementById('example'); Modal.setAppElement(appElement); -Modal.injectCSS(); var App = React.createClass({ diff --git a/lib/components/Modal.js b/lib/components/Modal.js index b790d5a1..37da231a 100644 --- a/lib/components/Modal.js +++ b/lib/components/Modal.js @@ -2,7 +2,6 @@ var React = require('react'); var ExecutionEnvironment = require('react/lib/ExecutionEnvironment'); var ModalPortal = React.createFactory(require('./ModalPortal')); var ariaAppHider = require('../helpers/ariaAppHider'); -var injectCSS = require('../helpers/injectCSS'); var elementClass = require('element-class'); var SafeHTMLElement = ExecutionEnvironment.canUseDOM ? window.HTMLElement : {}; @@ -10,16 +9,23 @@ var SafeHTMLElement = ExecutionEnvironment.canUseDOM ? window.HTMLElement : {}; var Modal = module.exports = React.createClass({ displayName: 'Modal', - statics: { setAppElement: ariaAppHider.setElement, - injectCSS: injectCSS + 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'); + } }, propTypes: { isOpen: React.PropTypes.bool.isRequired, - onRequestClose: React.PropTypes.func, + style : React.PropTypes.shape({ + content: React.PropTypes.object, + overlay: React.PropTypes.object + }), appElement: React.PropTypes.instanceOf(SafeHTMLElement), + onRequestClose: React.PropTypes.func, closeTimeoutMS: React.PropTypes.number, ariaHideApp: React.PropTypes.bool }, diff --git a/lib/components/ModalPortal.js b/lib/components/ModalPortal.js index fb1644af..06ba9e1b 100644 --- a/lib/components/ModalPortal.js +++ b/lib/components/ModalPortal.js @@ -2,7 +2,8 @@ var React = require('react'); var div = React.DOM.div; var focusManager = require('../helpers/focusManager'); var scopeTab = require('../helpers/scopeTab'); -var cx = require('classnames'); +var Assign = require('lodash.assign'); + // so that our CSS is statically analyzable var CLASS_NAMES = { @@ -18,7 +19,31 @@ var CLASS_NAMES = { } }; -var OVERLAY_STYLES = { position: 'fixed', left: 0, right: 0, top: 0, bottom: 0 }; +var defaultStyles = { + overlay : { + 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' + + } +}; function stopPropagation(event) { event.stopPropagation(); @@ -28,6 +53,15 @@ var ModalPortal = module.exports = React.createClass({ displayName: 'ModalPortal', + getDefaultProps: function() { + return { + style: { + overlay : {}, + content : {} + } + } + }, + getInitialState: function() { return { afterOpen: false, @@ -132,27 +166,27 @@ var ModalPortal = module.exports = React.createClass({ return !this.props.isOpen && !this.state.beforeClose; }, - buildClassName: function(which) { + buildClassName: function(which, additional) { var className = CLASS_NAMES[which].base; if (this.state.afterOpen) className += ' '+CLASS_NAMES[which].afterOpen; if (this.state.beforeClose) className += ' '+CLASS_NAMES[which].beforeClose; - return className; + return additional ? className + ' ' + additional : className; }, render: function() { return this.shouldBeClosed() ? div() : ( div({ ref: "overlay", - className: cx(this.buildClassName('overlay'), this.props.overlayClassName), - style: OVERLAY_STYLES, + className: this.buildClassName('overlay', this.props.overlayClassName), + style: Assign({}, defaultStyles.overlay, this.props.style.overlay || {}), onClick: this.handleOverlayClick }, div({ ref: "content", - style: this.props.style, - className: cx(this.buildClassName('content'), this.props.className), + style: Assign({}, defaultStyles.content, this.props.style.content || {}), + className: this.buildClassName('content', this.props.className), tabIndex: "-1", onClick: stopPropagation, onKeyDown: this.handleKeyDown diff --git a/lib/helpers/ariaAppHider.js b/lib/helpers/ariaAppHider.js index 886ab3d1..02ba80eb 100644 --- a/lib/helpers/ariaAppHider.js +++ b/lib/helpers/ariaAppHider.js @@ -1,7 +1,11 @@ -var _element = null; +var _element = document.body; function setElement(element) { - _element = element; + if (typeof element === 'string') { + var el = document.querySelectorAll(element); + element = 'length' in el ? el[0] : el; + } + _element = element || _element; } function hide(appElement) { @@ -27,7 +31,7 @@ function validateElement(appElement) { } function resetForTesting() { - _element = null; + _element = document.body; } exports.toggle = toggle; diff --git a/lib/helpers/injectCSS.js b/lib/helpers/injectCSS.js deleted file mode 100644 index 9defc0e9..00000000 --- a/lib/helpers/injectCSS.js +++ /dev/null @@ -1,48 +0,0 @@ -module.exports = function() { - injectStyle([ - '.ReactModal__Overlay {', - ' background-color: rgba(255, 255, 255, 0.75);', - '}', - '.ReactModal__Content {', - ' position: absolute;', - ' top: 40px;', - ' left: 40px;', - ' right: 40px;', - ' bottom: 40px;', - ' border: 1px solid #ccc;', - ' background: #fff;', - ' overflow: auto;', - ' -webkit-overflow-scrolling: touch;', - ' border-radius: 4px;', - ' outline: none;', - ' padding: 20px;', - '}', - '@media (max-width: 768px) {', - ' .ReactModal__Content {', - ' top: 10px;', - ' left: 10px;', - ' right: 10px;', - ' bottom: 10px;', - ' padding: 10px;', - ' }', - '}' - ].join('\n')); -}; - -function injectStyle(css) { - var style = document.getElementById('rackt-style'); - if (!style) { - style = document.createElement('style'); - style.setAttribute('id', 'rackt-style'); - style.setAttribute("type", "text/css"); - } - - if (style.styleSheet) { - style.styleSheet.cssText = css; - document.body.appendChild(style); - } else { - style.innerHTML = css; - document.head.appendChild(style); - } -} - diff --git a/package.json b/package.json index a8df6610..34b54933 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-modal", - "version": "0.3.0", + "version": "0.4.0", "description": "Accessible modal dialog component for React.JS", "main": "./lib/index", "repository": { @@ -32,6 +32,7 @@ "karma-cli": "0.1.0", "karma-firefox-launcher": "0.1.6", "karma-mocha": "0.2.0", + "karma-safari-launcher": "^0.1.1", "mocha": "2.2.5", "react": ">=0.13.3", "reactify": "^1.1.1", @@ -40,8 +41,8 @@ "webpack-dev-server": "1.10.1" }, "dependencies": { - "classnames": "^2.1.3", - "element-class": "^0.2.0" + "element-class": "^0.2.0", + "lodash.assign": "^3.2.0" }, "peerDependencies": { "react": ">=0.13.3" @@ -60,4 +61,4 @@ "browserify-shim": { "react": "global:React" } -} \ No newline at end of file +} diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js index 2678df8a..eeece65e 100644 --- a/specs/Modal.spec.js +++ b/specs/Modal.spec.js @@ -23,14 +23,6 @@ describe('Modal', function () { unmountModal(); }); - it('throws without an appElement', function() { - var node = document.createElement('div'); - throws(function() { - React.render(React.createElement(Modal, {isOpen: true}), node); - }); - React.unmountComponentAtNode(node); - }); - it('uses the global appElement', function() { var app = document.createElement('div'); var node = document.createElement('div'); @@ -100,30 +92,45 @@ describe('Modal', function () { it('supports custom className', function() { var modal = renderModal({isOpen: true, className: 'myClass'}); - equal(modal.portal.refs.content.getDOMNode().className.contains('myClass'), true); + equal(modal.portal.refs.content.getDOMNode().className.indexOf('myClass') !== -1, true); unmountModal(); }); it('supports overlayClassName', function () { var modal = renderModal({isOpen: true, overlayClassName: 'myOverlayClass'}); - equal(modal.portal.refs.overlay.getDOMNode().className.contains('myOverlayClass'), true); + equal(modal.portal.refs.overlay.getDOMNode().className.indexOf('myOverlayClass') !== -1, true); unmountModal(); }); it('supports adding style to the modal contents', function () { - var modal = renderModal({isOpen: true, style: {width: '20px'}}); + var modal = renderModal({isOpen: true, style: {content: {width: '20px'}}}); equal(modal.portal.refs.content.getDOMNode().style.width, '20px'); }); + it('supports overridding style on the modal contents', function() { + var modal = renderModal({isOpen: true, style: {content: {position: 'static'}}}); + equal(modal.portal.refs.content.getDOMNode().style.position, 'static'); + }); + + it('supports adding style on the modal overlay', function() { + var modal = renderModal({isOpen: true, style: {overlay: {width: '75px'}}}); + equal(modal.portal.refs.overlay.getDOMNode().style.width, '75px'); + }); + + it('supports overridding style on the modal overlay', function() { + var modal = renderModal({isOpen: true, style: {overlay: {position: 'static'}}}); + equal(modal.portal.refs.overlay.getDOMNode().style.position, 'static'); + }); + it('adds class to body when open', function() { var modal = renderModal({isOpen: false}); - equal(document.body.className.contains('ReactModal__Body--open'), false); + equal(document.body.className.indexOf('ReactModal__Body--open') !== -1, false); modal.setProps({ isOpen: true}); - equal(document.body.className.contains('ReactModal__Body--open'), true); + equal(document.body.className.indexOf('ReactModal__Body--open') !== -1, true); modal = renderModal({isOpen: false}); - equal(document.body.className.contains('ReactModal__Body--open'), false); + equal(document.body.className.indexOf('ReactModal__Body--open') !== -1, false); unmountModal(); });