diff --git a/package.json b/package.json index 2143116c..550fc079 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ }, "dependencies": { "dom-helpers": "^3.4.0", + "fastdom": "^1.0.8", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, diff --git a/src/CSSTransition.js b/src/CSSTransition.js index 9cc2543e..9ffcb3b5 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.js @@ -1,14 +1,9 @@ import * as PropTypes from 'prop-types'; -import addOneClass from 'dom-helpers/class/addClass'; - -import removeOneClass from 'dom-helpers/class/removeClass'; import React from 'react'; import Transition from './Transition'; import { classNamesShape } from './utils/PropTypes'; - -const addClass = (node, classes) => node && classes && classes.split(' ').forEach(c => addOneClass(node, c)); -const removeClass = (node, classes) => node && classes && classes.split(' ').forEach(c => removeOneClass(node, c)); +import { addClass, removeClass } from './utils/ClassHelpers'; /** * A transition component inspired by the excellent @@ -90,7 +85,7 @@ class CSSTransition extends React.Component { appearing ? 'appear' : 'enter' ); - this.reflowAndAddClass(node, activeClassName) + addClass(node, activeClassName, true /* reflow */) if (this.props.onEntering) { this.props.onEntering(node, appearing) @@ -100,7 +95,7 @@ class CSSTransition extends React.Component { onEntered = (node, appearing) => { const appearClassName = this.getClassNames('appear').doneClassName; const enterClassName = this.getClassNames('enter').doneClassName; - const doneClassName = appearing + const doneClassName = (appearing && appearClassName && enterClassName) ? `${appearClassName} ${enterClassName}` : enterClassName; @@ -127,7 +122,7 @@ class CSSTransition extends React.Component { onExiting = (node) => { const { activeClassName } = this.getClassNames('exit') - this.reflowAndAddClass(node, activeClassName) + addClass(node, activeClassName) if (this.props.onExiting) { this.props.onExiting(node) @@ -173,17 +168,6 @@ class CSSTransition extends React.Component { doneClassName && removeClass(node, doneClassName); } - reflowAndAddClass(node, className) { - // This is for to force a repaint, - // which is necessary in order to transition styles when adding a class name. - if (className) { - /* eslint-disable no-unused-expressions */ - node && node.scrollTop; - /* eslint-enable no-unused-expressions */ - addClass(node, className); - } - } - render() { const props = { ...this.props }; diff --git a/src/utils/ClassHelpers.js b/src/utils/ClassHelpers.js new file mode 100644 index 00000000..8913766e --- /dev/null +++ b/src/utils/ClassHelpers.js @@ -0,0 +1,50 @@ +import fastdom from 'fastdom'; +import addOneClass from 'dom-helpers/class/addClass'; +import removeOneClass from 'dom-helpers/class/removeClass'; + +export function addClass(node, classes, reflow) { + mutateClass(node, classes, reflow, addOneClass); +} +export function removeClass(node, classes, reflow) { + mutateClass(node, classes, reflow, removeOneClass); +} + +function mutateClass(node, classes, reflow, fn) { + if (!node) return; + if (classes && typeof classes === 'string') { + const run = () => { + // A reflow is necessary to get the browser to respect the transition. However, it doesn't + // need to be done on every single class change, only when the 'active' class is added. + // Batching reflows allows us to avoid read-write-read context switches, by reading + // (node.scrollTop) completely before we write. + if (reflow) emptyReflow(); + classes.split(' ').forEach(c => fn(node, c)); + } + // If possible, on browsers, batch these mutations as to avoid synchronous layouts. + // But watch out - if we are batching them, and the page is inactive, we can end up + // hitching up the page for a very long time as these callbacks queue up on + // requestAnimationFrame. So only queue them up if the page is visible. + if (process.browser && document.hidden === false) { + // Is this an entering animation where we'll need to force a reflow? + if (reflow) reflowNodes.push(node); + // Schedule the modification for next tick. + fastdom.mutate(run); + } else { + run(); + } + } +} + +const reflowNodes = []; +// Empty the `reflowNodes` array completely and reflow all of them at once. +function emptyReflow() { + let node; + while (node = reflowNodes.pop()) forceReflow(node); +} + +// This is for to force a repaint, +// which is necessary in order to transition styles when adding a class name. +function forceReflow(node) { + /* eslint-disable no-unused-expressions */ + node && node.scrollTop; +} diff --git a/stories/TransitionGroup.js b/stories/TransitionGroup.js index 7e8183ef..888d0037 100644 --- a/stories/TransitionGroup.js +++ b/stories/TransitionGroup.js @@ -12,8 +12,7 @@ storiesOf('Css Transition Group', module) .add('Animates on all', () => ( (