@samitouri / QOS-React-2 / commits / 80f1590265

Add ReactTransitionGroup

This introduces <ReactTransitionGroup>, a component that works a lot like Angular's ng-animate. The problem we're currently facing is twofold: 1. We don't really have a great convention surrounding CSS transitions in React 2. (harder) we can't animate nodes that are leaving the DOM, as their nodes are instantly destroyed. To solve the first issue I've adopted Angular's convention. It's implemented in ReactTransitionableChild. The second part is what's tricky. To do this I've implemented three modules: - ReactTransitionableChild, which can keep its old children around if they change to null - ReactTransitionKeySet, which can look at a prev and next set of child keys and merge them in a reasonable way - ReactTransitionGroup, which combines ReactTransitionableChild and ReactTransitionKeySet to keep nodes that are leaving the DOM in the DOM until their animations are complete

Pete Hunt committed Sep 3, 2013 at 18:15 UTC 80f1590265e2c721707aef471cca995c4a7567a8
7 files changed +779
src/addons/transitions/ReactTransitionEvents.js new
+84
@@ -0,0 +1,84 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactTransitionEvents
17 + */
18 +
19 +"use strict";
20 +
21 +var ExecutionEnvironment = require('ExecutionEnvironment');
22 +
23 +var EVENT_NAME_MAP = {
24 + 'transition': ['animationend', 'transitionend'],
25 + 'WebkitTransition': ['webkitAnimationEnd', 'webkitTransitionEnd'],
26 + 'MozTransition': ['mozAnimationEnd', 'mozTransitionEnd'],
27 + 'OTransition': ['oAnimationEnd', 'oTransitionEnd'],
28 + 'msTransition': ['MSAnimationEnd', 'MSTransitionEnd']
29 +};
30 +
31 +var endEvents = null;
32 +
33 +function detectEvents() {
34 + var testEl = document.createElement('div');
35 + var style = testEl.style;
36 + for (var styleName in EVENT_NAME_MAP) {
37 + if (styleName in style) {
38 + endEvents = EVENT_NAME_MAP[styleName];
39 + return;
40 + }
41 + }
42 +}
43 +
44 +if (ExecutionEnvironment.canUseDOM) {
45 + detectEvents();
46 +}
47 +
48 +// We use the raw {add|remove}EventListener() call because EventListener
49 +// does not know how to remove event listeners and we really should
50 +// clean up. Also, these events are not triggered in older browsers
51 +// so we should be A-OK here.
52 +
53 +function addEventListener(node, eventName, eventListener) {
54 + node.addEventListener(eventName, eventListener, false);
55 +}
56 +
57 +function removeEventListener(node, eventName, eventListener) {
58 + node.removeEventListener(eventName, eventListener, false);
59 +}
60 +
61 +var ReactTransitionEvents = {
62 + addEndEventListener: function(node, eventListener) {
63 + if (!endEvents) {
64 + // If CSS transitions are not supported, trigger an "end animation"
65 + // event immediately.
66 + window.setTimeout(eventListener, 0);
67 + return;
68 + }
69 + endEvents.forEach(function(endEvent) {
70 + addEventListener(node, endEvent, eventListener);
71 + });
72 + },
73 +
74 + removeEndEventListener: function(node, eventListener) {
75 + if (!endEvents) {
76 + return;
77 + }
78 + endEvents.forEach(function(endEvent) {
79 + removeEventListener(node, endEvent, eventListener);
80 + });
81 + }
82 +};
83 +
84 +module.exports = ReactTransitionEvents;
src/addons/transitions/ReactTransitionGroup.js new
+112
@@ -0,0 +1,112 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactTransitionGroup
17 + */
18 +
19 +"use strict";
20 +
21 +var React = require('React');
22 +var ReactTransitionableChild = require('ReactTransitionableChild');
23 +var ReactTransitionKeySet = require('ReactTransitionKeySet');
24 +
25 +var invariant = require('invariant');
26 +
27 +var ReactTransitionGroupMixin = {
28 + componentWillMount: function() {
29 + // _transitionGroupCurrentKeys stores the union of previous *and* next keys.
30 + // If this were a component we'd store it as state, however, since this must
31 + // be a mixin, we need to keep the result of the union of keys in each
32 + // call to animateChildren() which happens in render(), so we can't
33 + // call setState() in there.
34 + this._transitionGroupCurrentKeys = {};
35 + },
36 +
37 + /**
38 + * Render some children in a transitionable way.
39 + */
40 + renderTransitionableChildren: function(sourceChildren) {
41 + invariant(
42 + this.getTransitionConfig,
43 + 'renderTransitionableChildren(): You must provide a ' +
44 + 'getTransitionConfig() method.'
45 + );
46 +
47 + var children = {};
48 + var childMapping = ReactTransitionKeySet.getChildMapping(sourceChildren);
49 + var transitionConfig = this.getTransitionConfig();
50 + var currentKeys = ReactTransitionKeySet.mergeKeySets(
51 + this._transitionGroupCurrentKeys,
52 + ReactTransitionKeySet.getKeySet(sourceChildren)
53 + );
54 +
55 + for (var key in currentKeys) {
56 + // Here is how we keep the nodes in the DOM. ReactTransitionableChild
57 + // knows how to hold onto its child if it changes to undefined. Here, we
58 + // may look up an old key in the new children, and it may switch to
59 + // undefined. React's reconciler will keep the ReactTransitionableChild
60 + // instance alive such that we can animate it.
61 + if (childMapping[key] || transitionConfig.leave) {
62 + children[key] = ReactTransitionableChild({
63 + name: transitionConfig.name,
64 + enter: transitionConfig.enter,
65 + onDoneLeaving: this._handleDoneLeaving.bind(this, key)
66 + }, childMapping[key]);
67 + }
68 + }
69 +
70 + this._transitionGroupCurrentKeys = currentKeys;
71 +
72 + return children;
73 + },
74 +
75 + _handleDoneLeaving: function(key) {
76 + // When the leave animation finishes, we should blow away the actual DOM
77 + // node.
78 + delete this._transitionGroupCurrentKeys[key];
79 + this.forceUpdate();
80 + }
81 +};
82 +
83 +var ReactTransitionGroup = React.createClass({
84 + mixins: [ReactTransitionGroupMixin],
85 +
86 + getDefaultProps: function() {
87 + return {
88 + transitionEnter: true,
89 + transitionLeave: true,
90 + component: React.DOM.span
91 + };
92 + },
93 +
94 + getTransitionConfig: function() {
95 + return {
96 + name: this.props.transitionName,
97 + enter: this.props.transitionEnter,
98 + leave: this.props.transitionLeave
99 + };
100 + },
101 +
102 + render: function() {
103 + return this.transferPropsTo(
104 + this.props.component(
105 + null,
106 + this.renderTransitionableChildren(this.props.children)
107 + )
108 + );
109 + }
110 +});
111 +
112 +module.exports = ReactTransitionGroup;
src/addons/transitions/ReactTransitionKeySet.js new
+108
@@ -0,0 +1,108 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @typechecks static-only
17 + * @providesModule ReactTransitionKeySet
18 + */
19 +
20 +"use strict";
21 +
22 +var React = require('React');
23 +
24 +var MERGE_KEY_SETS_TAIL_SENTINEL = {};
25 +
26 +var ReactTransitionKeySet = {
27 + /**
28 + * Given `this.props.children`, return an object mapping key to child. Just
29 + * simple syntactic sugar around React.mapChildren().
30 + *
31 + * @param {*} children `this.props.children`
32 + * @return {object} Mapping of key to child
33 + */
34 + getChildMapping: function(children) {
35 + return React.mapChildren(children, function(child) {
36 + return child;
37 + });
38 + },
39 +
40 + /**
41 + * Simple syntactic sugar to get an object with keys of all of `children`.
42 + * Does not have references to the children themselves.
43 + *
44 + * @param {*} children `this.props.children`
45 + * @return {object} Mapping of key to the value "true"
46 + */
47 + getKeySet: function(children) {
48 + return React.mapChildren(children, function() {
49 + return true;
50 + });
51 + },
52 +
53 + /**
54 + * When you're adding or removing children some may be added or removed in the
55 + * same render pass. We want ot show *both* since we want to simultaneously
56 + * animate elements in and out. This function takes a previous set of keys
57 + * and a new set of keys and merges them with its best guess of the correct
58 + * ordering. In the future we may expose some of the utilities in
59 + * ReactMultiChild to make this easy, but for now React itself does not
60 + * directly have this concept of the union of prevChildren and nextChildren
61 + * so we implement it here.
62 + *
63 + * @param {object} prev prev child keys as returned from
64 + * `ReactTransitionKeySet.getKeySet()`.
65 + * @param {object} next next child keys as returned from
66 + * `ReactTransitionKeySet.getKeySet()`.
67 + * @return {object} a key set that contains all keys in `prev` and all keys
68 + * in `next` in a reasonable order.
69 + */
70 + mergeKeySets: function(prev, next) {
71 + var keySet = {};
72 + var prevKeys = Object.keys(prev).concat([MERGE_KEY_SETS_TAIL_SENTINEL]);
73 + var nextKeys = Object.keys(next).concat([MERGE_KEY_SETS_TAIL_SENTINEL]);
74 + var i;
75 + for (i = 0; i < prevKeys.length - 1; i++) {
76 + var prevKey = prevKeys[i];
77 + if (next[prevKey]) {
78 + continue;
79 + }
80 +
81 + // This key is not in the new set. Place it in our
82 + // best guess where it should go. We do this by searching
83 + // for a key after the current one in prevKeys that is
84 + // still in nextKeys, and inserting right before it.
85 + // I know this is O(n^2), but this is not a particularly
86 + // hot code path.
87 + var insertPos = -1;
88 +
89 + for (var j = i + 1; j < prevKeys.length; j++) {
90 + insertPos = nextKeys.indexOf(prevKeys[j]);
91 + if (insertPos >= 0) {
92 + break;
93 + }
94 + }
95 +
96 + // Insert before insertPos
97 + nextKeys.splice(insertPos, 0, prevKey);
98 + }
99 +
100 + for (i = 0; i < nextKeys.length - 1; i++) {
101 + keySet[nextKeys[i]] = true;
102 + }
103 +
104 + return keySet;
105 + }
106 +};
107 +
108 +module.exports = ReactTransitionKeySet;
src/addons/transitions/ReactTransitionableChild.js new
+155
@@ -0,0 +1,155 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactTransitionableChild
17 + */
18 +
19 +"use strict";
20 +
21 +var React = require('React');
22 +var CSSCore = require('CSSCore');
23 +var ReactTransitionEvents = require('ReactTransitionEvents');
24 +
25 +// We don't remove the element from the DOM until we receive an animationend or
26 +// transitionend event. If the user screws up and forgets to add an animation
27 +// their node will be stuck in the DOM forever, so we detect if an animation
28 +// does not start and if it doesn't, we just call the end listener immediately.
29 +var TICK = 17;
30 +var NO_EVENT_TIMEOUT = 5000;
31 +
32 +var noEventListener = null;
33 +
34 +if (__DEV__) {
35 + noEventListener = function() {
36 + console.warn(
37 + 'transition(): tried to perform an animation without ' +
38 + 'an animationend or transitionend event after timeout (' +
39 + NO_EVENT_TIMEOUT + 'ms). You should either disable this ' +
40 + 'transition in JS or add a CSS animation/transition.'
41 + );
42 + };
43 +}
44 +
45 +/**
46 + * This component is simply responsible for watching when its single child
47 + * changes to undefined and animating the old child out. It does this by
48 + * recording its old child in savedChildren when it detects this event is about
49 + * to occur.
50 + */
51 +var ReactTransitionableChild = React.createClass({
52 + /**
53 + * Perform an actual DOM transition. This takes care of a few things:
54 + * - Adding the second CSS class to trigger the transition
55 + * - Listening for the finish event
56 + * - Cleaning up the css (unless noReset is true)
57 + */
58 + transition: function(animationType, noReset, finishCallback) {
59 + var node = this.getDOMNode();
60 + var className = this.props.name + '-' + animationType;
61 + var activeClassName = className + '-active';
62 + var noEventTimeout = null;
63 +
64 + var endListener = function() {
65 + if (__DEV__) {
66 + clearTimeout(noEventTimeout);
67 + }
68 +
69 + // If this gets invoked after the component is unmounted it's OK.
70 + if (!noReset) {
71 + // Usually this means you're about to remove the node if you want to
72 + // leave it in its animated state.
73 + CSSCore.removeClass(node, className);
74 + CSSCore.removeClass(node, activeClassName);
75 + }
76 +
77 + ReactTransitionEvents.removeEndEventListener(node, endListener);
78 +
79 + // Usually this optional callback is used for informing an owner of
80 + // a leave animation and telling it to remove the child.
81 + finishCallback && finishCallback();
82 + };
83 +
84 + ReactTransitionEvents.addEndEventListener(node, endListener);
85 +
86 + CSSCore.addClass(node, className);
87 +
88 + // Need to do this to actually trigger a transition.
89 + this.queueClass(activeClassName);
90 +
91 + if (__DEV__) {
92 + noEventTimeout = setTimeout(noEventListener, NO_EVENT_TIMEOUT);
93 + }
94 + },
95 +
96 + queueClass: function(className) {
97 + this.classNameQueue.push(className);
98 + this.runNextTick(this.flushClassNameQueue);
99 + },
100 +
101 + runNextTick: function() {
102 + if (this.props.runNextTick) {
103 + this.props.runNextTick(this.flushClassNameQueue);
104 + return;
105 + }
106 +
107 + if (!this.timeout) {
108 + this.timeout = setTimeout(this.flushClassNameQueue, TICK);
109 + }
110 + },
111 +
112 + flushClassNameQueue: function() {
113 + if (this.isMounted()) {
114 + this.classNameQueue.forEach(
115 + CSSCore.addClass.bind(CSSCore, this.getDOMNode())
116 + );
117 + }
118 + this.classNameQueue.length = 0;
119 + this.timeout = null;
120 + },
121 +
122 + componentWillMount: function() {
123 + this.classNameQueue = [];
124 + },
125 +
126 + componentWillUnmount: function() {
127 + if (this.timeout) {
128 + clearTimeout(this.timeout);
129 + }
130 + },
131 +
132 + componentWillReceiveProps: function(nextProps) {
133 + if (!nextProps.children && this.props.children) {
134 + this.savedChildren = this.props.children;
135 + }
136 + },
137 +
138 + componentDidMount: function(node) {
139 + if (this.props.enter) {
140 + this.transition('enter');
141 + }
142 + },
143 +
144 + componentDidUpdate: function(prevProps, prevState, node) {
145 + if (prevProps.children && !this.props.children) {
146 + this.transition('leave', true, this.props.onDoneLeaving);
147 + }
148 + },
149 +
150 + render: function() {
151 + return this.props.children || this.savedChildren;
152 + }
153 +});
154 +
155 +module.exports = ReactTransitionableChild;
src/addons/transitions/__tests__/ReactTransitionGroup-test.js new
+86
@@ -0,0 +1,86 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @jsx React.DOM
17 + * @emails react-core
18 + */
19 +
20 +"use strict";
21 +
22 +var React;
23 +var ReactTransitionGroup;
24 +var mocks;
25 +
26 +// Most of the real functionality is covered in other unit tests, this just
27 +// makes sure we're wired up correctly.
28 +describe('ReactTransitionGroup', function() {
29 + beforeEach(function() {
30 + React = require('React');
31 + ReactTransitionGroup = require('ReactTransitionGroup');
32 + mocks = require('mocks');
33 + });
34 +
35 + it('should warn after time with no transitionend', function() {
36 + var container;
37 + var a;
38 +
39 + container = document.createElement('div');
40 + a = React.renderComponent(
41 + <ReactTransitionGroup transitionName="yolo">
42 + <span key="one" id="one" />
43 + </ReactTransitionGroup>,
44 + container
45 + );
46 + expect(a.getDOMNode().childNodes.length).toBe(1);
47 +
48 + setTimeout.mock.calls.length = 0;
49 +
50 + React.renderComponent(
51 + <ReactTransitionGroup transitionName="yolo">
52 + <span key="two" id="two" />
53 + </ReactTransitionGroup>,
54 + container
55 + );
56 + expect(a.getDOMNode().childNodes.length).toBe(2);
57 + expect(a.getDOMNode().childNodes[0].id).toBe('two');
58 + expect(a.getDOMNode().childNodes[1].id).toBe('one');
59 +
60 + console.warn = mocks.getMockFunction();
61 + setTimeout.mock.calls[2][0]();
62 +
63 + expect(a.getDOMNode().childNodes.length).toBe(2);
64 + expect(console.warn.mock.calls.length).toBe(1);
65 + });
66 +
67 + it('should keep both sets of DOM nodes around', function() {
68 + var container = document.createElement('div');
69 + var a = React.renderComponent(
70 + <ReactTransitionGroup transitionName="yolo">
71 + <span key="one" id="one" />
72 + </ReactTransitionGroup>,
73 + container
74 + );
75 + expect(a.getDOMNode().childNodes.length).toBe(1);
76 + React.renderComponent(
77 + <ReactTransitionGroup transitionName="yolo">
78 + <span key="two" id="two" />
79 + </ReactTransitionGroup>,
80 + container
81 + );
82 + expect(a.getDOMNode().childNodes.length).toBe(2);
83 + expect(a.getDOMNode().childNodes[0].id).toBe('two');
84 + expect(a.getDOMNode().childNodes[1].id).toBe('one');
85 + });
86 +});
src/addons/transitions/__tests__/ReactTransitionKeySet-test.js new
+130
@@ -0,0 +1,130 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @jsx React.DOM
17 + * @emails react-core
18 + */
19 +
20 +"use strict";
21 +
22 +var React;
23 +var ReactTransitionKeySet;
24 +
25 +describe('ReactTransitionKeySet', function() {
26 + beforeEach(function() {
27 + React = require('React');
28 + ReactTransitionKeySet = require('ReactTransitionKeySet');
29 + });
30 +
31 + it('should support getChildMapping', function() {
32 + var oneone = <div key="oneone" />;
33 + var onetwo = <div key="onetwo" />;
34 + var one = <div key="one">{oneone}{onetwo}</div>;
35 + var two = <div key="two" />;
36 + var component = <div>{one}{two}</div>;
37 + expect(ReactTransitionKeySet.getChildMapping(component.props.children))
38 + .toEqual({
39 + '{one}': one,
40 + '{two}': two
41 + });
42 + });
43 +
44 + it('should support getKeySet', function() {
45 + var oneone = <div key="oneone" />;
46 + var onetwo = <div key="onetwo" />;
47 + var one = <div key="one">{oneone}{onetwo}</div>;
48 + var two = <div key="two" />;
49 + var component = <div>{one}{two}</div>;
50 + expect(ReactTransitionKeySet.getKeySet(component.props.children)).toEqual({
51 + '{one}': true,
52 + '{two}': true
53 + });
54 + });
55 +
56 + it('should support mergeKeySets for adding keys', function() {
57 + var prev = {
58 + one: true,
59 + two: true
60 + };
61 + var next = {
62 + one: true,
63 + two: true,
64 + three: true
65 + };
66 + expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
67 + one: true,
68 + two: true,
69 + three: true
70 + });
71 + });
72 +
73 + it('should support mergeKeySets for removing keys', function() {
74 + var prev = {
75 + one: true,
76 + two: true,
77 + three: true
78 + };
79 + var next = {
80 + one: true,
81 + two: true
82 + };
83 + expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
84 + one: true,
85 + two: true,
86 + three: true
87 + });
88 + });
89 +
90 + it('should support mergeKeySets for adding and removing', function() {
91 + var prev = {
92 + one: true,
93 + two: true,
94 + three: true
95 + };
96 + var next = {
97 + one: true,
98 + two: true,
99 + four: true
100 + };
101 + expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
102 + one: true,
103 + two: true,
104 + three: true,
105 + four: true
106 + });
107 + });
108 +
109 + it('should reconcile overlapping insertions and deletions', function() {
110 + var prev = {
111 + one: true,
112 + two: true,
113 + four: true,
114 + five: true
115 + };
116 + var next = {
117 + one: true,
118 + two: true,
119 + three: true,
120 + five: true
121 + };
122 + expect(ReactTransitionKeySet.mergeKeySets(prev, next)).toEqual({
123 + one: true,
124 + two: true,
125 + three: true,
126 + four: true,
127 + five: true
128 + });
129 + });
130 +});
src/addons/transitions/__tests__/ReactTransitionableChild-test.js new
+104
@@ -0,0 +1,104 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @jsx React.DOM
17 + * @emails react-core
18 + */
19 +
20 +"use strict";
21 +
22 +var React;
23 +var ReactTransitionableChild;
24 +
25 +var mocks = require('mocks');
26 +
27 +describe('ReactTransitionableChild', function() {
28 + beforeEach(function() {
29 + React = require('React');
30 + ReactTransitionableChild = require('ReactTransitionableChild');
31 + });
32 +
33 + it('should keep the DOM node around', function() {
34 + var container = document.createElement('div');
35 + var ac = React.renderComponent(
36 + <ReactTransitionableChild><span id="test" /></ReactTransitionableChild>,
37 + container
38 + );
39 + expect(ac.getDOMNode().id).toBe('test');
40 + ac = React.renderComponent(<ReactTransitionableChild />, container);
41 + expect(ac.getDOMNode().id).toBe('test');
42 + });
43 +
44 + it('should manage enter css classes correctly', function() {
45 + var runNextTick = mocks.getMockFunction();
46 + var container = document.createElement('div');
47 + var ac = React.renderComponent(
48 + <ReactTransitionableChild
49 + runNextTick={runNextTick}
50 + name="myanim"
51 + enter={true}>
52 + <span id="test" />
53 + </ReactTransitionableChild>,
54 + container
55 + );
56 + expect(ac.getDOMNode().id).toBe('test');
57 + expect(ac.getDOMNode().className.trim()).toBe('myanim-enter');
58 + expect(runNextTick.mock.calls.length).toBe(1);
59 + runNextTick.mock.calls[0][0]();
60 + expect(ac.getDOMNode().className.trim()).toBe(
61 + 'myanim-enter myanim-enter-active'
62 + );
63 + expect(runNextTick.mock.calls.length).toBe(1);
64 + });
65 +
66 + it('should manage leave css classes correctly', function() {
67 + var runNextTick = mocks.getMockFunction();
68 + var container = document.createElement('div');
69 + var ac = React.renderComponent(
70 + <ReactTransitionableChild
71 + runNextTick={runNextTick}
72 + name="myanim"
73 + enter={true}
74 + leave={true}>
75 + <span id="test" />
76 + </ReactTransitionableChild>,
77 + container
78 + );
79 + runNextTick.mock.calls[0][0]();
80 + expect(ac.getDOMNode().className.trim()).toBe(
81 + 'myanim-enter myanim-enter-active'
82 + );
83 +
84 + // TODO: we should just trigger the CSS animation end event to
85 + // clean these up
86 + ac.getDOMNode().className = '';
87 + React.renderComponent(
88 + <ReactTransitionableChild
89 + runNextTick={runNextTick}
90 + name="myanim"
91 + enter={true}
92 + leave={true}
93 + />,
94 + container
95 + );
96 + expect(ac.getDOMNode().className.trim()).toBe('myanim-leave');
97 + expect(runNextTick.mock.calls.length).toBe(2);
98 + runNextTick.mock.calls[1][0]();
99 + expect(ac.getDOMNode().className.trim()).toBe(
100 + 'myanim-leave myanim-leave-active'
101 + );
102 + expect(runNextTick.mock.calls.length).toBe(2);
103 + });
104 +});