├── .npmignore ├── .gitignore ├── gulpfile.js ├── assets └── modal.css ├── example ├── browser.js └── style.css ├── package.json ├── README.md └── src ├── index.js └── TimeoutTransitionGroup.js /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var react = require('gulp-react'); 3 | 4 | gulp.task('build', function () { 5 | return gulp.src('./src/**') 6 | .pipe(react({ 7 | harmony: true 8 | })) 9 | .pipe(gulp.dest('./lib')); 10 | }); 11 | 12 | gulp.task('default', ['build']); 13 | -------------------------------------------------------------------------------- /assets/modal.css: -------------------------------------------------------------------------------- 1 | .modal-dialog { 2 | display: inline-block; 3 | height: auto; 4 | max-width: 630px; 5 | outline: none; 6 | overflow: hidden; 7 | padding: 100px 80px; 8 | vertical-align: middle; 9 | width: 25%; 10 | z-index: 2000; 11 | } 12 | 13 | .overlay { 14 | position: fixed; 15 | overflow: auto; 16 | text-align: center; 17 | top: 0; 18 | left: 0; 19 | bottom: 0; 20 | right: 0; 21 | display: block; 22 | z-index: 900; 23 | background-color: rgba(255,255,255,0.97); 24 | width: 100%; 25 | padding: 0; 26 | margin: 0; 27 | border: 0; 28 | } 29 | 30 | .overlay:before { 31 | content: ''; 32 | display: inline-block; 33 | height: 80%; 34 | vertical-align: middle; 35 | margin-right: -0.25em; 36 | } 37 | -------------------------------------------------------------------------------- /example/browser.js: -------------------------------------------------------------------------------- 1 | var insertCSS = require('insert-css'); 2 | var domReady = require('domready'); 3 | var domify = require('domify'); 4 | var React = require('react') 5 | var Modal = require('../src/index'); 6 | 7 | var fs = require('fs'); 8 | var style = fs.readFileSync(__dirname+'/style.css', 'utf8'); 9 | insertCSS(style); 10 | 11 | var SomePage = React.createClass({ 12 | getInitialState: function() { 13 | return { showDialog: false }; 14 | }, 15 | showDialog: function() { 16 | this.setState({showDialog: !this.state.showDialog}); 17 | }, 18 | closeDialog: function() { 19 | this.setState({showDialog: false}); 20 | }, 21 | render: function() { 22 | if (this.state.showDialog) { 23 | node = ( 24 |

Plain old Modal

25 | 26 |
) 27 | } else { 28 | node = null 29 | } 30 | 31 | return
32 | 33 | {node} 34 |
35 | } 36 | }); 37 | 38 | domReady(function () { 39 | document.body.appendChild(domify('
')); 40 | React.render(, document.querySelector('.container')); 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-modal-component", 3 | "version": "1.0.9", 4 | "description": "Yet another modal dialog built on react but with a simpler api and support for react-style animations", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "prepublish": "gulp build", 9 | "example": "beefy example/browser.js -- -t [reactify --es6] -t brfs" 10 | }, 11 | "author": "vegetableman ", 12 | "homepage": "https://github.com/vegetableman/react-modal-component", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "beefy": "^2.1.3", 16 | "brfs": "^1.4.0", 17 | "browserify": "^9.0.8", 18 | "domify": "^1.3.3", 19 | "domready": "^1.0.7", 20 | "gulp": "^3.8.11", 21 | "gulp-react": "^3.0.1", 22 | "insert-css": "^0.2.0", 23 | "reactify": "^1.1.0" 24 | }, 25 | "dependencies": { 26 | "attach-dom-events": "^1.0.0", 27 | "react": "^0.13.1", 28 | "select-parent": "^1.0.1" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/vegetableman/react-modal-component.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/vegetableman/react-modal-component/issues", 36 | "email": "vegetablebot@gmail.com" 37 | }, 38 | "keywords": [ 39 | "react", 40 | "modal", 41 | "dialog", 42 | "transition", 43 | "react-modal" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: helvetica; 3 | 4 | } 5 | 6 | .zoom-appear { 7 | -webkit-transform: scale(0.7); 8 | -moz-transform: scale(0.7); 9 | -ms-transform: scale(0.7); 10 | transform: scale(0.7); 11 | opacity: 0; 12 | -webkit-transition: all 0.3s; 13 | -moz-transition: all 0.3s; 14 | transition: all 0.3s; 15 | } 16 | 17 | .zoom-appear.zoom-appear-active { 18 | -webkit-transform: scale(1); 19 | -moz-transform: scale(1); 20 | -ms-transform: scale(1); 21 | transform: scale(1); 22 | opacity: 1; 23 | } 24 | 25 | .zoom-leave { 26 | -webkit-transform: scale(1); 27 | -moz-transform: scale(1); 28 | -ms-transform: scale(1); 29 | transform: scale(1); 30 | -webkit-transition: all 0.3s; 31 | -moz-transition: all 0.3s; 32 | transition: all 0.3s; 33 | opacity: 1; 34 | } 35 | 36 | .zoom-leave.zoom-leave-active { 37 | -webkit-transform: scale(0.7); 38 | -moz-transform: scale(0.7); 39 | -ms-transform: scale(0.7); 40 | transform: scale(0.7); 41 | opacity: 0; 42 | } 43 | 44 | .modal-dialog { 45 | box-shadow: 0 0px 15px rgba(0,0,0,.5); 46 | display: inline-block; 47 | height: auto; 48 | max-width: 630px; 49 | outline: none; 50 | overflow: hidden; 51 | padding: 100px 80px; 52 | text-align: center; 53 | vertical-align: middle; 54 | width: 25%; 55 | z-index: 2000; 56 | } 57 | 58 | .overlay { 59 | position: fixed; 60 | overflow: auto; 61 | text-align: center; 62 | top: 0; 63 | left: 0; 64 | bottom: 0; 65 | right: 0; 66 | display: block; 67 | z-index: 900; 68 | background-color: rgba(255,255,255,0.97); 69 | width: 100%; 70 | padding: 0; 71 | margin: 0; 72 | border: 0; 73 | } 74 | 75 | .overlay:before { 76 | content: ''; 77 | display: inline-block; 78 | height: 80%; 79 | vertical-align: middle; 80 | margin-right: -0.25em; 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-modal-component 2 | Yet another modal dialog built on react but with a simpler api and supports react-style animations. Inspired by https://github.com/rackt/react-modal. 3 | 4 | ## Example Usage 5 | 6 | ```css 7 | 8 | .fade-appear { 9 | opacity: 0; 10 | transition: opacity .5s ease-in; 11 | } 12 | 13 | .fade-appear.fade-appear-active { 14 | opacity: 1; 15 | } 16 | 17 | .fade-leave { 18 | opacity: 1; 19 | transition: opacity .5s ease-in; 20 | } 21 | 22 | .fade-leave.fade-leave-active { 23 | opacity: 0; 24 | } 25 | 26 | ``` 27 | 28 | ```js 29 | var Modal = require('react-modal-component'); 30 | 31 | var Component = React.createClass({ 32 | getInitialState: function() { 33 | return { showModal: false }; 34 | }, 35 | openModal: function() { 36 | this.setState({showModal: true}); 37 | }, 38 | closeModal: function() { 39 | this.setState({showModal: false}); 40 | }, 41 | render: function() { 42 | var node = null; 43 | 44 | if (this.state.showModal) { 45 | node = ( 46 | 47 |

Plain old Modal

48 | 49 |
50 | ) 51 | } 52 | 53 | return 54 | ( 55 |
56 | 57 | {node} 58 |
59 | ) 60 | } 61 | }); 62 | 63 | ``` 64 | 65 | ## Installation 66 | 67 | `npm install react-modal-component --save` 68 | 69 | optional :- 70 | 71 | Use [modal.css](./assets/modal.css) included in this repo derived from [medium](https://medium.com) to support a responsive modal dialog. 72 | 73 | ## API 74 | 75 | #### Modal(props) 76 | 77 | Type: React Component 78 | 79 | Basic modal. 80 | 81 | ### props.className 82 | 83 | Class name for the modal. (default: `.modal-dialog`) 84 | 85 | ### props.overlay 86 | 87 | Class name for the overlay/backdrop. (default: `.overlay`) 88 | 89 | ### props.appendTo 90 | 91 | DOM node where the modal is appended. (default: `document.body`) 92 | 93 | #### props.transitionName 94 | 95 | Transition name to base the animation on. 96 | 97 | ### props.close 98 | 99 | Function to call to close the dialog. Required to support props `closeOnEsc` and `closeOnOutsideClick`. 100 | 101 | ### props.closeOnEsc 102 | 103 | Boolean value to support closing of dialog on Esc. (default: `false`) 104 | 105 | ### props.closeOnOutsideClick 106 | 107 | Boolean value to support closing of dialog on clicking outside the dialog. (default: `false`) 108 | 109 | ### props.enterTimeout 110 | 111 | (see below) 112 | 113 | ### props.leaveTimeout 114 | 115 | (see below) 116 | 117 | 118 | ## Additional Information: 119 | 120 | The CSSTransitionGroup component uses the ```transitionend``` event, which browsers will not send for any number of reasons, including the 121 | transitioning node not being painted or in an unfocused tab. 122 | 123 | This component supports a variant of [TimeoutTransitionGroup](https://github.com/Khan/react-components/blob/master/js/timeout-transition-group.jsx) to define a user-defined timeout to determine 124 | when it is a good time to remove the component. Note:- It's modified to support ```enterTimeout``` for ```appear``` transition as well. 125 | 126 | ## Todo 127 | 128 | * Support server rendering. 129 | 130 | ## Example 131 | 132 | To run the example: 133 | 134 | `npm install` 135 | 136 | `npm run example` 137 | 138 | ## License 139 | MIT 140 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TimeoutTransitionGroup = require('./TimeoutTransitionGroup'); 3 | var domEvents = require('attach-dom-events'); 4 | var selectParent = require('select-parent'); 5 | 6 | attachEvents = domEvents.on; 7 | detachEvents = domEvents.off; 8 | 9 | var PropTypes = React.PropTypes; 10 | var ReactTransitionGroup = React.addons.TransitionGroup; 11 | var validateClosePropTypes = function(props, propName, componentName) { 12 | var propValue = props[propName]; 13 | if (propValue != null && typeof propValue !== 'boolean') { 14 | return new Error( 15 | 'Expected a boolean for ' + propName + ' in ' + 16 | componentName + '.' 17 | ); 18 | } 19 | if(propValue && !props.close) { 20 | return new Error( 21 | 'Expected a function for prop `close` in ' + 22 | componentName + ', since prop `'+ propName + '` is set true.' 23 | ); 24 | } 25 | }; 26 | 27 | var Modal = React.createClass({ 28 | displayName: 'Modal', 29 | 30 | propTypes: { 31 | appendTo: PropTypes.object, 32 | overlay: PropTypes.string, 33 | className: PropTypes.string, 34 | transitionName: PropTypes.string, 35 | transitionEnter: PropTypes.bool, 36 | transitionLeave: PropTypes.bool, 37 | transitionAppear: PropTypes.bool, 38 | enterTimeout: PropTypes.number, 39 | leaveTimeout: PropTypes.number, 40 | close: PropTypes.func, 41 | closeOnEsc: validateClosePropTypes, 42 | closeOnOutsideClick: validateClosePropTypes 43 | }, 44 | 45 | getDefaultProps: function() { 46 | return { 47 | appendTo: document.body, 48 | overlay: 'overlay', 49 | className: 'modal-dialog', 50 | transitionAppear: true, 51 | transitionEnter: true, 52 | transitionLeave: true, 53 | closeOnEsc: false, 54 | closeOnOutsideClick: false 55 | }; 56 | }, 57 | 58 | render: function() { 59 | return null; 60 | }, 61 | 62 | componentDidMount: function() { 63 | this.node = document.createElement('div'); 64 | this.node.className = this.props.overlay; 65 | this.props.appendTo.appendChild(this.node); 66 | React.render(<_Modal {...this.props}/>, this.node); 67 | if (this.props.closeOnOutsideClick) { 68 | attachEvents(this.node, { 69 | 'click': this._closeOnOutsideClick 70 | }); 71 | } 72 | if (this.props.closeOnEsc) { 73 | attachEvents(document.body, { 74 | 'keyup': this._closeOnEsc 75 | }); 76 | } 77 | }, 78 | 79 | _closeOnEsc: function(e) { 80 | if (e.keyCode === 27 && this.props.close) { 81 | this.props.close(); 82 | } 83 | }, 84 | 85 | _closeOnOutsideClick: function(e) { 86 | if (!e.target.classList.contains(this.props.className) && 87 | !selectParent('.'+ this.props.className, e.target) && 88 | this.props.close) { 89 | this.props.close(); 90 | } 91 | }, 92 | 93 | onTransitionEnd: function() { 94 | React.unmountComponentAtNode(this.node); 95 | document.body.removeChild(this.node); 96 | if (this.props.closeOnOutsideClick) { 97 | detachEvents(this.node, { 98 | 'click': this._closeOnOutsideClick 99 | }); 100 | } 101 | if (this.props.closeOnEsc) { 102 | detachEvents(document.body, { 103 | 'keyup': this._closeOnEsc 104 | }); 105 | } 106 | this.node = null; 107 | }, 108 | 109 | componentWillUnmount: function() { 110 | if (this.props.transitionName) { 111 | React.render(<_Modal {...this.props} children={null} onTransitionEnd={this.onTransitionEnd}/>, 112 | this.node); 113 | } else { 114 | this.onTransitionEnd(); 115 | } 116 | } 117 | }); 118 | 119 | 120 | var _Modal = React.createClass({ 121 | 122 | render: function() { 123 | var {appendTo, overlay, className, children, ...other} = this.props, 124 | key = 'modal-'+ Math.random(); 125 | 126 | if (children) { 127 | node = (
128 | {children} 129 |
) 130 | } else { 131 | node = 132 | } 133 | 134 | return 135 | {node} 136 | 137 | } 138 | }); 139 | 140 | 141 | module.exports = Modal; 142 | -------------------------------------------------------------------------------- /src/TimeoutTransitionGroup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The CSSTransitionGroup component uses the 'transitionend' event, which 3 | * browsers will not send for any number of reasons, including the 4 | * transitioning node not being painted or in an unfocused tab. 5 | * 6 | * This TimeoutTransitionGroup instead uses a user-defined timeout to determine 7 | * when it is a good time to remove the component. Currently there is only one 8 | * timeout specified, but in the future it would be nice to be able to specify 9 | * separate timeouts for enter and leave, in case the timeouts for those 10 | * animations differ. Even nicer would be some sort of inspection of the CSS to 11 | * automatically determine the duration of the animation or transition. 12 | * 13 | * This is adapted from Facebook's CSSTransitionGroup which is in the React 14 | * addons and under the Apache 2.0 License. 15 | */ 16 | 17 | var React = require('react/addons'); 18 | var ReactTransitionGroup = React.addons.TransitionGroup; 19 | var TICK = 17; 20 | 21 | /** 22 | * EVENT_NAME_MAP is used to determine which event fired when a 23 | * transition/animation ends, based on the style property used to 24 | * define that event. 25 | */ 26 | var EVENT_NAME_MAP = { 27 | transitionend: { 28 | 'transition': 'transitionend', 29 | 'WebkitTransition': 'webkitTransitionEnd', 30 | 'MozTransition': 'mozTransitionEnd', 31 | 'OTransition': 'oTransitionEnd', 32 | 'msTransition': 'MSTransitionEnd' 33 | }, 34 | 35 | animationend: { 36 | 'animation': 'animationend', 37 | 'WebkitAnimation': 'webkitAnimationEnd', 38 | 'MozAnimation': 'mozAnimationEnd', 39 | 'OAnimation': 'oAnimationEnd', 40 | 'msAnimation': 'MSAnimationEnd' 41 | } 42 | }; 43 | 44 | var endEvents = []; 45 | 46 | (function detectEvents() { 47 | if (typeof window === "undefined") { 48 | return; 49 | } 50 | 51 | var testEl = document.createElement('div'); 52 | var style = testEl.style; 53 | 54 | // On some platforms, in particular some releases of Android 4.x, the 55 | // un-prefixed "animation" and "transition" properties are defined on the 56 | // style object but the events that fire will still be prefixed, so we need 57 | // to check if the un-prefixed events are useable, and if not remove them 58 | // from the map 59 | if (!('AnimationEvent' in window)) { 60 | delete EVENT_NAME_MAP.animationend.animation; 61 | } 62 | 63 | if (!('TransitionEvent' in window)) { 64 | delete EVENT_NAME_MAP.transitionend.transition; 65 | } 66 | 67 | for (var baseEventName in EVENT_NAME_MAP) { 68 | if (EVENT_NAME_MAP.hasOwnProperty(baseEventName)) { 69 | var baseEvents = EVENT_NAME_MAP[baseEventName]; 70 | for (var styleName in baseEvents) { 71 | if (styleName in style) { 72 | endEvents.push(baseEvents[styleName]); 73 | break; 74 | } 75 | } 76 | 77 | } 78 | } 79 | })(); 80 | 81 | function animationSupported() { 82 | return endEvents.length !== 0; 83 | } 84 | 85 | 86 | function addEventListener(node, eventName, eventListener) { 87 | node.addEventListener(eventName, eventListener, false); 88 | } 89 | 90 | 91 | function removeEventListener(node, eventName, eventListener) { 92 | node.removeEventListener(eventName, eventListener, false); 93 | } 94 | 95 | 96 | /** 97 | * Functions for element class management to replace dependency on jQuery 98 | * addClass, removeClass and hasClass 99 | */ 100 | function addClass(element, className) { 101 | if (element.classList) { 102 | element.classList.add(className); 103 | } else if (!hasClass(element, className)) { 104 | element.className = element.className + ' ' + className; 105 | } 106 | return element; 107 | } 108 | 109 | function removeClass(element, className) { 110 | if (hasClass(element, className)) { 111 | if (element.classList) { 112 | element.classList.remove(className); 113 | } else { 114 | element.className = (' ' + element.className + ' ') 115 | .replace(' ' + className + ' ', ' ').trim(); 116 | } 117 | } 118 | return element; 119 | } 120 | function hasClass(element, className) { 121 | if (element.classList) { 122 | return element.classList.contains(className); 123 | } else { 124 | return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1; 125 | } 126 | } 127 | 128 | var TimeoutTransitionGroupChild = React.createClass({ 129 | statics: { 130 | leaveTranstion: false 131 | }, 132 | transition: function(animationType, finishCallback) { 133 | var node = this.getDOMNode(); 134 | var className = this.props.name + '-' + animationType; 135 | var activeClassName = className + '-active'; 136 | 137 | var endListener = function() { 138 | removeClass(node, className); 139 | removeClass(node, activeClassName); 140 | endEvents.forEach(function(endEvent) { 141 | removeEventListener(node, endEvent, endListener); 142 | }); 143 | // Usually this optional callback is used for informing an owner of 144 | // a leave animation and telling it to remove the child. 145 | finishCallback && finishCallback(); 146 | }; 147 | 148 | if (!animationSupported()) { 149 | endListener(); 150 | } else { 151 | if (this.props.enterTimeout && (animationType === "enter" || animationType === "appear")) { 152 | this.animationTimeout = setTimeout(endListener, 153 | this.props.enterTimeout); 154 | } else if (this.props.leaveTimeout && animationType === "leave") { 155 | this.animationTimeout = setTimeout(endListener, 156 | this.props.leaveTimeout); 157 | } 158 | else { 159 | endEvents.forEach(function(endEvent) { 160 | addEventListener(node, endEvent, endListener); 161 | }); 162 | } 163 | } 164 | 165 | addClass(node, className); 166 | 167 | // Need to do this to actually trigger a transition. 168 | this.queueClass(activeClassName); 169 | }, 170 | 171 | queueClass: function(className) { 172 | this.classNameQueue.push(className); 173 | 174 | if (!this.timeout) { 175 | this.timeout = setTimeout(this.flushClassNameQueue, TICK); 176 | } 177 | }, 178 | 179 | flushClassNameQueue: function() { 180 | if (this.isMounted()) { 181 | this.classNameQueue.forEach(function(name) { 182 | addClass(this.getDOMNode(), name); 183 | }.bind(this)); 184 | } 185 | this.classNameQueue.length = 0; 186 | this.timeout = null; 187 | }, 188 | 189 | componentWillMount: function() { 190 | TimeoutTransitionGroupChild.leaveTransition = false; 191 | this.classNameQueue = []; 192 | }, 193 | 194 | componentWillUnmount: function() { 195 | if (this.timeout) { 196 | clearTimeout(this.timeout); 197 | } 198 | if (this.animationTimeout) { 199 | clearTimeout(this.animationTimeout); 200 | } 201 | }, 202 | 203 | componentWillEnter: function(done) { 204 | if (this.props.enter) { 205 | this.transition('enter', done); 206 | } else { 207 | done(); 208 | } 209 | }, 210 | 211 | componentWillAppear: function(done) { 212 | if (this.props.appear) { 213 | this.transition('appear', done); 214 | } else { 215 | done(); 216 | } 217 | }, 218 | 219 | componentWillLeave: function(done) { 220 | TimeoutTransitionGroupChild.leaveTransition = true 221 | if (this.props.leave) { 222 | this.transition('leave', done); 223 | } else { 224 | done(); 225 | } 226 | }, 227 | 228 | componentDidUpdate: function() { 229 | if(TimeoutTransitionGroupChild.leaveTransition) { 230 | this.props.onTransitionEnd(); 231 | } 232 | }, 233 | 234 | render: function() { 235 | return React.Children.only(this.props.children); 236 | } 237 | }); 238 | 239 | var TimeoutTransitionGroup = React.createClass({ 240 | propTypes: { 241 | enterTimeout: React.PropTypes.number, 242 | leaveTimeout: React.PropTypes.number, 243 | transitionName: React.PropTypes.string, 244 | transitionEnter: React.PropTypes.bool, 245 | transitionLeave: React.PropTypes.bool, 246 | transitionAppear: React.PropTypes.bool 247 | }, 248 | 249 | getDefaultProps: function() { 250 | return { 251 | transitionEnter: true, 252 | transitionLeave: true 253 | }; 254 | }, 255 | 256 | _wrapChild: function(child) { 257 | return ( 258 | 266 | {child} 267 | 268 | ); 269 | }, 270 | 271 | render: function() { 272 | return ( 273 | 276 | ); 277 | } 278 | }); 279 | 280 | module.exports = TimeoutTransitionGroup; 281 | --------------------------------------------------------------------------------