├── .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 |
--------------------------------------------------------------------------------