Skip to content

Commit 129cb11

Browse files
Andaristjquense
authored andcommittedMay 9, 2019
fix: issue with dynamically applied classes not being properly removed for reentering items (#499)
* Add failing test for not removing dynamically applied classes * Fixed issue with not removing dynamically applied classes * Clear cached applied classes when removing them * Few stylistic changes suggested at code review * Fix linting errors
1 parent 65cd9f8 commit 129cb11

File tree

3 files changed

+141
-46
lines changed

3 files changed

+141
-46
lines changed
 

‎src/CSSTransition.js

+60-46
Original file line numberDiff line numberDiff line change
@@ -74,71 +74,62 @@ class CSSTransition extends React.Component {
7474
static defaultProps = {
7575
classNames: ''
7676
}
77-
onEnter = (node, appearing) => {
78-
const { className } = this.getClassNames(appearing ? 'appear' : 'enter')
7977

78+
appliedClasses = {
79+
appear: {},
80+
enter: {},
81+
exit: {},
82+
}
83+
84+
onEnter = (node, appearing) => {
8085
this.removeClasses(node, 'exit');
81-
addClass(node, className)
86+
this.addClass(node, appearing ? 'appear' : 'enter', 'base');
8287

8388
if (this.props.onEnter) {
8489
this.props.onEnter(node, appearing)
8590
}
8691
}
8792

8893
onEntering = (node, appearing) => {
89-
const { activeClassName } = this.getClassNames(
90-
appearing ? 'appear' : 'enter'
91-
);
92-
93-
this.reflowAndAddClass(node, activeClassName)
94+
const type = appearing ? 'appear' : 'enter';
95+
this.addClass(node, type, 'active')
9496

9597
if (this.props.onEntering) {
9698
this.props.onEntering(node, appearing)
9799
}
98100
}
99101

100102
onEntered = (node, appearing) => {
101-
const appearClassName = this.getClassNames('appear').doneClassName;
102-
const enterClassName = this.getClassNames('enter').doneClassName;
103-
const doneClassName = appearing
104-
? `${appearClassName} ${enterClassName}`
105-
: enterClassName;
106-
107-
this.removeClasses(node, appearing ? 'appear' : 'enter');
108-
addClass(node, doneClassName);
103+
const type = appearing ? 'appear' : 'enter'
104+
this.removeClasses(node, type);
105+
this.addClass(node, type, 'done');
109106

110107
if (this.props.onEntered) {
111108
this.props.onEntered(node, appearing)
112109
}
113110
}
114111

115112
onExit = (node) => {
116-
const { className } = this.getClassNames('exit')
117-
118113
this.removeClasses(node, 'appear');
119114
this.removeClasses(node, 'enter');
120-
addClass(node, className)
115+
this.addClass(node, 'exit', 'base')
121116

122117
if (this.props.onExit) {
123118
this.props.onExit(node)
124119
}
125120
}
126121

127122
onExiting = (node) => {
128-
const { activeClassName } = this.getClassNames('exit')
129-
130-
this.reflowAndAddClass(node, activeClassName)
123+
this.addClass(node, 'exit', 'active')
131124

132125
if (this.props.onExiting) {
133126
this.props.onExiting(node)
134127
}
135128
}
136129

137130
onExited = (node) => {
138-
const { doneClassName } = this.getClassNames('exit');
139-
140131
this.removeClasses(node, 'exit');
141-
addClass(node, doneClassName);
132+
this.addClass(node, 'exit', 'done');
142133

143134
if (this.props.onExited) {
144135
this.props.onExited(node)
@@ -148,46 +139,69 @@ class CSSTransition extends React.Component {
148139
getClassNames = (type) => {
149140
const { classNames } = this.props;
150141
const isStringClassNames = typeof classNames === 'string';
151-
const prefix = isStringClassNames && classNames ? classNames + '-' : '';
142+
const prefix = isStringClassNames && classNames
143+
? `${classNames}-`
144+
: '';
152145

153-
let className = isStringClassNames ?
154-
prefix + type : classNames[type]
146+
let baseClassName = isStringClassNames
147+
? `${prefix}${type}`
148+
: classNames[type]
155149

156-
let activeClassName = isStringClassNames ?
157-
className + '-active' : classNames[type + 'Active'];
150+
let activeClassName = isStringClassNames
151+
? `${baseClassName}-active`
152+
: classNames[`${type}Active`];
158153

159-
let doneClassName = isStringClassNames ?
160-
className + '-done' : classNames[type + 'Done'];
154+
let doneClassName = isStringClassNames
155+
? `${baseClassName}-done`
156+
: classNames[`${type}Done`];
161157

162158
return {
163-
className,
159+
baseClassName,
164160
activeClassName,
165161
doneClassName
166162
};
167163
}
168164

169-
removeClasses(node, type) {
170-
const { className, activeClassName, doneClassName } = this.getClassNames(type)
171-
className && removeClass(node, className);
172-
activeClassName && removeClass(node, activeClassName);
173-
doneClassName && removeClass(node, doneClassName);
174-
}
165+
addClass(node, type, phase) {
166+
let className = this.getClassNames(type)[`${phase}ClassName`];
167+
168+
if (type === 'appear' && phase === 'done') {
169+
className += ` ${this.getClassNames('enter').doneClassName}`;
170+
}
175171

176-
reflowAndAddClass(node, className) {
177172
// This is for to force a repaint,
178173
// which is necessary in order to transition styles when adding a class name.
179-
if (className) {
174+
if (phase === 'active') {
180175
/* eslint-disable no-unused-expressions */
181176
node && node.scrollTop;
182-
/* eslint-enable no-unused-expressions */
183-
addClass(node, className);
184177
}
178+
179+
this.appliedClasses[type][phase] = className
180+
addClass(node, className)
185181
}
186182

187-
render() {
188-
const props = { ...this.props };
183+
removeClasses(node, type) {
184+
const {
185+
base: baseClassName,
186+
active: activeClassName,
187+
done: doneClassName
188+
} = this.appliedClasses[type]
189+
190+
this.appliedClasses[type] = {};
189191

190-
delete props.classNames;
192+
if (baseClassName) {
193+
removeClass(node, baseClassName);
194+
}
195+
if (activeClassName) {
196+
removeClass(node, activeClassName);
197+
}
198+
if (doneClassName) {
199+
removeClass(node, doneClassName);
200+
}
201+
}
202+
203+
render() {
204+
const { classNames: _, ...props } = this.props;
191205

192206
return (
193207
<Transition

‎test/.eslintrc.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
env:
33
jest: true
4+
es6: true
45
rules:
56
no-require: off
67
global-require: off

‎test/CSSTransition-test.js

+80
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { mount } from 'enzyme';
33

44
import CSSTransition from '../src/CSSTransition';
5+
import TransitionGroup from '../src/TransitionGroup';
56

67
describe('CSSTransition', () => {
78

@@ -334,4 +335,83 @@ describe('CSSTransition', () => {
334335
});
335336
});
336337
});
338+
339+
describe('reentering', () => {
340+
it('should remove dynamically applied classes', done => {
341+
let count = 0;
342+
class Test extends React.Component {
343+
render() {
344+
const { direction, text, ...props } = this.props;
345+
346+
return (
347+
<TransitionGroup
348+
component={null}
349+
childFactory={child =>
350+
React.cloneElement(child, {
351+
classNames: direction
352+
})
353+
}
354+
>
355+
<CSSTransition
356+
key={text}
357+
timeout={100}
358+
{...props}
359+
>
360+
<span>{text}</span>
361+
</CSSTransition>
362+
</TransitionGroup>
363+
)
364+
}
365+
}
366+
367+
const instance = mount(<Test direction="down" text="foo" />)
368+
369+
const rerender = getProps => new Promise(resolve =>
370+
instance.setProps({
371+
onEnter: undefined,
372+
onEntering: undefined,
373+
onEntered: undefined,
374+
onExit: undefined,
375+
onExiting: undefined,
376+
onExited: undefined,
377+
...getProps(resolve)
378+
})
379+
);
380+
381+
Promise.resolve().then(() =>
382+
rerender(resolve => ({
383+
direction: 'up',
384+
text: 'bar',
385+
386+
onEnter(node) {
387+
count++;
388+
expect(node.className).toEqual('up-enter');
389+
},
390+
onEntering(node) {
391+
count++;
392+
expect(node.className).toEqual('up-enter up-enter-active');
393+
resolve()
394+
}
395+
}))
396+
).then(() => {
397+
return rerender(resolve => ({
398+
direction: 'down',
399+
text: 'foo',
400+
401+
onEntering(node) {
402+
count++;
403+
expect(node.className).toEqual('down-enter down-enter-active');
404+
},
405+
onEntered(node) {
406+
count++;
407+
expect(node.className).toEqual('down-enter-done');
408+
resolve();
409+
}
410+
}))
411+
}).then(() => {
412+
expect(count).toEqual(4);
413+
done();
414+
});
415+
});
416+
});
337417
});

0 commit comments

Comments
 (0)
Please sign in to comment.