@samitouri / QOS-React-2 / commits / 6bd6ef208b

ReactCSSTransitionGroup timeouts

As discussed in issue 1326 (https://github.com/facebook/react/issues/1326) transitionend events are unreliable; they may not fire because the element is no longer painted, the browser tab is no longer focused or for a range of other reasons. This is particularly harmful during leave transitions since the leaving element will be permanently stuck in the DOM (and possibly visible). The ReactCSSTransitionGroup now requires timeouts to be passed in explicitly for each type of animation. Omitting the timeout duration for a transition now triggers a PropTypes warning with a link to the updated documentation.

Daniel Rodgers-Pryor committed Aug 5, 2015 at 13:04 UTC 6bd6ef208b302879eb38018d4192a054f40c1c8b
4 files changed +101 -47
docs/docs/10.1-animation.md
+7 -9
@@ -44,7 +44,7 @@ var TodoList = React.createClass({
44 return (
45 <div>
46 <button onClick={this.handleAdd}>Add Item</button>
47 - <ReactCSSTransitionGroup transitionName="example">
47 + <ReactCSSTransitionGroup transitionName="example" transitionEnterTimeout={500} transitionLeaveTimeout={300} >
48 {items}
49 </ReactCSSTransitionGroup>
50 </div>
@@ -67,23 +67,21 @@ You can use these classes to trigger a CSS animation or transition. For example,
67
68 .example-enter.example-enter-active {
69 opacity: 1;
70 - transition: opacity .5s ease-in;
70 + transition: opacity 500ms ease-in;
71 }
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:
72
76 -```css
73 .example-leave {
74 opacity: 1;
75 }
76
77 .example-leave.example-leave-active {
78 opacity: 0.01;
83 - transition: opacity .5s ease-in;
79 + transition: opacity 300ms ease-in;
80 }
81 ```
82
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 +
85 ### Animate Initial Mounting
86
87 `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`.
@@ -91,7 +89,7 @@ You'll notice that when you try to remove an item `ReactCSSTransitionGroup` keep
89 ```javascript{3-5}
90 render: function() {
91 return (
94 - <ReactCSSTransitionGroup transitionName="example" transitionAppear={true}>
92 + <ReactCSSTransitionGroup transitionName="example" transitionAppear={true} transitionAppearTimeout={500}>
93 <h1>Fading at Initial Mount</h1>
94 </ReactCSSTransitionGroup>
95 );
@@ -184,7 +182,7 @@ var ImageCarousel = React.createClass({
182 render: function() {
183 return (
184 <div>
187 - <ReactCSSTransitionGroup transitionName="carousel">
185 + <ReactCSSTransitionGroup transitionName="carousel" transitionEnterTimeout={300} transitionLeaveTimeout={300}>
186 <img src={this.props.imageSrc} key={this.props.imageSrc} />
187 </ReactCSSTransitionGroup>
188 </div>
src/addons/transitions/ReactCSSTransitionGroup.js
+32
@@ -23,6 +23,31 @@ var ReactCSSTransitionGroupChild = React.createFactory(
23 require('ReactCSSTransitionGroupChild')
24 );
25
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 +
51 var ReactCSSTransitionGroup = React.createClass({
52 displayName: 'ReactCSSTransitionGroup',
53
@@ -43,9 +68,13 @@ var ReactCSSTransitionGroup = React.createClass({
68 appearActive: React.PropTypes.string,
69 }),
70 ]).isRequired,
71 +
72 transitionAppear: React.PropTypes.bool,
73 transitionEnter: React.PropTypes.bool,
74 transitionLeave: React.PropTypes.bool,
75 + transitionAppearTimeout: createTransitionTimeoutPropValidator('Appear'),
76 + transitionEnterTimeout: createTransitionTimeoutPropValidator('Enter'),
77 + transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave'),
78 },
79
80 getDefaultProps: function() {
@@ -66,6 +95,9 @@ var ReactCSSTransitionGroup = React.createClass({
95 appear: this.props.transitionAppear,
96 enter: this.props.transitionEnter,
97 leave: this.props.transitionLeave,
98 + appearTimeout: this.props.transitionAppearTimeout,
99 + enterTimeout: this.props.transitionEnterTimeout,
100 + leaveTimeout: this.props.transitionLeaveTimeout,
101 },
102 child
103 );
src/addons/transitions/ReactCSSTransitionGroupChild.js
+28 -31
@@ -19,35 +19,31 @@ var CSSCore = require('CSSCore');
19 var ReactTransitionEvents = require('ReactTransitionEvents');
20
21 var onlyChild = require('onlyChild');
22 -var warning = require('warning');
22
23 // We don't remove the element from the DOM until we receive an animationend or
24 // transitionend event. If the user screws up and forgets to add an animation
25 // their node will be stuck in the DOM forever, so we detect if an animation
26 // does not start and if it doesn't, we just call the end listener immediately.
27 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 -}
28
29 var ReactCSSTransitionGroupChild = React.createClass({
30 displayName: 'ReactCSSTransitionGroupChild',
31
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) {
47 var node = ReactDOM.findDOMNode(this);
48
49 if (!node) {
@@ -59,16 +55,14 @@ var ReactCSSTransitionGroupChild = React.createClass({
55
56 var className = this.props.name[animationType] || this.props.name + '-' + animationType;
57 var activeClassName = this.props.name[animationType + 'Active'] || className + '-active';
62 -
63 - var noEventTimeout = null;
58 + var timeout = null;
59
60 var endListener = function(e) {
61 if (e && e.target !== node) {
62 return;
63 }
69 - if (__DEV__) {
70 - clearTimeout(noEventTimeout);
71 - }
64 +
65 + clearTimeout(timeout);
66
67 CSSCore.removeClass(node, className);
68 CSSCore.removeClass(node, activeClassName);
@@ -82,15 +76,18 @@ var ReactCSSTransitionGroupChild = React.createClass({
76 }
77 };
78
85 - ReactTransitionEvents.addEndEventListener(node, endListener);
86 -
79 CSSCore.addClass(node, className);
80
81 // Need to do this to actually trigger a transition.
82 this.queueClass(activeClassName);
83
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);
91 }
92 },
93
@@ -124,7 +121,7 @@ var ReactCSSTransitionGroupChild = React.createClass({
121
122 componentWillAppear: function(done) {
123 if (this.props.appear) {
127 - this.transition('appear', done);
124 + this.transition('appear', done, this.props.appearTimeout);
125 } else {
126 done();
127 }
@@ -132,7 +129,7 @@ var ReactCSSTransitionGroupChild = React.createClass({
129
130 componentWillEnter: function(done) {
131 if (this.props.enter) {
135 - this.transition('enter', done);
132 + this.transition('enter', done, this.props.enterTimeout);
133 } else {
134 done();
135 }
@@ -140,7 +137,7 @@ var ReactCSSTransitionGroupChild = React.createClass({
137
138 componentWillLeave: function(done) {
139 if (this.props.leave) {
143 - this.transition('leave', done);
140 + this.transition('leave', done, this.props.leaveTimeout);
141 } else {
142 done();
143 }
src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js
+34 -7
@@ -29,9 +29,29 @@ describe('ReactCSSTransitionGroup', function() {
29 spyOn(console, 'error');
30 });
31
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() {
49 var a = ReactDOM.render(
34 - <ReactCSSTransitionGroup transitionName="yolo">
50 + <ReactCSSTransitionGroup
51 + transitionName="yolo"
52 + transitionEnter={false}
53 + transitionLeaveTimeout={200}
54 + >
55 <span key="one" id="one" />
56 </ReactCSSTransitionGroup>,
57 container
@@ -41,7 +61,11 @@ describe('ReactCSSTransitionGroup', function() {
61 setTimeout.mock.calls.length = 0;
62
63 ReactDOM.render(
44 - <ReactCSSTransitionGroup transitionName="yolo">
64 + <ReactCSSTransitionGroup
65 + transitionName="yolo"
66 + transitionEnter={false}
67 + transitionLeaveTimeout={200}
68 + >
69 <span key="two" id="two" />
70 </ReactCSSTransitionGroup>,
71 container
@@ -53,14 +77,18 @@ describe('ReactCSSTransitionGroup', function() {
77 // For some reason jst is adding extra setTimeout()s and grunt test isn't,
78 // so we need to do this disgusting hack.
79 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) {
81 setTimeout.mock.calls[i][0]();
82 break;
83 }
84 }
85
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');
92 });
93
94 it('should keep both sets of DOM nodes around', function() {
@@ -170,5 +198,4 @@ describe('ReactCSSTransitionGroup', function() {
198 expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1);
199 expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('one');
200 });
173 -
201 });