Skip to content

Commit 3a0b4da

Browse files
committed
Add transition Component
1 parent 5cf1fd4 commit 3a0b4da

2 files changed

Lines changed: 517 additions & 0 deletions

File tree

src/Transition.js

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
'use strict';
2+
import React from 'react';
3+
import TransitionEvents from './utils/TransitionEvents';
4+
import classnames from 'classnames';
5+
6+
function omit(obj, keys) {
7+
let included = Object.keys(obj).filter( k => keys.indexOf(k) === -1);
8+
let newObj = {};
9+
10+
included.forEach( key => newObj[key] = obj[key] );
11+
return newObj;
12+
}
13+
14+
function ensureTransitionEnd(node, handler, duration){
15+
let fired = false;
16+
let done = e => {
17+
if (!fired) {
18+
fired = true;
19+
handler(e);
20+
}
21+
};
22+
23+
if ( node ) {
24+
TransitionEvents.addEndEventListener(node, done);
25+
setTimeout(done, duration);
26+
} else {
27+
setTimeout(done, 0);
28+
}
29+
}
30+
31+
// reading a dimension prop will cause the browser to recalculate,
32+
// which will let our animations work
33+
let triggerBrowserReflow = node => node.offsetHeight; //eslint-disable-line no-unused-expressions
34+
35+
class Transition extends React.Component {
36+
37+
constructor(props, context){
38+
super(props, context);
39+
40+
this.state = {
41+
in: !props.in,
42+
transitioning: false
43+
};
44+
45+
this.needsTransition = true;
46+
}
47+
48+
componentWillReceiveProps(nextProps) {
49+
if (nextProps.in !== this.props.in) {
50+
this.needsTransition = true;
51+
}
52+
}
53+
54+
componentDidUpdate() {
55+
this.processChild();
56+
}
57+
58+
componentWillMount() {
59+
this._mounted = true;
60+
61+
if (!this.props.transitionAppear) {
62+
this.needsTransition = false;
63+
this.setState({ in: this.props.in });
64+
}
65+
}
66+
67+
componentWillUnmount(){
68+
this._mounted = false;
69+
}
70+
71+
componentDidMount() {
72+
if (this.props.transitionAppear) {
73+
this.processChild();
74+
}
75+
}
76+
77+
processChild(){
78+
let needsTransition = this.needsTransition;
79+
let enter = this.props.in;
80+
81+
if (needsTransition) {
82+
this.needsTransition = false;
83+
this[enter ? 'performEnter' : 'performLeave']();
84+
}
85+
}
86+
87+
performEnter() {
88+
let maybeNode = React.findDOMNode(this);
89+
90+
let enter = node => {
91+
node = this.props.transitioningNode(node) || node;
92+
93+
this.props.onEnter(node);
94+
95+
this.safeSetState({ in: true, transitioning: true, needInitialRender: false }, ()=> {
96+
97+
this.props.onEntering(node);
98+
99+
ensureTransitionEnd(node, () => {
100+
if ( this.state.in ){
101+
this.safeSetState({
102+
transitioning: false
103+
}, () => this.props.onEntered(node));
104+
}
105+
106+
}, this.props.duration);
107+
});
108+
};
109+
110+
if (maybeNode) {
111+
enter(maybeNode);
112+
}
113+
else if (this.props.unmountOnExit) {
114+
this._ensureNode(enter);
115+
}
116+
}
117+
118+
performLeave() {
119+
let node = React.findDOMNode(this);
120+
121+
node = this.props.transitioningNode(node) || node;
122+
123+
this.props.onExit(node);
124+
125+
this.setState({ in: false, transitioning: true }, () => {
126+
this.props.onExiting(node);
127+
128+
ensureTransitionEnd(node, () => {
129+
if ( !this.state.in ){
130+
this.safeSetState({ transitioning: false }, ()=> this.props.onExited(node));
131+
}
132+
}, this.props.duration);
133+
});
134+
}
135+
136+
_ensureNode(callback) {
137+
138+
this.setState({ needInitialRender: true }, ()=> {
139+
let node = React.findDOMNode(this);
140+
141+
triggerBrowserReflow(node);
142+
143+
callback(node);
144+
});
145+
}
146+
147+
safeSetState(newState, cb){
148+
if (this._mounted) {
149+
this.setState(newState, cb);
150+
}
151+
}
152+
153+
render() {
154+
let childProps = omit(this.props, Object.keys(Transition.propTypes).concat('children'));
155+
156+
let child = this.props.children;
157+
let starting = this.state.needInitialRender;
158+
let out = !this.state.in && !this.state.transitioning;
159+
160+
if ( !child || (this.props.unmountOnExit && out && !starting) ){
161+
return null;
162+
}
163+
164+
let classes = '';
165+
166+
// for whatever reason classnames() doesn't actually work here,
167+
// maybe because they aren't always single classes?
168+
if (this.state.in && !this.state.transitioning) {
169+
classes = this.props.enteredClassName;
170+
}
171+
172+
else if (this.state.in && this.state.transitioning) {
173+
classes = this.props.enteringClassName;
174+
}
175+
176+
else if (!this.state.in && !this.state.transitioning) {
177+
classes = this.props.exitedClassName;
178+
}
179+
180+
else if (!this.state.in && this.state.transitioning) {
181+
classes = this.props.exitingClassName;
182+
}
183+
184+
return React.cloneElement(child, {
185+
...childProps,
186+
className: classnames(
187+
child.props.className
188+
, this.props.className
189+
, classes)
190+
});
191+
}
192+
}
193+
194+
Transition.propTypes = {
195+
/**
196+
* Triggers the Enter or Exit animation
197+
*/
198+
in: React.PropTypes.bool,
199+
200+
/**
201+
* Specify whether the transitioning component should be unmounted (removed from the DOM) once the exit animation finishes.
202+
*/
203+
unmountOnExit: React.PropTypes.bool,
204+
205+
/**
206+
* Specify whether transitions should run when the Transition component mounts.
207+
*/
208+
transitionAppear: React.PropTypes.bool,
209+
210+
/**
211+
* Provide the durration of the animation in milliseconds, used to ensure that finishing callbacks are fired even if the
212+
* original browser transition end events are canceled.
213+
*/
214+
duration: React.PropTypes.number,
215+
216+
/**
217+
* A css class or classes applied once the Component has exited.
218+
*/
219+
exitedClassName: React.PropTypes.string,
220+
/**
221+
* A css class or classes applied while the Component is exiting.
222+
*/
223+
exitingClassName: React.PropTypes.string,
224+
/**
225+
* A css class or classes applied once the Component has entered.
226+
*/
227+
enteredClassName: React.PropTypes.string,
228+
/**
229+
* A css class or classes applied while the Component is entering.
230+
*/
231+
enteringClassName: React.PropTypes.string,
232+
233+
/**
234+
* A function that returns the DOM node to animate. This Node will have the transition classes applied to it.
235+
* When left out, the Component will use its immediate child.
236+
*
237+
* @private
238+
*/
239+
transitioningNode: React.PropTypes.func,
240+
241+
/**
242+
* A callback fired just before the "entering" classes are applied
243+
*/
244+
onEnter: React.PropTypes.func,
245+
/**
246+
* A callback fired just after the "entering" classes are applied
247+
*/
248+
onEntering: React.PropTypes.func,
249+
/**
250+
* A callback fired after "enter" classes are applied
251+
*/
252+
onEntered: React.PropTypes.func,
253+
/**
254+
* A callback fired after "exiting" classes are applied
255+
*/
256+
onExit: React.PropTypes.func,
257+
/**
258+
* A callback fired after "exiting" classes are applied
259+
*/
260+
onExiting: React.PropTypes.func,
261+
/**
262+
* A callback fired after "exit" classes are applied
263+
*/
264+
onExited: React.PropTypes.func
265+
};
266+
267+
// name the function so it is clearer in the documentation
268+
const noop = ()=>{};
269+
270+
Transition.defaultProps = {
271+
in: false,
272+
duration: 300,
273+
unmountOnExit: false,
274+
transitionAppear: false,
275+
transitioningNode: noop,
276+
277+
onEnter: noop,
278+
onEntering: noop,
279+
onEntered: noop,
280+
281+
onExit: noop,
282+
onExiting: noop,
283+
onExited: noop
284+
};
285+
286+
export default Transition;

0 commit comments

Comments
 (0)