Skip to content

Instantly share code, notes, and snippets.

@gaearon
Created December 19, 2014 20:53
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gaearon/f18a33e3bd58275762f4 to your computer and use it in GitHub Desktop.
Save gaearon/f18a33e3bd58275762f4 to your computer and use it in GitHub Desktop.
TimeoutTransitionGroup with a few tweaks as explained in https://github.com/facebook/react/issues/2292#issuecomment-57945100
'use strict';
/**
* Adapted from https://github.com/Khan/react-components/blob/master/js/timeout-transition-group.jsx
* with the following additions:
*
* - Use BEM-ish modifiers (--enter, --enter--active, --leave, --leave--active)
* - Work better with rAF batching strategy (see https://github.com/facebook/react/issues/2292)
*
* The CSSTransitionGroup component uses the 'transitionend' event, which
* browsers will not send for any number of reasons, including the
* transitioning node not being painted or in an unfocused tab.
*
* This TimeoutTransitionGroup instead uses a user-defined timeout to determine
* when it is a good time to remove the component. Currently there is only one
* timeout specified, but in the future it would be nice to be able to specify
* separate timeouts for enter and leave, in case the timeouts for those
* animations differ. Even nicer would be some sort of inspection of the CSS to
* automatically determine the duration of the animation or transition.
*
* This is adapted from Facebook's CSSTransitionGroup which is in the React
* addons and under the Apache 2.0 License.
*/
var React = require('react'),
ReactTransitionGroup = require('react/lib/ReactTransitionGroup'),
CSSCore = require('react/lib/CSSCore');
var TICK = 17;
/**
* EVENT_NAME_MAP is used to determine which event fired when a
* transition/animation ends, based on the style property used to
* define that event.
*/
var EVENT_NAME_MAP = {
transitionend: {
'transition': 'transitionend',
'WebkitTransition': 'webkitTransitionEnd',
'MozTransition': 'mozTransitionEnd',
'OTransition': 'oTransitionEnd',
'msTransition': 'MSTransitionEnd'
},
animationend: {
'animation': 'animationend',
'WebkitAnimation': 'webkitAnimationEnd',
'MozAnimation': 'mozAnimationEnd',
'OAnimation': 'oAnimationEnd',
'msAnimation': 'MSAnimationEnd'
}
};
var endEvents = [];
(function detectEvents() {
var testEl = document.createElement('div');
var style = testEl.style;
// On some platforms, in particular some releases of Android 4.x, the
// un-prefixed "animation" and "transition" properties are defined on the
// style object but the events that fire will still be prefixed, so we need
// to check if the un-prefixed events are useable, and if not remove them
// from the map
if (!('AnimationEvent' in window)) {
delete EVENT_NAME_MAP.animationend.animation;
}
if (!('TransitionEvent' in window)) {
delete EVENT_NAME_MAP.transitionend.transition;
}
for (var baseEventName in EVENT_NAME_MAP) {
if (EVENT_NAME_MAP.hasOwnProperty(baseEventName)) {
var baseEvents = EVENT_NAME_MAP[baseEventName];
for (var styleName in baseEvents) {
if (styleName in style) {
endEvents.push(baseEvents[styleName]);
break;
}
}
}
}
})();
function animationSupported() {
return endEvents.length !== 0;
}
var TimeoutTransitionGroupChild = React.createClass({
transition: function(animationType, finishCallback) {
var node = this.getDOMNode();
var className = this.props.name + '--' + animationType;
var activeClassName = className + '--active';
var endListener = function() {
// https://github.com/facebook/react/issues/2292
// We differ from original implementation in that we don't want --leave classes
// to be removed here if element is to be removed from DOM anyway, as this
// causes flicker with alternative batching strategies.
// Instead, we only remove `enter` classes in the end callback.
// We will remove `leave` classes only if we're about to `enter` again (see below).
if (animationType === 'enter') {
CSSCore.removeClass(node, className);
CSSCore.removeClass(node, activeClassName);
}
// Usually this optional callback is used for informing an owner of
// a leave animation and telling it to remove the child.
if (finishCallback) {
finishCallback();
}
};
// Remove `leave` class if we're about to `enter` again.
// (See rationale above.)
if (animationType === 'enter') {
CSSCore.removeClass(node, this.props.name + '--leave');
CSSCore.removeClass(node, this.props.name + '--leave--active');
}
if (!animationSupported()) {
endListener();
} else {
if (animationType === 'enter') {
this.animationTimeout = setTimeout(endListener,
this.props.enterTimeout);
} else if (animationType === 'leave') {
this.animationTimeout = setTimeout(endListener,
this.props.leaveTimeout);
}
}
CSSCore.addClass(node, className);
// Need to do this to actually trigger a transition.
this.queueClass(activeClassName);
},
queueClass: function(className) {
this.classNameQueue.push(className);
if (!this.timeout) {
this.timeout = setTimeout(this.flushClassNameQueue, TICK);
}
},
flushClassNameQueue: function() {
if (this.isMounted()) {
var node = this.getDOMNode();
this.classNameQueue.forEach(className => CSSCore.addClass(node, className));
}
this.classNameQueue.length = 0;
this.timeout = null;
},
componentWillMount: function() {
this.classNameQueue = [];
},
componentWillUnmount: function() {
if (this.timeout) {
clearTimeout(this.timeout);
}
if (this.animationTimeout) {
clearTimeout(this.animationTimeout);
}
},
componentWillEnter: function(done) {
if (this.props.enter) {
this.transition('enter', done);
} else {
done();
}
},
componentWillLeave: function(done) {
if (this.props.leave) {
this.transition('leave', done);
} else {
done();
}
},
render: function() {
return React.Children.only(this.props.children);
}
});
var TimeoutTransitionGroup = React.createClass({
propTypes: {
enterTimeout: React.PropTypes.number.isRequired,
leaveTimeout: React.PropTypes.number.isRequired,
transitionName: React.PropTypes.string.isRequired,
transitionEnter: React.PropTypes.bool,
transitionLeave: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
transitionEnter: true,
transitionLeave: true
};
},
_wrapChild: function(child) {
return (
<TimeoutTransitionGroupChild
enterTimeout={this.props.enterTimeout}
leaveTimeout={this.props.leaveTimeout}
name={this.props.transitionName}
enter={this.props.transitionEnter}
leave={this.props.transitionLeave}>
{child}
</TimeoutTransitionGroupChild>
);
},
render: function() {
return (
<ReactTransitionGroup
{...this.props}
childFactory={this._wrapChild} />
);
}
});
module.exports = TimeoutTransitionGroup;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment