Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
},
"dependencies": {
"dom-helpers": "^3.4.0",
"fastdom": "^1.0.8",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
Expand Down
24 changes: 4 additions & 20 deletions src/CSSTransition.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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;

Expand All @@ -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)
Expand Down Expand Up @@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the reflow being removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point is to avoid synchronous layout, which this forces. In my testing it was no longer necessary when using fastdom as the class manipulation happens on another tick.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit concerned about this, bc my previous testing trying to use rAF directly instead of the sync reflow led to animations not running. I wish I had a specific test case to try tho, I don't honestly remember what the cases were. Maybe during interrupts or fast toggling?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't been able to replicate any problems in my testing - ideas?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theoretically, what is ensuring that active classes are being added on a different tick than the initial classes? From what I can tell here, every call to fastdom.mutate adds to the same queue, if the initial and active classes are added within the the rAF time frame (very likely?) they will flush on the same tick.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To follow up on this, we've been using this in prod for 3mo+ without any issue. The forced reflow is unnecessary as fastdom will delay the change until the next animation frame.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The -active classes work as expected, I tested this by adding opacity: 0 to -enter and opacity: 1 to -enter-active. The element faded in, which wouldn't happen if they were added in the same tick.

/* eslint-enable no-unused-expressions */
addClass(node, className);
}
}

render() {
const props = { ...this.props };

Expand Down
50 changes: 50 additions & 0 deletions src/utils/ClassHelpers.js
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 2 additions & 3 deletions stories/TransitionGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ storiesOf('Css Transition Group', module)
.add('Animates on all', () => (
<CSSTransitionGroupFixture
description={`
Should animate when items are added to the list but not when they are
removed or on initial appear
Should animate on appear, add, and remove
`}
appear
items={[ 'Item number: 1' ]}
Expand All @@ -37,7 +36,7 @@ storiesOf('Css Transition Group', module)
.add('Animates on exit', () => (
<CSSTransitionGroupFixture
description={`
Should animate when items are removed to the list but not when they are
Should animate when items are removed from the list but not when they are
added or on initial appear
`}
items={[
Expand Down
14 changes: 14 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5506,6 +5506,16 @@ fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"

fast-memoize@^2.2.7:
version "2.4.0"
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.4.0.tgz#2f79eca41c41112b0b70cf53ac3940e206574648"

fastdom@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/fastdom/-/fastdom-1.0.8.tgz#10f9d36998fd6efae30e529597d788e750c9febb"
dependencies:
strictdom "^1.0.1"

fastparse@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
Expand Down Expand Up @@ -11425,6 +11435,10 @@ strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"

strictdom@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/strictdom/-/strictdom-1.0.1.tgz#189de91649f73d44d59b8432efa68ef9d2659460"

string-length@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
Expand Down