Created
December 19, 2014 20:53
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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