Skip to content

Commit ea827eb

Browse files
committed
Merge pull request facebook#4561 from djrodgerspryor/css_transition_group_robust_cleanup
Robust animation-end handling in ReactCSSTransitionGroup
2 parents b88592a + 6bd6ef2 commit ea827eb

File tree

4 files changed

+101
-47
lines changed

4 files changed

+101
-47
lines changed

docs/docs/10.1-animation.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ var TodoList = React.createClass({
4444
return (
4545
<div>
4646
<button onClick={this.handleAdd}>Add Item</button>
47-
<ReactCSSTransitionGroup transitionName="example">
47+
<ReactCSSTransitionGroup transitionName="example" transitionEnterTimeout={500} transitionLeaveTimeout={300} >
4848
{items}
4949
</ReactCSSTransitionGroup>
5050
</div>
@@ -67,31 +67,29 @@ You can use these classes to trigger a CSS animation or transition. For example,
6767

6868
.example-enter.example-enter-active {
6969
opacity: 1;
70-
transition: opacity .5s ease-in;
70+
transition: opacity 500ms ease-in;
7171
}
72-
```
73-
74-
You'll notice that when you try to remove an item `ReactCSSTransitionGroup` keeps it in the DOM. If you're using an unminified build of React with add-ons you'll see a warning that React was expecting an animation or transition to occur. That's because `ReactCSSTransitionGroup` keeps your DOM elements on the page until the animation completes. Try adding this CSS:
7572

76-
```css
7773
.example-leave {
7874
opacity: 1;
7975
}
8076

8177
.example-leave.example-leave-active {
8278
opacity: 0.01;
83-
transition: opacity .5s ease-in;
79+
transition: opacity 300ms ease-in;
8480
}
8581
```
8682

83+
You'll notice that animation durations need to be specified in both the CSS and the render method; this tells React when to remove the animation classes from the element and -- if it's leaving -- when to remove the element from the DOM.
84+
8785
### Animate Initial Mounting
8886

8987
`ReactCSSTransitionGroup` provides the optional prop `transitionAppear`, to add an extra transition phase at the initial mount of the component. There is generally no transition phase at the initial mount as the default value of `transitionAppear` is `false`. The following is an example which passes the prop `transitionAppear` with the value `true`.
9088

9189
```javascript{3-5}
9290
render: function() {
9391
return (
94-
<ReactCSSTransitionGroup transitionName="example" transitionAppear={true}>
92+
<ReactCSSTransitionGroup transitionName="example" transitionAppear={true} transitionAppearTimeout={500}>
9593
<h1>Fading at Initial Mount</h1>
9694
</ReactCSSTransitionGroup>
9795
);
@@ -184,7 +182,7 @@ var ImageCarousel = React.createClass({
184182
render: function() {
185183
return (
186184
<div>
187-
<ReactCSSTransitionGroup transitionName="carousel">
185+
<ReactCSSTransitionGroup transitionName="carousel" transitionEnterTimeout={300} transitionLeaveTimeout={300}>
188186
<img src={this.props.imageSrc} key={this.props.imageSrc} />
189187
</ReactCSSTransitionGroup>
190188
</div>

src/addons/transitions/ReactCSSTransitionGroup.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,31 @@ var ReactCSSTransitionGroupChild = React.createFactory(
2323
require('ReactCSSTransitionGroupChild')
2424
);
2525

26+
function createTransitionTimeoutPropValidator(transitionType) {
27+
var timeoutPropName = 'transition' + transitionType + 'Timeout';
28+
var enabledPropName = 'transition' + transitionType;
29+
30+
return function(props) {
31+
// If the transition is enabled
32+
if (props[enabledPropName]) {
33+
// If no timeout duration is provided
34+
if (!props[timeoutPropName]) {
35+
return new Error(
36+
timeoutPropName + ' wasn\'t supplied to ReactCSSTransitionGroup: ' +
37+
'this can cause unreliable animations and won\'t be supported in ' +
38+
'a future version of React. See ' +
39+
'https://fb.me/react-animation-transition-group-timeout for more ' +
40+
'information.'
41+
);
42+
43+
// If the duration isn't a number
44+
} else if (typeof props[timeoutPropName] !== 'number') {
45+
return new Error(timeoutPropName + ' must be a number (in milliseconds)');
46+
}
47+
}
48+
};
49+
}
50+
2651
var ReactCSSTransitionGroup = React.createClass({
2752
displayName: 'ReactCSSTransitionGroup',
2853

@@ -43,9 +68,13 @@ var ReactCSSTransitionGroup = React.createClass({
4368
appearActive: React.PropTypes.string,
4469
}),
4570
]).isRequired,
71+
4672
transitionAppear: React.PropTypes.bool,
4773
transitionEnter: React.PropTypes.bool,
4874
transitionLeave: React.PropTypes.bool,
75+
transitionAppearTimeout: createTransitionTimeoutPropValidator('Appear'),
76+
transitionEnterTimeout: createTransitionTimeoutPropValidator('Enter'),
77+
transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave'),
4978
},
5079

5180
getDefaultProps: function() {
@@ -66,6 +95,9 @@ var ReactCSSTransitionGroup = React.createClass({
6695
appear: this.props.transitionAppear,
6796
enter: this.props.transitionEnter,
6897
leave: this.props.transitionLeave,
98+
appearTimeout: this.props.transitionAppearTimeout,
99+
enterTimeout: this.props.transitionEnterTimeout,
100+
leaveTimeout: this.props.transitionLeaveTimeout,
69101
},
70102
child
71103
);

src/addons/transitions/ReactCSSTransitionGroupChild.js

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,35 +19,31 @@ var CSSCore = require('CSSCore');
1919
var ReactTransitionEvents = require('ReactTransitionEvents');
2020

2121
var onlyChild = require('onlyChild');
22-
var warning = require('warning');
2322

2423
// We don't remove the element from the DOM until we receive an animationend or
2524
// transitionend event. If the user screws up and forgets to add an animation
2625
// their node will be stuck in the DOM forever, so we detect if an animation
2726
// does not start and if it doesn't, we just call the end listener immediately.
2827
var TICK = 17;
29-
var NO_EVENT_TIMEOUT = 5000;
30-
31-
var noEventListener = null;
32-
33-
34-
if (__DEV__) {
35-
noEventListener = function() {
36-
warning(
37-
false,
38-
'transition(): tried to perform an animation without ' +
39-
'an animationend or transitionend event after timeout (' +
40-
'%sms). You should either disable this ' +
41-
'transition in JS or add a CSS animation/transition.',
42-
NO_EVENT_TIMEOUT
43-
);
44-
};
45-
}
4628

4729
var ReactCSSTransitionGroupChild = React.createClass({
4830
displayName: 'ReactCSSTransitionGroupChild',
4931

50-
transition: function(animationType, finishCallback) {
32+
propTypes: {
33+
name: React.PropTypes.string.isRequired,
34+
35+
// Once we require timeouts to be specified, we can remove the
36+
// boolean flags (appear etc.) and just accept a number
37+
// or a bool for the timeout flags (appearTimeout etc.)
38+
appear: React.PropTypes.bool,
39+
enter: React.PropTypes.bool,
40+
leave: React.PropTypes.bool,
41+
appearTimeout: React.PropTypes.number,
42+
enterTimeout: React.PropTypes.number,
43+
leaveTimeout: React.PropTypes.number,
44+
},
45+
46+
transition: function(animationType, finishCallback, userSpecifiedDelay) {
5147
var node = ReactDOM.findDOMNode(this);
5248

5349
if (!node) {
@@ -59,16 +55,14 @@ var ReactCSSTransitionGroupChild = React.createClass({
5955

6056
var className = this.props.name[animationType] || this.props.name + '-' + animationType;
6157
var activeClassName = this.props.name[animationType + 'Active'] || className + '-active';
62-
63-
var noEventTimeout = null;
58+
var timeout = null;
6459

6560
var endListener = function(e) {
6661
if (e && e.target !== node) {
6762
return;
6863
}
69-
if (__DEV__) {
70-
clearTimeout(noEventTimeout);
71-
}
64+
65+
clearTimeout(timeout);
7266

7367
CSSCore.removeClass(node, className);
7468
CSSCore.removeClass(node, activeClassName);
@@ -82,15 +76,18 @@ var ReactCSSTransitionGroupChild = React.createClass({
8276
}
8377
};
8478

85-
ReactTransitionEvents.addEndEventListener(node, endListener);
86-
8779
CSSCore.addClass(node, className);
8880

8981
// Need to do this to actually trigger a transition.
9082
this.queueClass(activeClassName);
9183

92-
if (__DEV__) {
93-
noEventTimeout = setTimeout(noEventListener, NO_EVENT_TIMEOUT);
84+
// If the user specified a timeout delay.
85+
if (userSpecifiedDelay) {
86+
// Clean-up the animation after the specified delay
87+
timeout = setTimeout(endListener, userSpecifiedDelay);
88+
} else {
89+
// DEPRECATED: this listener will be removed in a future version of react
90+
ReactTransitionEvents.addEndEventListener(node, endListener);
9491
}
9592
},
9693

@@ -124,23 +121,23 @@ var ReactCSSTransitionGroupChild = React.createClass({
124121

125122
componentWillAppear: function(done) {
126123
if (this.props.appear) {
127-
this.transition('appear', done);
124+
this.transition('appear', done, this.props.appearTimeout);
128125
} else {
129126
done();
130127
}
131128
},
132129

133130
componentWillEnter: function(done) {
134131
if (this.props.enter) {
135-
this.transition('enter', done);
132+
this.transition('enter', done, this.props.enterTimeout);
136133
} else {
137134
done();
138135
}
139136
},
140137

141138
componentWillLeave: function(done) {
142139
if (this.props.leave) {
143-
this.transition('leave', done);
140+
this.transition('leave', done, this.props.leaveTimeout);
144141
} else {
145142
done();
146143
}

src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,29 @@ describe('ReactCSSTransitionGroup', function() {
2929
spyOn(console, 'error');
3030
});
3131

32-
it('should warn after time with no transitionend', function() {
32+
it('should warn if timeouts aren\'t specified', function() {
33+
ReactDOM.render(
34+
<ReactCSSTransitionGroup
35+
transitionName="yolo"
36+
transitionEnter={false}
37+
transitionLeave={true}
38+
>
39+
<span key="one" id="one" />
40+
</ReactCSSTransitionGroup>,
41+
container
42+
);
43+
44+
// Warning about the missing transitionLeaveTimeout prop
45+
expect(console.error.argsForCall.length).toBe(1);
46+
});
47+
48+
it('should clean-up silently after the timeout elapses', function() {
3349
var a = ReactDOM.render(
34-
<ReactCSSTransitionGroup transitionName="yolo">
50+
<ReactCSSTransitionGroup
51+
transitionName="yolo"
52+
transitionEnter={false}
53+
transitionLeaveTimeout={200}
54+
>
3555
<span key="one" id="one" />
3656
</ReactCSSTransitionGroup>,
3757
container
@@ -41,7 +61,11 @@ describe('ReactCSSTransitionGroup', function() {
4161
setTimeout.mock.calls.length = 0;
4262

4363
ReactDOM.render(
44-
<ReactCSSTransitionGroup transitionName="yolo">
64+
<ReactCSSTransitionGroup
65+
transitionName="yolo"
66+
transitionEnter={false}
67+
transitionLeaveTimeout={200}
68+
>
4569
<span key="two" id="two" />
4670
</ReactCSSTransitionGroup>,
4771
container
@@ -53,14 +77,18 @@ describe('ReactCSSTransitionGroup', function() {
5377
// For some reason jst is adding extra setTimeout()s and grunt test isn't,
5478
// so we need to do this disgusting hack.
5579
for (var i = 0; i < setTimeout.mock.calls.length; i++) {
56-
if (setTimeout.mock.calls[i][1] === 5000) {
80+
if (setTimeout.mock.calls[i][1] === 200) {
5781
setTimeout.mock.calls[i][0]();
5882
break;
5983
}
6084
}
6185

62-
expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2);
63-
expect(console.error.argsForCall.length).toBe(1);
86+
// No warnings
87+
expect(console.error.argsForCall.length).toBe(0);
88+
89+
// The leaving child has been removed
90+
expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1);
91+
expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two');
6492
});
6593

6694
it('should keep both sets of DOM nodes around', function() {
@@ -170,5 +198,4 @@ describe('ReactCSSTransitionGroup', function() {
170198
expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1);
171199
expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('one');
172200
});
173-
174201
});

0 commit comments

Comments
 (0)