├── .babelrc
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── bors.toml
├── demo
├── 404.html
├── components
│ ├── Album.js
│ ├── Button.js
│ ├── Demo.js
│ ├── List.js
│ ├── Navbar.js
│ ├── Numbers.js
│ └── Square.js
├── img
│ ├── 1.jpg
│ └── 2.jpg
├── index.html
├── main.js
└── styles
│ ├── base.css
│ ├── demo1.css
│ └── demo2.css
├── gulpfile.js
├── index.js
├── package-lock.json
├── package.json
└── src
├── AnimatedProperties.js
├── Animation.js
├── StringCache.js
├── Transition.js
├── TransitionChild.js
├── TransitionInfo.js
├── TransitionParser.js
├── __tests__
├── Transition-test.js
├── TransitionEvent.js
├── TransitionInfo-test.js
├── TransitionParser-test.js
└── installMockRAF.js
└── shallowEqual.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-2", "react"],
3 | "plugins": [
4 | ["transform-object-assign"],
5 | ]
6 | }
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | var OFF = 0;
2 | var WARNING = 1;
3 | var ERROR = 2;
4 |
5 | module.exports = {
6 | 'parser': 'babel-eslint',
7 |
8 | 'env': {
9 | 'browser': true,
10 | 'node': true,
11 | 'jest': true,
12 | },
13 |
14 | 'ecmaFeatures': {
15 | 'jsx': true,
16 | 'modules': true,
17 | },
18 |
19 | 'extends': 'eslint:recommended',
20 |
21 | 'plugins': [
22 | 'react',
23 | ],
24 |
25 | 'globals': {
26 | 'Promise': true,
27 | },
28 |
29 | 'rules': {
30 | 'brace-style': [ERROR, 'stroustrup', { 'allowSingleLine': true }],
31 | 'comma-dangle': [ERROR, 'always-multiline'],
32 | 'comma-spacing': [ERROR, {'before': false, 'after': true}],
33 | 'id-length': [ERROR, {'min': 2, 'exceptions': ['i', 'j', 'k']}],
34 | 'indent': [ERROR, 2, {'SwitchCase': WARNING}],
35 | 'key-spacing': [ERROR, {'beforeColon': false, 'afterColon': true}],
36 | 'max-len': [ERROR, 80, 4, {'ignoreUrls': true}],
37 | 'new-cap': [ERROR, {'newIsCap': true, 'capIsNew': true}],
38 | 'no-cond-assign': ERROR,
39 | 'no-console': [ERROR, {'allow': ['warn', 'error']}],
40 | 'no-dupe-keys': ERROR,
41 | 'no-dupe-args': ERROR,
42 | 'no-multiple-empty-lines': ERROR,
43 | 'no-trailing-spaces': ERROR,
44 | 'no-undef': ERROR,
45 | 'no-unreachable': ERROR,
46 | 'no-unused-expressions': ERROR,
47 | 'no-unused-vars': ERROR,
48 | 'object-curly-spacing': [ERROR, 'never'],
49 | 'object-shorthand': OFF,
50 | 'one-var': [ERROR, 'never'],
51 | 'quotes': [ERROR, 'single', 'avoid-escape'],
52 | 'semi': ERROR,
53 | 'space-before-blocks': [ERROR, 'always'],
54 | 'space-before-function-paren': [
55 | ERROR, {'anonymous': 'always', 'named': 'never'}
56 | ],
57 | 'space-infix-ops': ERROR,
58 | 'space-unary-ops': [ERROR, {'words': true, 'nonwords': false}],
59 |
60 | // ES6
61 | 'constructor-super': OFF,
62 | 'no-const-assign': OFF,
63 | 'no-var': OFF,
64 | 'object-shorthand': OFF,
65 | 'prefer-arrow-callback': OFF,
66 | 'prefer-const': [OFF, {'destructuring': 'any'}],
67 | 'prefer-template': OFF,
68 |
69 | // React
70 | 'jsx-quotes': [ERROR, 'prefer-double'],
71 | 'react/display-name': ERROR,
72 | 'react/jsx-boolean-value': ERROR,
73 | 'react/jsx-closing-bracket-location': ERROR,
74 | 'react/jsx-curly-spacing': ERROR,
75 | 'react/jsx-handler-names': [
76 | ERROR,
77 | {'eventHandlerPrefix': '_?handle', 'eventHandlerPropPrefix': 'on'},
78 | ],
79 | 'react/jsx-indent-props': OFF,
80 | 'react/jsx-indent': [ERROR, ERROR],
81 | 'react/jsx-key': ERROR,
82 | 'react/jsx-max-props-per-line': [ERROR, {'maximum': 3}],
83 | 'react/jsx-no-duplicate-props': ERROR,
84 | 'react/jsx-no-undef': ERROR,
85 | 'react/sort-prop-types': ERROR,
86 | 'react/jsx-uses-react': WARNING,
87 | 'react/jsx-uses-vars': WARNING,
88 | 'react/jsx-wrap-multilines': [
89 | ERROR,
90 | {'declaration': true, 'assignment': true, 'return': true},
91 | ],
92 | 'react/no-danger': ERROR,
93 | 'react/no-deprecated': ERROR,
94 | 'react/no-did-update-set-state': ERROR,
95 | 'react/no-direct-mutation-state': ERROR,
96 | 'react/no-multi-comp': ERROR,
97 | 'react/no-string-refs': ERROR,
98 | 'react/no-unknown-property': ERROR,
99 | 'react/prop-types': ERROR,
100 | 'react/self-closing-comp': ERROR,
101 | 'react/sort-comp': ERROR,
102 | },
103 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | node_modules
3 | lib
4 | demo/build
5 | demo/deploy
6 | .publish
7 | .DS_Store
8 | package-backup-*.json
9 | coverage
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | node_modules
4 | src
5 | demo
6 | build
7 | .publish
8 | .babelrc
9 | .eslintrc.js
10 | .travis.yml
11 | gulpfile.js
12 | package-backup-*.json
13 | coverage
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4"
4 | script:
5 | - npm run lint
6 | - npm run build
7 | - npm run test
8 | cache:
9 | directories:
10 | - node_modules
11 | after_script:
12 | - npm run coveralls
13 | branches:
14 | only:
15 | # This is where pull requests from "bors r+" are built.
16 | - staging
17 | # This is where pull requests from "bors try" are built.
18 | - trying
19 | # Uncomment this to enable building pull requests.
20 | - master
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Felipe Thomé
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of the copyright holder nor the names of its contributors
15 | may be used to endorse or promote products derived from this software without
16 | specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Inline Transition Group
2 |
3 | This component helps you to control transitions defined with inline styles. Built with [ReactTransitionHooks](https://github.com/felipethome/react-transition-hooks), the goal is to supply a more up-to-date alternative to [ReactCSSTransitionGroup](https://facebook.github.io/react/docs/animation.html).
4 |
5 | [](https://travis-ci.org/felipethome/react-inline-transition-group) [](https://coveralls.io/github/felipethome/react-inline-transition-group)
6 |
7 | ## Advantages
8 |
9 | * You don't need to decouple your styles from the component.
10 | * You don't need to supply timeout properties as in *ReactCSSTransitionGroup*.
11 | * You have callbacks to control the start and end of your transitions for each child.
12 | * *ReactCSSTransitionGroup* uses timeouts to control the animations which means some situations can break its behavior, like in frame rates lower than 60fps.
13 | * *ReactCSSTransitionGroup* uses *ReactTransitionGroup* which means you can't interrupt animations.
14 |
15 | ## Live Demo
16 |
17 | Check out the [demo](http://felipethome.github.io/react-inline-transition-group/).
18 |
19 | ## How to install
20 |
21 | npm install react-inline-transition-group
22 |
23 | ## How to use
24 |
25 | Import the component to your project and then wrap the nodes you want to control the transition with it. Example:
26 |
27 | ```jsx
28 | import React from 'react';
29 | import ReactDOM from 'react-dom';
30 | import Transition from 'react-inline-transition-group';
31 |
32 | export default class Demo extends React.Component {
33 | constructor() {
34 | super();
35 | this.state = {count: 1};
36 | }
37 |
38 | handleAdd = () => {
39 | this.setState((previousState) => {
40 | return {count: previousState.count + 1};
41 | });
42 | };
43 |
44 | handleRemove = () => {
45 | this.setState((previousState) => {
46 | return {count: Math.max(previousState.count - 1, 0)};
47 | });
48 | };
49 |
50 | handlePhaseEnd = (phase, id) => {
51 | if (phase === 'leave') console.log(id + ' left');
52 | };
53 |
54 | render() {
55 | const styles = {
56 | container: {
57 | width: '500px',
58 | },
59 |
60 | base: {
61 | width: '100%',
62 | height: '50px',
63 | background: '#4CAF50',
64 | opacity: 0,
65 | },
66 |
67 | appear: {
68 | opacity: 1,
69 | transition: 'all 500ms',
70 | },
71 |
72 | leave: {
73 | opacity: 0,
74 | transition: 'all 250ms',
75 | },
76 |
77 | custom: {
78 | background: '#3F51B5',
79 | },
80 | };
81 |
82 | const elems = [];
83 |
84 | // Don't forget that for most React components use array indexes as
85 | // keys is a bad idea (but not for this example).
86 | for (let i = 0; i < this.state.count; i++)
87 | elems.push(
{i}
);
88 |
89 | return (
90 |
91 |
92 | Add
93 | Remove
94 |
95 |
105 | {elems}
106 |
107 |
108 | );
109 | }
110 | }
111 |
112 | ReactDOM.render( , document.getElementById('container'));
113 | ```
114 |
115 | Notice above that `{elems}` are *divs*, but they could be any other React component, just remember to pass the property *style* that your React component is receiving down to the HTML element that will get these styles. Example:
116 |
117 | ```jsx
118 | const SomeComponent = (props) => (
119 |
120 | {props.children}
121 |
122 | );
123 |
124 | const App = () => {
125 | const elems = [];
126 |
127 | // Don't worry, you can still apply custom styles to each element.
128 | const otherStyle = { ... };
129 |
130 | for (let i = 0; i < this.state.count; i++)
131 | elems.push({i} );
132 |
133 | return (
134 |
137 | {elems}
138 |
139 | );
140 | };
141 | ```
142 |
143 |
144 | ## Properties
145 |
146 | Property name | Description
147 | ------------ | -------------
148 | **component** | String. The component that will wrap all the children. Default: `div`.
149 | **childrenStyles** | Object. This object has the properties: `base`, `appear`, `enter` and `leave`. Each of these properties is another object containing the styles for the respective phase. The `base` styles are applied to all children in all phases.
150 | **onPhaseStart** | Function. Callback that will be called with the current phase (`appear`, `enter` or `leave`) and the child `id` when the phase begins, in this order.
151 | **onPhaseEnd** | Function. Callback that will be called with the current phase (`appear`, `enter` or `leave`) and the child `id` when the phase ends, in this order.
152 |
153 | ### Notes
154 |
155 | 1. You can pass an `id` property to your children components and the callback will be called with it so you know exactly for which child the event happened. This `id` is optional.
156 |
157 | 2. The `onPhaseStart` callback will be called sooner a node is being added or removed to/from the group. If you have a delay in your CSS transition the component will not wait until the delay is complete to call the callback.
158 |
159 | 3. The `onPhaseEnd` callback will be called when the longest transition time (delay + duration) completes. Notice that if a transition is interrupted this callback will not be called.
160 |
161 | ## What is meant by phase
162 |
163 | There are three phases in this component (the same as in ReactCSSTransitionGroup):
164 |
165 | * **appear**: happens to any child component that is already inside of ReactInlineTransitionGroup at the moment of its creation, or in other words, at the time the ReactInlineTransitionGroup component just mounted.
166 |
167 | * **enter**: happens to any child component that is inserted in ReactInlineTransitionGroup after its creation.
168 |
169 | * **leave**: happens to any child component that is being removed from ReactInlineTransitionGroup.
170 |
171 | ## LICENSE
172 |
173 | BSD-3
--------------------------------------------------------------------------------
/bors.toml:
--------------------------------------------------------------------------------
1 | status = [
2 | "continuous-integration/travis-ci/push"
3 | ]
--------------------------------------------------------------------------------
/demo/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Inline Transition Group
6 |
7 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/demo/components/Album.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from './Button';
4 | import Transition from '../../src/Transition';
5 | import CSSTransition from 'react-addons-css-transition-group';
6 |
7 | export default class Album extends React.Component {
8 | static displayName = 'Album';
9 |
10 | static propTypes = {
11 | images: PropTypes.array,
12 | };
13 |
14 | state = {
15 | component: '',
16 | count: 1,
17 | show: false,
18 | };
19 |
20 | componentDidMount() {
21 | const promises = this.props.images.map(function (src) {
22 | return new Promise((resolve) => {
23 | var img = new Image();
24 | img.src = src;
25 | img.onload = resolve;
26 | });
27 | });
28 |
29 | Promise.all(promises).then(() => {
30 | this.setState({
31 | show: true,
32 | });
33 | });
34 | }
35 |
36 | _handleAdd = () => {
37 | this.setState((previousState) => {
38 | return {count: previousState.count + 1};
39 | });
40 | };
41 |
42 | _handleComponentChange = (component) => {
43 | this.setState({
44 | component: component,
45 | });
46 | };
47 |
48 | render() {
49 | const styles = {
50 | container: {
51 | position: 'absolute',
52 | top: '200px',
53 | left: '0px',
54 | right: '0px',
55 | margin: '0 auto 30px auto',
56 | padding: '30px',
57 | height: '360px',
58 | width: '500px',
59 | backgroundColor: '#FFF',
60 | boxShadow: '0 4px 5px 0 rgba(0, 0, 0, 0.14),' +
61 | '0 1px 10px 0 rgba(0, 0, 0, 0.12),' +
62 | '0 2px 4px -1px rgba(0, 0, 0, 0.4)',
63 | },
64 |
65 | base: {
66 | position: 'absolute',
67 | top: '0px',
68 | left: '0px',
69 | width: '500px',
70 | height: 'auto',
71 | background: '#FFF',
72 | borderRadius: '2px',
73 | boxSizing: 'border-box',
74 | marginBottom: '10px',
75 | opacity: '0',
76 | },
77 |
78 | appear: {
79 | opacity: '1',
80 | transition: 'all 1000ms ease-in',
81 | },
82 |
83 | leave: {
84 | opacity: '0',
85 | transition: 'all 1000ms ease-in',
86 | },
87 |
88 | button: {
89 | backgroundColor: '#0277BD',
90 | margin: '0px 15px 15px 0',
91 | },
92 |
93 | optionsContainer: {
94 | padding: '10px 10px 0px 10px',
95 | border: '1px solid #333',
96 | borderRadius: '2px',
97 | marginBottom: '20px',
98 | textAlign: 'center',
99 | },
100 |
101 | option: {
102 | marginBottom: '10px',
103 | },
104 |
105 | description: {
106 | marginBottom: '20px',
107 | },
108 | };
109 |
110 | let album;
111 | const elems = [];
112 | if (this.state.show) {
113 | if (this.state.count % 2) {
114 | elems.push(
115 |
121 | );
122 | }
123 | else {
124 | elems.push(
125 |
131 | );
132 | }
133 |
134 | if (this.state.component === 'react-addons') {
135 | album = (
136 |
145 | {elems}
146 |
147 | );
148 | }
149 | else {
150 | album = (
151 |
160 | {elems}
161 |
162 | );
163 | }
164 | }
165 |
166 | return (
167 |
168 |
187 |
188 | Press the Switch Image button before the transition finishes to see
189 | the difference between the components.
190 |
191 |
192 |
193 | Switch Image
194 |
195 |
196 | {album}
197 |
198 | );
199 | }
200 | }
201 |
202 | module.exports = Album;
--------------------------------------------------------------------------------
/demo/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class Button extends React.Component {
5 | static displayName = 'Button';
6 |
7 | static propTypes = {
8 | children: PropTypes.any,
9 | onMouseDown: PropTypes.func,
10 | onMouseUp: PropTypes.func,
11 | style: PropTypes.object,
12 | };
13 |
14 | state = {
15 | mouseDown: false,
16 | };
17 |
18 | _handleMouseDown = (event) => {
19 | this.setState({
20 | mouseDown: true,
21 | });
22 |
23 | if (this.props.onMouseDown) this.props.onMouseDown(event);
24 | };
25 |
26 | _handleMouseUp = (event) => {
27 | this.setState({
28 | mouseDown: false,
29 | });
30 |
31 | if (this.props.onMouseUp) this.props.onMouseUp(event);
32 | };
33 |
34 | render() {
35 | let {
36 | onMouseDown, // eslint-disable-line no-unused-vars
37 | onMouseUp, // eslint-disable-line no-unused-vars
38 | style, // eslint-disable-line no-unused-vars
39 | ...others, // eslint-disable-line comma-dangle
40 | } = this.props;
41 |
42 | const styles = {
43 | button: {
44 | background: 'rgba(255,255,255,0.2)',
45 | border: 'none',
46 | borderRadius: '2px',
47 | cursor: 'pointer',
48 | padding: '10px 15px',
49 | height: '36px',
50 | color: '#FFF',
51 | fontFamily: '"Roboto", sans-serif',
52 | fontSize: '0.9em',
53 | textTransform: 'uppercase',
54 | textDecoration: 'none',
55 | transition: 'ease-out box-shadow .2s',
56 | },
57 |
58 | buttonMouseDown: {
59 | boxShadow: '0 0 2px rgba(0,0,0,.12),0 2px 4px rgba(0,0,0,.24)',
60 | },
61 | };
62 |
63 | const buttonStyle = Object.assign(
64 | {},
65 | styles.button,
66 | this.state.mouseDown && styles.buttonMouseDown,
67 | this.props.style
68 | );
69 |
70 | return (
71 |
77 | {this.props.children}
78 |
79 | );
80 | }
81 | }
--------------------------------------------------------------------------------
/demo/components/Demo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {BrowserRouter as Router, Route, Link} from 'react-router-dom';
3 | import Navbar from './Navbar';
4 | import Button from './Button';
5 | import Square from './Square';
6 | import Album from './Album';
7 | import List from './List';
8 |
9 | const supportsHistory = 'pushState' in window.history;
10 |
11 | export default class Demo extends React.Component {
12 | static displayName = 'Demo';
13 |
14 | render() {
15 | const styles = {
16 | container: {
17 | display: 'flex',
18 | alignItems: 'center',
19 | justifyContent: 'center',
20 | boxSizing: 'padding-box',
21 | height: '100%',
22 | width: '100%',
23 | },
24 |
25 | button: {
26 | backgroundColor: '#0277BD',
27 | },
28 |
29 | link: {
30 | margin: '0 -4px 0 12px',
31 | },
32 | };
33 |
34 | const basename = (sessionStorage || {}).basename;
35 |
36 | const navbarActions = [
37 |
42 | Github
43 | ,
44 |
45 | Demo 1
46 | ,
47 |
48 | Demo 2
49 | ,
50 |
51 | Demo 3
52 | ,
53 | ];
54 |
55 | return (
56 |
60 |
61 |
62 |
63 |
64 | }
67 | />
68 |
69 |
70 |
71 | );
72 | }
73 | }
--------------------------------------------------------------------------------
/demo/components/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from './Button';
3 | import Transition from '../../src/Transition';
4 |
5 | export default class List extends React.Component {
6 | static displayName = 'List'
7 |
8 | state = {
9 | callbackMsg: '',
10 | count: 1,
11 | };
12 |
13 | _handleAdd = () => {
14 | this.setState(function (previousState) {
15 | return {count: Math.min(previousState.count + 1, 4)};
16 | });
17 | };
18 |
19 | _handleRemove = () => {
20 | this.setState(function (previousState) {
21 | return {count: Math.max(previousState.count - 1, 0)};
22 | });
23 | };
24 |
25 | _handlePhaseStart = (phase, id) => {
26 | switch (phase) {
27 | case 'appear':
28 | this._handleStartAppear(id);
29 | break;
30 | case 'enter':
31 | this._handleStartEnter(id);
32 | break;
33 | case 'leave':
34 | this._handleStartLeave(id);
35 | break;
36 | }
37 | };
38 |
39 | _handlePhaseEnd = (phase, id) => {
40 | switch (phase) {
41 | case 'appear':
42 | this._handleAppeared(id);
43 | break;
44 | case 'enter':
45 | this._handleEntered(id);
46 | break;
47 | case 'leave':
48 | this._handleLeft(id);
49 | break;
50 | }
51 | };
52 |
53 | _handleStartAppear = (id) => {
54 | this.setState({callbackMsg: id + ' start to appear'});
55 | };
56 |
57 | _handleStartEnter = (id) => {
58 | this.setState({callbackMsg: id + ' start to enter'});
59 | };
60 |
61 | _handleStartLeave = (id) => {
62 | this.setState({callbackMsg: id + ' start to leave'});
63 | };
64 |
65 | _handleAppeared = (id) => {
66 | this.setState({callbackMsg: id + ' appeared'});
67 | };
68 |
69 | _handleEntered = (id) => {
70 | this.setState({callbackMsg: id + ' entered'});
71 | };
72 |
73 | _handleLeft = (id) => {
74 | this.setState({callbackMsg: id + ' left'});
75 | };
76 |
77 | render() {
78 | var styles = {
79 | container: {
80 | position: 'absolute',
81 | top: '200px',
82 | left: '0px',
83 | right: '0px',
84 | margin: '0 auto 30px auto',
85 | padding: '30px',
86 | height: '300px',
87 | width: '500px',
88 | background: '#FFF',
89 | boxShadow: '0 4px 5px 0 rgba(0, 0, 0, 0.14),' +
90 | '0 1px 10px 0 rgba(0, 0, 0, 0.12),' +
91 | '0 2px 4px -1px rgba(0, 0, 0, 0.4)',
92 | },
93 |
94 | base: {
95 | background: '#FFF',
96 | borderRadius: '2px',
97 | boxSizing: 'border-box',
98 | height: '50px',
99 | marginBottom: '5px',
100 | padding: '10px',
101 | },
102 |
103 | appear: {
104 | background: '#81C784',
105 | transition: 'all 1000ms',
106 | },
107 |
108 | leave: {
109 | background: '#FFF',
110 | transition: 'all 500ms',
111 | },
112 |
113 | button: {
114 | backgroundColor: '#0277BD',
115 | margin: '0px 15px 15px 0',
116 | },
117 |
118 | callback: {
119 | height: '20px',
120 | backgroundColor: '#FFF',
121 | border: '1px solid #333',
122 | borderRadius: '2px',
123 | marginBottom: '15px',
124 | padding: '5px 5px 5px 5px',
125 | },
126 | };
127 |
128 | const elems = [];
129 |
130 | for (let i = 0; i < this.state.count; i++) {
131 | elems.push(
132 | {'id: ' + i}
133 | );
134 | }
135 |
136 | return (
137 |
138 |
139 |
140 | Add
141 |
142 |
143 | Remove
144 |
145 |
146 |
147 | {'Callback: ' + this.state.callbackMsg}
148 |
149 |
159 | {elems}
160 |
161 |
162 | );
163 | }
164 | }
--------------------------------------------------------------------------------
/demo/components/Navbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class Navbar extends React.Component {
5 | static displayName = 'Navbar';
6 |
7 | static propTypes = {
8 | actions: PropTypes.array,
9 | };
10 |
11 | render() {
12 | var styles = {
13 | container: {
14 | position: 'fixed',
15 | left: '0px',
16 | top: '0px',
17 | zIndex: '1',
18 | width: '100%',
19 | },
20 |
21 | banner: {
22 | display: 'flex',
23 | justifyContent: 'center',
24 | alignItems: 'center',
25 | flexDirection: 'column',
26 | background: '#01579B',
27 | height: '100px',
28 | width: '100%',
29 | },
30 |
31 | bannerProjectName: {
32 | fontSize: '2.5em',
33 | color: '#FFF',
34 | },
35 |
36 | bannerDescription: {
37 | color: '#CCC',
38 | marginTop: '5px',
39 | },
40 |
41 | menu: {
42 | display: 'flex',
43 | alignItems: 'center',
44 | justifyContent: 'center',
45 | background: '#01579B',
46 | boxShadow: '0 0px 8px rgba(0,0,0,.28)',
47 | height: '56px',
48 | width: '100%',
49 | margin: '0px',
50 | },
51 | };
52 |
53 | return (
54 |
55 |
56 |
57 | React Inline Transition Group
58 |
59 |
60 | Control CSS transitions defined with inline style.
61 |
62 |
63 |
64 | {this.props.actions}
65 |
66 |
67 | );
68 | }
69 | }
--------------------------------------------------------------------------------
/demo/components/Numbers.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Button = require('./Button');
3 | var Transition = require('../../src/Transition');
4 |
5 | var List = React.createClass({
6 | displayName: 'List',
7 |
8 | getInitialState: function () {
9 | return {
10 | callbackMsg: '',
11 | count: 1,
12 | };
13 | },
14 |
15 | _handleAdd: function () {
16 | this.setState(function (previousState) {
17 | return {count: Math.min(previousState.count + 1, 6)};
18 | });
19 | },
20 |
21 | _handleRemove: function () {
22 | this.setState(function (previousState) {
23 | return {count: Math.max(previousState.count - 1, 0)};
24 | });
25 | },
26 |
27 | _handlePhaseStart: function (phase, id) {
28 | switch (phase) {
29 | case 'appear':
30 | this._handleStartAppear(id);
31 | break;
32 | case 'enter':
33 | this._handleStartEnter(id);
34 | break;
35 | case 'leave':
36 | this._handleStartLeave(id);
37 | break;
38 | }
39 | },
40 |
41 | _handlePhaseEnd: function (phase, id) {
42 | switch (phase) {
43 | case 'appear':
44 | this._handleAppeared(id);
45 | break;
46 | case 'enter':
47 | this._handleEntered(id);
48 | break;
49 | case 'leave':
50 | this._handleLeft(id);
51 | break;
52 | }
53 | },
54 |
55 | _handleStartAppear: function (id) {
56 | this.setState({callbackMsg: id + ' start to appear'});
57 | },
58 |
59 | _handleStartEnter: function (id) {
60 | this.setState({callbackMsg: id + ' start to enter'});
61 | },
62 |
63 | _handleStartLeave: function (id) {
64 | this.setState({callbackMsg: id + ' start to leave'});
65 | },
66 |
67 | _handleAppeared: function (id) {
68 | this.setState({callbackMsg: id + ' appeared'});
69 | },
70 |
71 | _handleEntered: function (id) {
72 | this.setState({callbackMsg: id + ' entered'});
73 | },
74 |
75 | _handleLeft: function (id) {
76 | this.setState({callbackMsg: id + ' left'});
77 | },
78 |
79 | render: function () {
80 | var styles = {
81 | container: {
82 | background: '#FFF',
83 | boxShadow: '0 4px 5px 0 rgba(0, 0, 0, 0.14),' +
84 | '0 1px 10px 0 rgba(0, 0, 0, 0.12),' +
85 | '0 2px 4px -1px rgba(0, 0, 0, 0.4)',
86 | padding: '30px',
87 | height: '300px',
88 | width: '500px',
89 | overflow: 'hidden',
90 | },
91 |
92 | numbersContainer: {
93 | position: 'relative',
94 | height: '200px',
95 | display: 'flex',
96 | alignItems: 'center',
97 | justifyContent: 'center',
98 | },
99 |
100 | base: {
101 | position: 'absolute',
102 | background: '#F50057',
103 | borderRadius: '0%',
104 | height: '150px',
105 | width: '150px',
106 | fontSize: '80px',
107 | lineHeight: '150px',
108 | verticalAlign: 'middle',
109 | textAlign: 'center',
110 | color: 'transparent',
111 | opacity: '0.5',
112 | transformOrigin: 'center center',
113 | transform: 'scale(0) rotate(0deg) skew(-90deg, -90deg)',
114 | },
115 |
116 | appear: {
117 | background: '#3F51B5',
118 | borderRadius: '50%',
119 | color: 'white',
120 | opacity: '1',
121 | transform: 'scale(1) rotate(360deg) skew(0deg, 0deg)',
122 | transition: 'color 0.5s 1s, opacity 2s 1s, border-radius 2.5s, ' +
123 | 'background 3s ease-out, transform 2s',
124 | },
125 |
126 | leave: {
127 | background: 'transparent',
128 | borderRadius: '50%',
129 | transform: 'scale(0) rotate(0deg) skew(-90deg, -90deg)',
130 | transition: 'all 2000ms',
131 | },
132 |
133 | button: {
134 | backgroundColor: '#2980b9',
135 | margin: '0px 15px 15px 0',
136 | },
137 |
138 | callback: {
139 | height: '20px',
140 | backgroundColor: '#FFF',
141 | border: '1px solid #2980b9',
142 | borderRadius: '2px',
143 | marginBottom: '15px',
144 | padding: '5px 5px 5px 5px',
145 | },
146 | };
147 |
148 | var elems = [];
149 |
150 | for (var i = 0; i < this.state.count; i++) {
151 | elems.push(
152 | {i}
153 | );
154 | }
155 |
156 | return (
157 |
158 |
159 |
164 |
169 |
170 |
171 | {'Callback: ' + this.state.callbackMsg}
172 |
173 |
184 | {elems}
185 |
186 |
187 | );
188 | },
189 |
190 | });
191 |
192 | module.exports = List;
--------------------------------------------------------------------------------
/demo/components/Square.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Transition from '../../src/Transition';
4 | import CSSTransition from 'react-addons-css-transition-group';
5 |
6 | export default class square extends React.Component {
7 | static displayName = 'square';
8 |
9 | static propTypes = {
10 | active: PropTypes.bool,
11 | left: PropTypes.number,
12 | top: PropTypes.number,
13 | };
14 |
15 | state = {
16 | component: '',
17 | left: 0,
18 | top: 0,
19 | };
20 |
21 | componentWillMount() {
22 | this.count = 0;
23 | }
24 |
25 | _handleMouseMove = (event) => {
26 | this.setState({
27 | top: event.clientY,
28 | left: event.clientX,
29 | });
30 | };
31 |
32 | _handleTouchMove = (event) => {
33 | this.setState({
34 | top: event.touches[0].pageY,
35 | left: event.touches[0].pageX,
36 | });
37 | };
38 |
39 | _handleComponentChange = (component) => {
40 | this.setState({
41 | component: component,
42 | });
43 | };
44 |
45 | render() {
46 | const styles = {
47 | container: {
48 | position: 'relative',
49 | background: '#000',
50 | height: '100%',
51 | width: '100%',
52 | },
53 |
54 | square: {
55 | position: 'absolute',
56 | top: (this.state.top - 25) + 'px',
57 | left: (this.state.left - 40) + 'px',
58 | border: '2px solid #448AFF',
59 | height: '50px',
60 | width: '81px',
61 | opacity: '0.5',
62 | transform: 'scale(0) rotate(0deg)',
63 | },
64 |
65 | appear: {
66 | transition: 'all 600ms',
67 | opacity: '1',
68 | },
69 |
70 | leave: {
71 | transition: 'all 600ms',
72 | opacity: '0',
73 | transform: 'scale(2) rotate(135deg)',
74 | },
75 |
76 | optionsContainer: {
77 | position: 'absolute',
78 | top: '200px',
79 | left: '0px',
80 | right: '0px',
81 | width: '450px',
82 | margin: '0 auto 0px auto',
83 | padding: '10px 10px 0px 10px',
84 | color: '#FFF',
85 | textAlign: 'center',
86 | border: '1px solid #333',
87 | borderRadius: '2px',
88 | },
89 |
90 | option: {
91 | marginBottom: '10px',
92 | },
93 |
94 | description: {
95 | position: 'absolute',
96 | top: '300px',
97 | width: '100%',
98 | textAlign: 'center',
99 | color: '#FFF',
100 | },
101 | };
102 |
103 | let squareStyle;
104 | if (this.state.component === 'react-addons') {
105 | squareStyle = {
106 | top: (this.state.top - 25) + 'px',
107 | left: (this.state.left - 40) + 'px',
108 | };
109 | }
110 |
111 | const squares = [];
112 | squares.push(
113 |
118 | );
119 |
120 | let transitionComponent;
121 | if (this.state.component === 'react-addons') {
122 | transitionComponent = (
123 |
131 | {squares}
132 |
133 | );
134 | }
135 | else {
136 | transitionComponent = (
137 |
145 | {squares}
146 |
147 | );
148 | }
149 |
150 | return (
151 |
156 | {transitionComponent}
157 |
Move your cursor across the screen
158 |
177 |
178 | );
179 | }
180 | }
--------------------------------------------------------------------------------
/demo/img/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felipethome/react-inline-transition-group/209bd3189ecdd653889155a0089b4aff754e0697/demo/img/1.jpg
--------------------------------------------------------------------------------
/demo/img/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/felipethome/react-inline-transition-group/209bd3189ecdd653889155a0089b4aff754e0697/demo/img/2.jpg
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Inline Transition Group
6 |
7 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/demo/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import Demo from './components/Demo';
4 |
5 | render(
6 | ,
7 | document.getElementById('demo')
8 | );
--------------------------------------------------------------------------------
/demo/styles/base.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | font-family: 'Roboto', sans-serif;
6 | background: #F5F5F5;
7 | }
8 |
9 | .component-container {
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | height: 100%;
14 | width: 100%;
15 | overflow: auto;
16 | }
--------------------------------------------------------------------------------
/demo/styles/demo1.css:
--------------------------------------------------------------------------------
1 | .square {
2 | position: absolute;
3 | border: 2px solid #448AFF;
4 | height: 50px;
5 | width: 81px;
6 | opacity: 0.5;
7 | transform: scale(0) rotate(0deg);
8 | }
9 |
10 | .square-appear {
11 | }
12 |
13 | .square-appear.square-appear-active {
14 | opacity: 1;
15 | transition: all 600ms;
16 | }
17 |
18 | .square-enter {
19 | }
20 |
21 | .square-enter.square-enter-active {
22 | opacity: 1;
23 | transition: all 600ms;
24 | }
25 |
26 | .square-leave {
27 | }
28 |
29 | .square-leave.square-leave-active {
30 | opacity: 0;
31 | transform: scale(2) rotate(135deg);
32 | transition: all 600ms;
33 | }
--------------------------------------------------------------------------------
/demo/styles/demo2.css:
--------------------------------------------------------------------------------
1 | .album {
2 | position: absolute;
3 | top: 0px;
4 | left: 0px;
5 | width: 500px;
6 | height: auto;
7 | background: #FFF;
8 | border-radius: 2px;
9 | box-sizing: border-box;
10 | margin-bottom: 10px;
11 | }
12 |
13 | .album-appear {
14 | opacity: 0;
15 | }
16 |
17 | .album-appear.album-appear-active {
18 | opacity: 1;
19 | transition: opacity 1000ms ease-in;
20 | }
21 |
22 | .album-enter {
23 | opacity: 0;
24 | }
25 |
26 | .album-enter.album-enter-active {
27 | opacity: 1;
28 | transition: opacity 1000ms ease-in;
29 | }
30 |
31 | .album-leave {
32 | opacity: 1;
33 | }
34 |
35 | .album-leave.album-leave-active {
36 | opacity: 0;
37 | transition: opacity 1000ms ease-in;
38 | }
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable no-console */
2 |
3 | var browserify = require('browserify');
4 | var connect = require('gulp-connect');
5 | var merge = require('merge-stream');
6 | var notify = require('gulp-notify');
7 | var ghPages = require('gulp-gh-pages');
8 | var gulp = require('gulp');
9 | var gulpif = require('gulp-if');
10 | var gutil = require('gulp-util');
11 | var source = require('vinyl-source-stream');
12 | var streamify = require('gulp-streamify');
13 | var uglify = require('gulp-uglify');
14 | var watchify = require('watchify');
15 |
16 | var files = {
17 | deploy: [
18 | 'demo/index.html',
19 | 'demo/404.html',
20 | 'demo/img/**',
21 | 'demo/styles/**',
22 | ],
23 |
24 | dependencies: [
25 | 'react',
26 | 'react-dom',
27 | 'react-addons-transition-group',
28 | 'react-addons-css-transition-group',
29 | ],
30 |
31 | browserify: [
32 | './demo/main.js',
33 | ],
34 | };
35 |
36 | var browserifyTask = function (options) {
37 | var bundler = browserify({
38 | entries: [options.src],
39 | transform: [
40 | ['babelify', {
41 | presets: ['es2015', 'stage-2', 'react'],
42 | plugins: ['transform-class-properties'],
43 | }],
44 | ],
45 | debug: options.development,
46 | cache: {}, // Requirement of watchify
47 | packageCache: {}, // Requirement of watchify
48 | fullPaths: options.development,
49 | alias: ['/node_modules/react/react.js:react'],
50 | extensions: ['.js', '.jsx', '.json'],
51 | });
52 |
53 | var rebundle = function () {
54 | var start = Date.now();
55 | console.log('Building APP bundle');
56 | return bundler
57 | .bundle()
58 | .on('error', gutil.log)
59 | .pipe(source(options.output))
60 | .pipe(gulpif(!options.development, streamify(uglify())))
61 | .pipe(gulp.dest(options.dest))
62 | .pipe(gulpif(options.development, connect.reload()))
63 | .pipe(notify(function () {
64 | console.log('APP bundle built in ' + (Date.now() - start) + 'ms');
65 | }));
66 | };
67 |
68 | bundler.external(files.dependencies);
69 |
70 | if (options.development) {
71 | bundler = watchify(bundler);
72 | bundler.on('update', rebundle);
73 | }
74 |
75 | return rebundle();
76 | };
77 |
78 | var browserifyDepsTask = function (options) {
79 | var vendorsBundler = browserify({
80 | debug: options.development,
81 | require: files.dependencies,
82 | });
83 |
84 | var start = new Date();
85 | console.log('Building VENDORS bundle');
86 | return vendorsBundler
87 | .bundle()
88 | .on('error', gutil.log)
89 | .pipe(source(options.output))
90 | .pipe(gulpif(!options.development, streamify(uglify())))
91 | .on('error', gutil.log)
92 | .pipe(gulp.dest(options.dest))
93 | .pipe(notify(function () {
94 | console.log('VENDORS bundle built in ' + (Date.now() - start) + 'ms');
95 | }));
96 | };
97 |
98 | gulp.task('ghpages', ['deploy'], function () {
99 | return gulp.src('./demo/deploy/**/*')
100 | .pipe(ghPages());
101 | });
102 |
103 | gulp.task('deploy', function () {
104 | process.env.NODE_ENV = 'production';
105 |
106 | var browserifyDepsOpt = {
107 | development: false,
108 | src: files.dependencies,
109 | output: 'vendors.js',
110 | dest: './demo/deploy/build/scripts',
111 | };
112 |
113 | var browserifyOpt = {
114 | development: false,
115 | src: files.browserify,
116 | output: 'bundle.js',
117 | dest: './demo/deploy/build/scripts',
118 | };
119 |
120 | var copyFiles = gulp.src(files.deploy, {base: './demo'})
121 | .on('error', gutil.log)
122 | .pipe(gulp.dest('./demo/deploy'));
123 |
124 | return merge(
125 | copyFiles,
126 | browserifyDepsTask(browserifyDepsOpt),
127 | browserifyTask(browserifyOpt)
128 | );
129 | });
130 |
131 | gulp.task('dev', function () {
132 | process.env.NODE_ENV = 'development';
133 |
134 | var browserifyDepsOpt = {
135 | development: true,
136 | src: files.dependencies,
137 | output: 'vendors.js',
138 | dest: './demo/build/scripts',
139 | };
140 |
141 | var browserifyOpt = {
142 | development: true,
143 | src: files.browserify,
144 | output: 'bundle.js',
145 | dest: './demo/build/scripts',
146 | };
147 |
148 | var serverOpt = {
149 | root: './demo',
150 | port: 8080,
151 | livereload: true,
152 | fallback: './demo/index.html',
153 | };
154 |
155 | connect.server(serverOpt);
156 |
157 | return merge(
158 | browserifyDepsTask(browserifyDepsOpt),
159 | browserifyTask(browserifyOpt)
160 | );
161 | });
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/Transition');
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-inline-transition-group",
3 | "version": "2.2.1",
4 | "author": "Felipe Thomé",
5 | "description": "Like React CSS Transition Group, but with inline style.",
6 | "license": "BSD-3-Clause",
7 | "repository": "https://github.com/felipethome/react-inline-transition-group",
8 | "homepage": "http://felipethome.github.io/react-inline-transition-group",
9 | "keywords": [
10 | "react",
11 | "react-component",
12 | "react-addons",
13 | "transition-group",
14 | "inline-styles",
15 | "react-addons-css-transition-group"
16 | ],
17 | "peerDependencies": {
18 | "react": "0.14.x || 15.x || 16.x"
19 | },
20 | "dependencies": {
21 | "create-react-class": "^15.5.2",
22 | "prop-types": "^15.5.8",
23 | "react-transition-hooks": "^1.2.0"
24 | },
25 | "devDependencies": {
26 | "babel-cli": "^6.18.0",
27 | "babel-eslint": "^7.1.0",
28 | "babel-jest": "^21.2.0",
29 | "babel-plugin-transform-class-properties": "^6.18.0",
30 | "babel-plugin-transform-object-assign": "^6.8.0",
31 | "babel-preset-es2015": "^6.18.0",
32 | "babel-preset-react": "^6.16.0",
33 | "babel-preset-stage-2": "^6.18.0",
34 | "babelify": "^7.3.0",
35 | "browserify": "^13.1.1",
36 | "coveralls": "^2.11.14",
37 | "eslint": "^3.9.0",
38 | "eslint-plugin-react": "^6.4.1",
39 | "gulp": "^3.9.1",
40 | "gulp-connect": "^5.0.0",
41 | "gulp-gh-pages": "^0.5.4",
42 | "gulp-if": "^2.0.1",
43 | "gulp-notify": "^2.2.0",
44 | "gulp-streamify": "^1.0.2",
45 | "gulp-uglify": "^2.0.0",
46 | "gulp-util": "^3.0.7",
47 | "jest-cli": "^21.2.1",
48 | "react": "^15.6.2",
49 | "react-addons-css-transition-group": "^15.6.2",
50 | "react-addons-transition-group": "^15.6.2",
51 | "react-dom": "^15.6.2",
52 | "react-router-dom": "^4.2.2",
53 | "vinyl-source-stream": "^1.1.0",
54 | "watchify": "^3.7.0"
55 | },
56 | "scripts": {
57 | "build": "babel ./src --out-dir ./lib --ignore __tests__",
58 | "clean": "rm -rf demo/{build,deploy}",
59 | "dev": "gulp dev",
60 | "deploy": "gulp ghpages",
61 | "lint": "eslint src/",
62 | "test": "jest --verbose",
63 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
64 | },
65 | "jest": {
66 | "collectCoverage": true,
67 | "transform": {
68 | ".*": "/node_modules/babel-jest"
69 | },
70 | "testRegex": "(/__tests__/.*\\-test)\\.(js)$",
71 | "roots": [
72 | "/src"
73 | ]
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/AnimatedProperties.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2016, Felipe Thomé
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 |
9 | /**
10 | * Information about CSS properties that can be animated.
11 | *
12 | * See:
13 | * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties
14 | */
15 |
16 | var shorthandNames = {
17 | '-moz-outline-radius-bottomleft': '-moz-outline-radius',
18 | '-moz-outline-radius-bottomright': '-moz-outline-radius',
19 | '-moz-outline-radius-topleft': '-moz-outline-radius',
20 | '-moz-outline-radius-topright': '-moz-outline-radius',
21 | '-webkit-text-stroke-color': '-webkit-text-stroke',
22 | '-webkit-text-stroke-width': '-webkit-text-stroke',
23 |
24 | 'background-color': 'background',
25 | 'background-position': 'background',
26 | 'background-size': 'background',
27 |
28 | 'border-bottom-color': 'border border-bottom border-color',
29 | 'border-bottom-left-radius': 'border-radius',
30 | 'border-bottom-right-radius': 'border-radius',
31 | 'border-bottom-width': 'border border-bottom border-width',
32 |
33 | 'border-left-color': 'border border-left border-color',
34 | 'border-left-width': 'border border-left border-width',
35 |
36 | 'border-right-color': 'border border-right border-color',
37 | 'border-right-width': 'border border-right border-width',
38 |
39 | 'border-top-color': 'border border-top border-color',
40 | 'border-top-left-radius': 'border-radius',
41 | 'border-top-right-radius': 'border-radius',
42 | 'border-top-width': 'border border-top border-width',
43 |
44 | 'column-rule-color': 'column-rule',
45 | 'column-rule-width': 'column-rule',
46 |
47 | 'column-width': 'columns',
48 | 'column-count': 'columns',
49 |
50 | 'flex-basis': 'flex',
51 | 'flex-grow': 'flex',
52 | 'flex-shrink': 'flex',
53 |
54 | 'font-size': 'font',
55 | 'font-weight': 'font',
56 |
57 | 'grid-column-gap': 'grid-gap',
58 | 'grid-row-gap': 'grid-gap',
59 |
60 | 'line-height': 'font',
61 |
62 | 'margin-bottom': 'margin',
63 | 'margin-left': 'margin',
64 | 'margin-right': 'margin',
65 | 'margin-top': 'margin',
66 |
67 | 'mask-position': 'mask',
68 | 'mask-size': 'mask',
69 |
70 | 'outline-color': 'outline',
71 | 'outline-width': 'outline',
72 |
73 | 'padding-bottom': 'padding',
74 | 'padding-left': 'padding',
75 | 'padding-right': 'padding',
76 | 'padding-top': 'padding',
77 |
78 | 'text-emphasis-color': 'text-emphasis',
79 | };
80 |
81 | var getShorthandNames = function (property) {
82 | var shorthands = shorthandNames[property];
83 |
84 | if (shorthands) return shorthands.split(' ');
85 |
86 | return [];
87 | };
88 |
89 | module.exports = {
90 | getShorthandNames: getShorthandNames,
91 | };
--------------------------------------------------------------------------------
/src/Animation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2016, Felipe Thomé
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 |
9 | /**
10 | * Implements requestNextFrame allowing to apply transitions without worry about
11 | * the browser batch the styles.
12 | */
13 |
14 | var _frameIds = {};
15 |
16 | var _getKey = function () {
17 | var key;
18 | while (!key || _frameIds.hasOwnProperty(key)) {
19 | key = Math.floor(Math.random() * 1E9);
20 | }
21 | return key;
22 | };
23 |
24 | var requestNextFrame = function (callback) {
25 | var key = _getKey();
26 |
27 | _frameIds[key] = requestAnimationFrame(function () {
28 | _frameIds[key] = requestAnimationFrame(function (timestamp) {
29 | delete _frameIds[key];
30 | callback(timestamp);
31 | });
32 | });
33 |
34 | return key;
35 | };
36 |
37 | var cancelFrames = function (key) {
38 | if (Array.isArray(key)) {
39 | for (var i = 0; i < key.length; i++) {
40 | if (_frameIds[key[i]]) {
41 | cancelAnimationFrame(_frameIds[key[i]]);
42 | }
43 | }
44 | }
45 | else if (_frameIds[key]) {
46 | cancelAnimationFrame(_frameIds[key]);
47 | delete _frameIds[key];
48 | }
49 | };
50 |
51 | module.exports = {
52 | requestNextFrame: requestNextFrame,
53 | cancelFrames: cancelFrames,
54 | };
--------------------------------------------------------------------------------
/src/StringCache.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2016, Felipe Thomé
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 |
9 | /**
10 | * String cache. The cache has a capacity that is defined in the moment of its
11 | * creation.
12 | */
13 |
14 | function StringCache(capacity) {
15 | this._cache = {};
16 | this._capacity = capacity;
17 | this._size = 0;
18 | }
19 |
20 | StringCache.prototype.get = function (key) {
21 | return this._cache[key];
22 | };
23 |
24 | StringCache.prototype.set = function (key, value) {
25 | if (this._cache[key] !== undefined) {
26 | this._cache[key] = value;
27 | }
28 | else if (this._size < this._capacity) {
29 | this._size++;
30 | this._cache[key] = value;
31 | }
32 | };
33 |
34 | module.exports = StringCache;
--------------------------------------------------------------------------------
/src/Transition.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2016, Felipe Thomé
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 |
9 | var React = require('react');
10 | var createReactClass = require('create-react-class');
11 | var PropTypes = require('prop-types');
12 | var TransitionHooks = require('react-transition-hooks');
13 | var TransitionChild = require('./TransitionChild');
14 |
15 | var Transition = createReactClass({
16 | displayName: 'Transition',
17 |
18 | propTypes: {
19 | children: PropTypes.node,
20 | childrenStyles: PropTypes.shape({
21 | base: PropTypes.object,
22 | appear: PropTypes.object,
23 | enter: PropTypes.object,
24 | leave: PropTypes.object,
25 | }),
26 | component: PropTypes.oneOfType([
27 | PropTypes.string,
28 | PropTypes.func,
29 | ]),
30 | onPhaseEnd: PropTypes.func,
31 | onPhaseStart: PropTypes.func,
32 | },
33 |
34 | getDefaultProps: function () {
35 | return {
36 | childrenStyles: {},
37 | component: 'div',
38 | };
39 | },
40 |
41 | render: function () { // eslint-disable-line
42 | var {
43 | children,
44 | childrenStyles,
45 | component,
46 | onPhaseEnd,
47 | onPhaseStart,
48 | ...others
49 | } = this.props;
50 |
51 | return (
52 |
53 | {React.Children.map(children, function (child) {
54 | return (
55 |
69 | {child}
70 |
71 | );
72 | }, this)}
73 |
74 | );
75 | },
76 |
77 | });
78 |
79 | module.exports = Transition;
80 |
--------------------------------------------------------------------------------
/src/TransitionChild.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2016, Felipe Thomé
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 |
9 | var React = require('react');
10 | var ReactDOM = require('react-dom');
11 | var createReactClass = require('create-react-class');
12 | var PropTypes = require('prop-types');
13 | var Animation = require('./Animation');
14 | var TransitionInfo = require('./TransitionInfo');
15 | var TransitionParser = require('./TransitionParser');
16 | var shallowEqual = require('./shallowEqual');
17 |
18 | var TransitionChild = createReactClass({
19 | displayName: 'TransitionChild',
20 |
21 | propTypes: {
22 | children: PropTypes.any,
23 | childrenAppearStyle: PropTypes.object,
24 | childrenBaseStyle: PropTypes.object,
25 | childrenEnterStyle: PropTypes.object,
26 | childrenLeaveStyle: PropTypes.object,
27 | id: PropTypes.oneOfType(
28 | [PropTypes.string, PropTypes.number]
29 | ),
30 | onChildAppeared: PropTypes.func,
31 | onChildEntered: PropTypes.func,
32 | onChildLeft: PropTypes.func,
33 | onChildStartAppear: PropTypes.func,
34 | onChildStartEnter: PropTypes.func,
35 | onChildStartLeave: PropTypes.func,
36 | style: PropTypes.object,
37 | },
38 |
39 | getInitialState: function () {
40 | return {
41 | style: this._computeNewStyle(),
42 | };
43 | },
44 |
45 | componentDidMount: function () {
46 | this._frameIds = [];
47 | },
48 |
49 | componentWillReceiveProps: function (nextProps) {
50 | var oldBaseStyle = this.props.childrenBaseStyle;
51 | var newBaseStyle = nextProps.childrenBaseStyle;
52 | var oldPropsStyle = this.props.style;
53 | var newPropsStyle = nextProps.style;
54 | var oldPhaseStyle;
55 | var newPhaseStyle;
56 |
57 | switch(this._phase) {
58 | case 'appear':
59 | oldPhaseStyle = this.props.childrenAppearStyle;
60 | newPhaseStyle = nextProps.childrenAppearStyle;
61 | break;
62 | case 'enter':
63 | oldPhaseStyle = this.props.childrenEnterStyle;
64 | newPhaseStyle = nextProps.childrenEnterStyle;
65 | break;
66 | case 'leave':
67 | oldPhaseStyle = this.props.childrenLeaveStyle;
68 | newPhaseStyle = nextProps.childrenLeaveStyle;
69 | break;
70 | }
71 |
72 | if (!shallowEqual(oldBaseStyle, newBaseStyle) ||
73 | !shallowEqual(oldPropsStyle, newPropsStyle) ||
74 | !shallowEqual(oldPhaseStyle, newPhaseStyle)) {
75 | this.setState({
76 | style: Object.assign({}, newBaseStyle, newPhaseStyle, newPropsStyle),
77 | });
78 | }
79 | },
80 |
81 | componentWillUnmount: function () {
82 | var node = ReactDOM.findDOMNode(this);
83 | if (!node) return;
84 |
85 | node.removeEventListener('transitionend', this._handleReference);
86 |
87 | Animation.cancelFrames(this._frameIds);
88 | },
89 |
90 | componentWillAppear: function (callback) {
91 | if (this.props.onChildStartAppear) {
92 | this.props.onChildStartAppear('appear', this.props.id);
93 | }
94 |
95 | this._transition(callback, 'appear');
96 | },
97 |
98 | componentDidAppear: function () {
99 | if (this.props.onChildAppeared) {
100 | this.props.onChildAppeared('appear', this.props.id);
101 | }
102 | },
103 |
104 | componentWillEnter: function (callback) {
105 | if (this.props.onChildStartEnter) {
106 | this.props.onChildStartEnter('enter', this.props.id);
107 | }
108 |
109 | this._transition(callback, 'enter');
110 | },
111 |
112 | componentDidEnter: function () {
113 | if (this.props.onChildEntered) {
114 | this.props.onChildEntered('enter', this.props.id);
115 | }
116 | },
117 |
118 | componentWillLeave: function (callback) {
119 | if (this.props.onChildStartLeave) {
120 | this.props.onChildStartLeave('leave', this.props.id);
121 | }
122 |
123 | this._transition(callback, 'leave');
124 | },
125 |
126 | componentDidLeave: function () {
127 | if (this.props.onChildLeft) {
128 | this.props.onChildLeft('leave', this.props.id);
129 | }
130 | },
131 |
132 | _handleTransitionEnd: function (
133 | node, maxTimeProperty, propertyArray, callback, event
134 | ) {
135 | // Check if the element where the transitionend event occurred was the
136 | // node we are working with and not one of its children.
137 | if (node === event.target) {
138 | // TODO: Instead of this huge and ugly if statement expand the shorthand
139 | // properties and bind the expansion to the handler.
140 | if (maxTimeProperty === event.propertyName ||
141 | TransitionInfo.isShorthandEqualProperty(
142 | maxTimeProperty, event.propertyName, propertyArray
143 | ) ||
144 | maxTimeProperty === 'all' && !TransitionInfo.isInPropertyList(
145 | event.propertyName, propertyArray
146 | )) {
147 | node.removeEventListener('transitionend', this._handleReference);
148 | callback();
149 | }
150 | }
151 | },
152 |
153 | _computeNewStyle: function (phase) {
154 | var phaseStyle;
155 |
156 | if (phase === 'appear') phaseStyle = this.props.childrenAppearStyle;
157 | else if (phase === 'enter') phaseStyle = this.props.childrenEnterStyle;
158 | else if (phase === 'leave') phaseStyle = this.props.childrenLeaveStyle;
159 |
160 | return Object.assign(
161 | {}, this.props.childrenBaseStyle, phaseStyle, this.props.style
162 | );
163 | },
164 |
165 | _transition: function (callback, phase) {
166 | if ((phase === 'appear' && !this.props.childrenAppearStyle) ||
167 | (phase === 'enter' && !this.props.childrenEnterStyle) ||
168 | (phase === 'leave' && !this.props.childrenLeaveStyle)) {
169 | callback();
170 | }
171 | else {
172 | var frameId = Animation.requestNextFrame((function () {
173 | this._executeTransition(callback, phase);
174 | }).bind(this));
175 | this._frameIds.push(frameId);
176 | }
177 | },
178 |
179 | _executeTransition: function (callback, phase) {
180 | var node = ReactDOM.findDOMNode(this);
181 | if (!node) return;
182 |
183 | var nextStyle = this._computeNewStyle(phase);
184 | var transitionValues = TransitionParser.getTransitionValues(nextStyle);
185 |
186 | var maxTimeProperty = TransitionInfo.getMaximumTimeProperty(
187 | transitionValues
188 | );
189 |
190 | node.removeEventListener('transitionend', this._handleReference);
191 |
192 | if (maxTimeProperty) {
193 | // To guarantee the transitionend event of another phase will not
194 | // interfere with the handler of the current phase create a new one
195 | // every time.
196 | this._handleReference = this._handleTransitionEnd.bind(
197 | this,
198 | node,
199 | maxTimeProperty,
200 | transitionValues.transitionProperty,
201 | callback
202 | );
203 | node.addEventListener('transitionend', this._handleReference);
204 | }
205 | else {
206 | callback();
207 | }
208 |
209 | // Using setAttribute() or the functions in CSSPropertyOperations would
210 | // probably be faster, but stateless components are not working well with
211 | // this approach.
212 | this.setState({style: nextStyle});
213 |
214 | this._phase = phase;
215 | },
216 |
217 | render: function () {
218 | if (this.props.children) {
219 | return React.Children.only(
220 | React.cloneElement(this.props.children, {
221 | style: this.state.style,
222 | })
223 | );
224 | }
225 | // Null.
226 | return this.props.children;
227 | },
228 |
229 | });
230 |
231 | module.exports = TransitionChild;
232 |
--------------------------------------------------------------------------------
/src/TransitionInfo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2016, Felipe Thomé
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 |
9 | /**
10 | * Extract information of the parsed values of CSS transitions obtained from
11 | * TransitionParser.
12 | *
13 | * Specs: https://www.w3.org/TR/css3-transitions/
14 | */
15 |
16 | var AnimatedProperties = require('./AnimatedProperties');
17 |
18 | var getMaximumTimeProperty = function (transitions) {
19 | var longestTime = 0;
20 | var longestTimeProperty = '';
21 | var duration = -1;
22 | var delay = -1;
23 |
24 | if (transitions.transitionProperty) {
25 | var propertyArray = transitions.transitionProperty;
26 | var durationArray = transitions.transitionDuration;
27 | var delayArray = transitions.transitionDelay;
28 |
29 | if (durationArray === undefined || durationArray.length === 0) {
30 | durationArray = [0];
31 | }
32 |
33 | if (delayArray === undefined || delayArray.length === 0) {
34 | delayArray = [0];
35 | }
36 |
37 | for (var i = 0; i < propertyArray.length; i++) {
38 | duration = durationArray[i % durationArray.length];
39 | delay = delayArray[i % delayArray.length];
40 |
41 | if (duration + delay >= longestTime) {
42 | longestTime = duration + delay;
43 | longestTimeProperty = propertyArray[i];
44 | }
45 | }
46 | }
47 |
48 | if (longestTime === 0) return '';
49 |
50 | return longestTimeProperty;
51 | };
52 |
53 | var isInPropertyList = function (property, propertyArray) {
54 | var shorthandArray = AnimatedProperties.getShorthandNames(property);
55 | shorthandArray.push(property);
56 |
57 | for (var i = 0; i < shorthandArray.length; i++) {
58 | if (propertyArray.indexOf(shorthandArray[i]) >= 0) return true;
59 | }
60 |
61 | return false;
62 | };
63 |
64 | var isShorthandEqualProperty = function (shorthand, property, propertyArray) {
65 | var shorthandArray = AnimatedProperties.getShorthandNames(property);
66 | var idx = shorthandArray.indexOf(shorthand);
67 |
68 | if (idx === -1) return false;
69 |
70 | // Elements with a higher index in the shorthand array have a greater
71 | // specificity. So if one of the other elements in the shorthand array
72 | // is present in the property list we should return false because this
73 | // property we are current analysing is actually part of this other shorthand.
74 | // E.g.: shorthand = 'border', propertyArray = ['border', 'border-bottom'],
75 | // property = 'border-bottom-width'.
76 | for (var i = idx + 1; i < shorthandArray.length; i++) {
77 | if (isInPropertyList(shorthandArray[i], propertyArray)) return false;
78 | }
79 |
80 | return true;
81 | };
82 |
83 | module.exports = {
84 | getMaximumTimeProperty: getMaximumTimeProperty,
85 | isInPropertyList: isInPropertyList,
86 | isShorthandEqualProperty: isShorthandEqualProperty,
87 | };
--------------------------------------------------------------------------------
/src/TransitionParser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2016, Felipe Thomé
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | */
8 |
9 | /**
10 | * Parse the property, duration and delay of CSS transitions in React style
11 | * objects. The timing function is not considered, because it is not important
12 | * for the purpose of ReactInlineTransitionGroup.
13 | *
14 | * Also, it is not responsability of this parser to make the match between
15 | * properties and their delays and duration values. This will be performed by
16 | * the TransitionInfo module.
17 | *
18 | * Specs: https://www.w3.org/TR/css3-transitions/
19 | */
20 |
21 | var StringCache = require('./StringCache');
22 |
23 | // Map a transition string with its processed values. If the string is a
24 | // shorthand transition, the value will be an array of 3 positions, where each
25 | // position is another array with the processed values of properties,
26 | // durations and delays, respectively.
27 | var cache = new StringCache(150);
28 |
29 | var _parseNumericValues = function (valuesArray) {
30 | var ans = [];
31 | var re = /([0-9]*\.?[0-9]+)(m?s)/;
32 |
33 | for (var i = 0; i < valuesArray.length; i++) {
34 | var valuePieces = valuesArray[i].match(re);
35 | if (valuePieces) {
36 | if (valuePieces[2] === 's') ans.push(parseFloat(valuePieces[1] * 1000));
37 | else ans.push(parseFloat(valuePieces[1]));
38 | }
39 | else {
40 | throw new Error('Expected a time value instead of: ' + valuesArray[i]);
41 | }
42 | }
43 |
44 | return ans;
45 | };
46 |
47 | var _parseTransition = function (propertyStr, numeric) {
48 | var cachedValue = cache.get(propertyStr);
49 | if (cachedValue) return cachedValue;
50 |
51 | var values = propertyStr.toLowerCase().trim().split(/\s*,\s*/);
52 |
53 | if (numeric) {
54 | values = _parseNumericValues(values);
55 | }
56 |
57 | cache.set(propertyStr, values);
58 |
59 | return values;
60 | };
61 |
62 | var _parseShorthand = function (propertyStr) {
63 | var cachedValue = cache.get(propertyStr);
64 | if (cachedValue) return cachedValue;
65 |
66 | var propertyArray = [];
67 | var durationArray = [];
68 | var delayArray = [];
69 | var re = /^([0-9]*\.?[0-9]+)(m?s)$/;
70 | var transitions =
71 | propertyStr.toLowerCase().replace(/cubic\-bezier\((.*?)\)/g, '').trim()
72 | .split(/\s*,\s*/);
73 |
74 | for (var i = 0; i < transitions.length; i++) {
75 | var transitionPieces = transitions[i].split(/\s+/);
76 |
77 | propertyArray.push(transitionPieces[0]);
78 | durationArray.push(transitionPieces[1] || '0s');
79 | if (transitionPieces[2] === undefined) {
80 | delayArray.push('0s');
81 | }
82 | else if (re.test(transitionPieces[2])) {
83 | delayArray.push(transitionPieces[2]);
84 | }
85 | else if (transitionPieces[3] === undefined) {
86 | delayArray.push('0s');
87 | }
88 | else {
89 | delayArray.push(transitionPieces[3]);
90 | }
91 | }
92 |
93 | durationArray = _parseNumericValues(durationArray);
94 | delayArray = _parseNumericValues(delayArray);
95 |
96 | cache.set(propertyStr, [propertyArray, durationArray, delayArray]);
97 |
98 | return [propertyArray, durationArray, delayArray];
99 | };
100 |
101 | var getTransitionValues = function (styleObj) {
102 | var ans = {};
103 |
104 | if (styleObj === undefined) return ans;
105 |
106 | var keys = Object.keys(styleObj);
107 | var propertyFound = false;
108 | var durationFound = false;
109 | var delayFound = false;
110 |
111 | // Not guaranteed by spec, but normally Object.keys() return the keys in
112 | // the order they were assigned. Since, further transition keys in the
113 | // object will have priority over previous keys, iterate in descending order.
114 | for (var i = keys.length - 1; i >= 0; i--) {
115 | switch (keys[i]) {
116 | case 'transition':
117 | case 'WebkitTransition':
118 | case 'MozTransition':
119 | case 'msTransition':
120 | var shorthandValues = _parseShorthand(styleObj[keys[i]]);
121 | if (!propertyFound) ans.transitionProperty = shorthandValues[0];
122 | if (!durationFound) ans.transitionDuration = shorthandValues[1];
123 | if (!delayFound) ans.transitionDelay = shorthandValues[2];
124 | propertyFound = true;
125 | durationFound = true;
126 | delayFound = true;
127 | break;
128 | case 'transitionProperty':
129 | case 'WebkitTransitionProperty':
130 | case 'MozTransitionProperty':
131 | case 'msTransitionProperty':
132 | ans.transitionProperty = _parseTransition(styleObj[keys[i]]);
133 | propertyFound = true;
134 | break;
135 | case 'transitionDuration':
136 | case 'WebkitTransitionDuration':
137 | case 'MozTransitionDuration':
138 | case 'msTransitionDuration':
139 | ans.transitionDuration = _parseTransition(styleObj[keys[i]], true);
140 | durationFound = true;
141 | break;
142 | case 'transitionDelay':
143 | case 'WebkitTransitionDelay':
144 | case 'MozTransitionDelay':
145 | case 'msTransitionDelay':
146 | ans.transitionDelay = _parseTransition(styleObj[keys[i]], true);
147 | delayFound = true;
148 | break;
149 | }
150 |
151 | if (propertyFound && durationFound && delayFound) break;
152 | }
153 |
154 | return ans;
155 | };
156 |
157 | module.exports = {
158 | getTransitionValues: getTransitionValues,
159 | };
--------------------------------------------------------------------------------
/src/__tests__/Transition-test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-multi-comp, max-len, react/display-name */
2 |
3 | let React;
4 | let ReactDOM;
5 | let Transition;
6 | let installMockRAF;
7 | let TransitionEvent;
8 |
9 | describe('Transition', function () {
10 | let container;
11 |
12 | beforeEach(function () {
13 | jest.resetModuleRegistry();
14 | jest.useRealTimers();
15 |
16 | installMockRAF = require('./installMockRAF');
17 | installMockRAF();
18 |
19 | React = require('react');
20 | ReactDOM = require('react-dom');
21 | Transition = require('../Transition');
22 | TransitionEvent = require('./TransitionEvent');
23 |
24 | container = document.createElement('div');
25 | });
26 |
27 | it('should apply the base style in all children', function () {
28 | class Group extends React.Component {
29 | render() {
30 | const styles = {
31 | base: {
32 | background: 'red',
33 | },
34 | };
35 |
36 | return (
37 |
42 | 1
43 | 2
44 |
45 | );
46 | }
47 | }
48 |
49 | const container = document.createElement('div');
50 | const instance = ReactDOM.render( , container);
51 |
52 | const children = ReactDOM.findDOMNode(instance).getElementsByTagName('div');
53 |
54 | for (let i = 0; i < children.length; i++) {
55 | expect(children[i].style.background).toBe('red');
56 | }
57 | });
58 |
59 | it('should overwrite the phase style with props.style', function () {
60 | class Group extends React.Component {
61 | render() {
62 | var styles = {
63 | base: {
64 | background: 'red',
65 | },
66 |
67 | custom: {
68 | background: 'black',
69 | },
70 | };
71 |
72 | return (
73 |
78 | 1
79 |
80 | );
81 | }
82 | }
83 |
84 | const container = document.createElement('div');
85 | const instance = ReactDOM.render( , container);
86 |
87 | const children = ReactDOM.findDOMNode(instance).getElementsByTagName('div');
88 |
89 | for (let i = 0; i < children.length; i++) {
90 | expect(children[i].style.background).toEqual('black');
91 | }
92 | });
93 |
94 | it('should call the onPhaseStart and onPhaseEnd callbacks in the appear phase', function (done) {
95 | const log = [];
96 |
97 | const handlePhaseEnd = function (phase) {
98 | if (phase === 'appear') {
99 | log.push('end');
100 | expect(log).toEqual(['start', 'end']);
101 | done();
102 | }
103 | };
104 |
105 | const handlePhaseStart = function (phase) {
106 | if (phase === 'appear') {
107 | log.push('start');
108 | }
109 | };
110 |
111 | class Group extends React.Component {
112 | render() {
113 | const styles = {
114 | base: {
115 | background: 'red',
116 | },
117 |
118 | appear: {
119 | background: 'black',
120 | },
121 | };
122 |
123 | return (
124 |
132 | 1
133 |
134 | );
135 | }
136 | }
137 |
138 | ReactDOM.render( , container);
139 | });
140 |
141 | it('should call the onPhaseStart and onPhaseEnd callbacks in the enter phase', function (done) {
142 | const log = [];
143 |
144 | const handlePhaseEnd = function (phase) {
145 | if (phase === 'enter') {
146 | log.push('end');
147 | }
148 | };
149 |
150 | const handlePhaseStart = function (phase) {
151 | if (phase === 'enter') {
152 | log.push('start');
153 | }
154 | };
155 |
156 | class Group extends React.Component {
157 | state = {count: 1};
158 |
159 | render() {
160 | const styles = {
161 | base: {
162 | background: 'red',
163 | },
164 |
165 | appear: {
166 | background: 'black',
167 | },
168 | };
169 |
170 | const elems = [];
171 | for (let i = 0; i < this.state.count; i++) {
172 | elems.push({i}
);
173 | }
174 |
175 | return (
176 |
184 | {elems}
185 |
186 | );
187 | }
188 | }
189 |
190 | const instance = ReactDOM.render( , container);
191 |
192 | instance.setState({count: 2}, function () {
193 | expect(log).toEqual(['start', 'end']);
194 | done();
195 | });
196 | });
197 |
198 | it('should call the onPhaseStart and onPhaseEnd callbacks in the leave phase', function (done) {
199 | const log = [];
200 |
201 | const terminateTest = function () {
202 | expect(log).toEqual(['start', 'end']);
203 | done();
204 | };
205 |
206 | const handlePhaseEnd = function (phase) {
207 | if (phase === 'leave') {
208 | log.push('end');
209 | terminateTest();
210 | }
211 | };
212 |
213 | const handlePhaseStart = function (phase) {
214 | if (phase === 'leave') {
215 | log.push('start');
216 | }
217 | };
218 |
219 | class Group extends React.Component {
220 | state = {count: 1};
221 |
222 | render() {
223 | const styles = {
224 | base: {
225 | background: 'red',
226 | },
227 |
228 | appear: {
229 | background: 'black',
230 | },
231 | };
232 |
233 | const elems = [];
234 | for (let i = 0; i < this.state.count; i++) {
235 | elems.push({i}
);
236 | }
237 |
238 | return (
239 |
247 | {elems}
248 |
249 | );
250 | }
251 | }
252 |
253 | const instance = ReactDOM.render( , container);
254 |
255 | instance.setState({count: 0});
256 | });
257 |
258 | it('should apply different styles for appear and enter phases', function (done) {
259 | let instance;
260 | let children;
261 | const log = [];
262 |
263 | const terminateTest = function () {
264 | children = ReactDOM.findDOMNode(instance).getElementsByTagName('div');
265 |
266 | log.push(children[0].style.background);
267 | log.push(children[1].style.background);
268 | expect(log).toEqual(['black', 'blue']);
269 |
270 | done();
271 | };
272 |
273 | const handlePhaseEnd = function (phase) {
274 | if (phase === 'leave') {
275 | terminateTest();
276 | }
277 | };
278 |
279 | class Group extends React.Component {
280 | state = {count: 1};
281 |
282 | render() {
283 | const styles = {
284 | base: {
285 | background: 'red',
286 | },
287 |
288 | appear: {
289 | background: 'black',
290 | },
291 |
292 | enter: {
293 | background: 'blue',
294 | },
295 | };
296 |
297 | const elems = [];
298 | for (let i = 0; i < this.state.count; i++) {
299 | elems.push({i}
);
300 | }
301 |
302 | return (
303 |
311 | {elems}
312 |
313 | );
314 | }
315 | }
316 |
317 | instance = ReactDOM.render( , container);
318 | instance.setState({count: 3});
319 | instance.setState({count: 2});
320 | });
321 |
322 | it('should handle correctly the transitionend event', function (done) {
323 | const handlePhaseEnd = function (phase) {
324 | if (phase === 'appear') {
325 | done();
326 | }
327 | };
328 |
329 | class Group extends React.Component {
330 | state = {count: 1};
331 |
332 | render() {
333 | const styles = {
334 | base: {
335 | background: 'red',
336 | },
337 |
338 | appear: {
339 | background: 'black',
340 | transition: 'background 1s',
341 | },
342 | };
343 |
344 | const elems = [];
345 | for (let i = 0; i < this.state.count; i++) {
346 | elems.push({i}
);
347 | }
348 |
349 | return (
350 |
357 | {elems}
358 |
359 | );
360 | }
361 | }
362 |
363 | const instance = ReactDOM.render( , container);
364 | const children = ReactDOM.findDOMNode(instance).getElementsByTagName('div');
365 |
366 | var event = new TransitionEvent('transitionend', {
367 | propertyName: 'background-color',
368 | });
369 | children[0].dispatchEvent(event);
370 | });
371 | });
372 |
--------------------------------------------------------------------------------
/src/__tests__/TransitionEvent.js:
--------------------------------------------------------------------------------
1 | class TransitionEvent extends Event {
2 | constructor(type, options) {
3 | super(type);
4 | for (var key in options) {
5 | if (options.hasOwnProperty(key)) this[key] = options[key];
6 | }
7 | }
8 | }
9 |
10 | module.exports = TransitionEvent;
--------------------------------------------------------------------------------
/src/__tests__/TransitionInfo-test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 |
3 | const TransitionInfo = require('../TransitionInfo');
4 |
5 | describe('TransitionInfo', function () {
6 | it('should return the property with the longest time', function () {
7 | const transitionValues = {
8 | transitionProperty: ['background', 'height', 'width'],
9 | transitionDuration: [1000, 2000, 1000],
10 | transitionDelay: [0, 0, 2000],
11 | };
12 |
13 | const property = TransitionInfo.getMaximumTimeProperty(transitionValues);
14 |
15 | expect(property).toEqual('width');
16 | });
17 |
18 | it('should return the longest time property with lists of different size', function () {
19 | const transitionValues = {
20 | transitionProperty: ['background', 'height', 'width'],
21 | transitionDuration: [1000],
22 | transitionDelay: [1000, 2000],
23 | };
24 |
25 | const property = TransitionInfo.getMaximumTimeProperty(transitionValues);
26 |
27 | expect(property).toEqual('height');
28 | });
29 |
30 | it('should return an empty string when there is no valid transition', function () {
31 | const transitionValues = {
32 | transitionProperty: ['background', 'height', 'width'],
33 | };
34 |
35 | const property = TransitionInfo.getMaximumTimeProperty(transitionValues);
36 |
37 | expect(property).toEqual('');
38 | });
39 |
40 | it('should return an empty when duration is zero', function () {
41 | const transitionValues = {
42 | transitionProperty: ['background', 'height', 'width'],
43 | transitionDuration: [0],
44 | };
45 |
46 | const property = TransitionInfo.getMaximumTimeProperty(transitionValues);
47 |
48 | expect(property).toEqual('');
49 | });
50 |
51 | it('should return the right property when "all" is present', function () {
52 | const transitionValues = {
53 | transitionProperty: ['all', 'height', 'width'],
54 | transitionDuration: [1000, 2000, 1000],
55 | };
56 |
57 | const property = TransitionInfo.getMaximumTimeProperty(transitionValues);
58 |
59 | expect(property).toEqual('height');
60 | });
61 |
62 | it('should check if a property is in the property list', function () {
63 | const propertyArray = ['height', 'width'];
64 |
65 | const isPresent = TransitionInfo.isInPropertyList('height', propertyArray);
66 |
67 | expect(isPresent).toEqual(true);
68 | });
69 |
70 | it('should check if a property is in the property list as a shorthand', function () {
71 | const propertyArray = ['border', 'height', 'width'];
72 |
73 | const isPresent = TransitionInfo.isInPropertyList(
74 | 'border-bottom-width', propertyArray
75 | );
76 |
77 | expect(isPresent).toEqual(true);
78 | });
79 |
80 | it('should return false if a property is not in the property list', function () {
81 | const propertyArray = ['border', 'height', 'width'];
82 |
83 | const isPresent = TransitionInfo.isInPropertyList('opacity', propertyArray);
84 |
85 | expect(isPresent).toEqual(false);
86 | });
87 |
88 | it('should return false if a property is not in the property list not even as shorthand', function () {
89 | const propertyArray = ['border', 'height', 'width'];
90 |
91 | const isPresent = TransitionInfo.isInPropertyList('border-bottom', propertyArray);
92 |
93 | expect(isPresent).toEqual(false);
94 | });
95 |
96 | it('should check if a property is equal a shorthand based on the property list', function () {
97 | const propertyArray = ['border', 'height', 'width'];
98 |
99 | const isPresent = TransitionInfo.isShorthandEqualProperty(
100 | 'border', 'border-bottom-width', propertyArray
101 | );
102 |
103 | expect(isPresent).toEqual(true);
104 | });
105 |
106 | it('should handle correctly shorthands with higher specificity', function () {
107 | const propertyArray = ['border', 'border-bottom', 'width'];
108 |
109 | let isPresent = TransitionInfo.isShorthandEqualProperty(
110 | 'border', 'border-bottom-width', propertyArray
111 | );
112 |
113 | expect(isPresent).toEqual(false);
114 |
115 | isPresent = TransitionInfo.isShorthandEqualProperty(
116 | 'border-bottom', 'border-bottom-width', propertyArray
117 | );
118 |
119 | expect(isPresent).toEqual(true);
120 | });
121 | });
--------------------------------------------------------------------------------
/src/__tests__/TransitionParser-test.js:
--------------------------------------------------------------------------------
1 | let TransitionParser;
2 |
3 | describe('TransitionParser', function () {
4 |
5 | beforeEach(function () {
6 | jest.resetModuleRegistry();
7 | TransitionParser = require('../TransitionParser');
8 | });
9 |
10 | it('should return an empty object when there is no transition', function () {
11 | const style = {
12 | background: '#FFF',
13 | height: '50px',
14 | width: '50px',
15 | };
16 |
17 | const transitions = TransitionParser.getTransitionValues(style);
18 |
19 | expect(transitions).toEqual({});
20 | });
21 |
22 | it('should parse transition shorthand correctly', function () {
23 | const style = {
24 | background: '#FFF',
25 | height: '50px',
26 | width: '50px',
27 | transition: 'background 1s 1s, height 2s ' +
28 | 'color 2s cubic-bezier(0.25, 0, .45, 2), ' +
29 | 'width 3s cubic-bezier(0.15, 1, 0.75, 4)',
30 | };
31 |
32 | const transitions = TransitionParser.getTransitionValues(style);
33 |
34 | expect(transitions).toEqual({
35 | transitionProperty: ['background', 'height', 'width'],
36 | transitionDuration: [1000, 2000, 3000],
37 | transitionDelay: [1000, 2000, 0],
38 | });
39 | });
40 |
41 | it('should parse transitions with different lengths correctly', function () {
42 | const style = {
43 | background: '#FFF',
44 | height: '50px',
45 | width: '50px',
46 | transitionProperty: 'background, height, width',
47 | transitionDuration: '1s, 2s',
48 | transitionDelay: '3s',
49 | };
50 |
51 | const transitions = TransitionParser.getTransitionValues(style);
52 |
53 | expect(transitions).toEqual({
54 | transitionProperty: ['background', 'height', 'width'],
55 | transitionDuration: [1000, 2000],
56 | transitionDelay: [3000],
57 | });
58 | });
59 |
60 | it('should parse respecting the order of appearance', function () {
61 | let style = {
62 | background: '#FFF',
63 | height: '50px',
64 | transition: 'background 1s 3s, height 2s 3s',
65 | transitionDelay: '4s',
66 | };
67 |
68 | let transitions = TransitionParser.getTransitionValues(style);
69 |
70 | expect(transitions).toEqual({
71 | transitionProperty: ['background', 'height'],
72 | transitionDuration: [1000, 2000],
73 | transitionDelay: [4000],
74 | });
75 |
76 | style = {
77 | background: '#FFF',
78 | height: '50px',
79 | transitionDelay: '4s',
80 | transition: 'background 1s 3s, height 2s 3s',
81 | };
82 |
83 | transitions = TransitionParser.getTransitionValues(style);
84 |
85 | expect(transitions).toEqual({
86 | transitionProperty: ['background', 'height'],
87 | transitionDuration: [1000, 2000],
88 | transitionDelay: [3000, 3000],
89 | });
90 |
91 | style = {
92 | background: '#FFF',
93 | height: '50px',
94 | transitionDelay: '4s',
95 | transition: 'background 1s 3s, height 2s 3s',
96 | transitionDuration: '5s, 6s',
97 | };
98 |
99 | transitions = TransitionParser.getTransitionValues(style);
100 |
101 | expect(transitions).toEqual({
102 | transitionProperty: ['background', 'height'],
103 | transitionDuration: [5000, 6000],
104 | transitionDelay: [3000, 3000],
105 | });
106 | });
107 |
108 | it('should parse the "all" property correctly', function () {
109 | const style = {
110 | background: '#FFF',
111 | height: '50px',
112 | width: '50px',
113 | transitionProperty: 'all, background',
114 | transitionDuration: '1s, 2s',
115 | };
116 |
117 | const transitions = TransitionParser.getTransitionValues(style);
118 |
119 | expect(transitions).toEqual({
120 | transitionProperty: ['all', 'background'],
121 | transitionDuration: [1000, 2000],
122 | });
123 | });
124 |
125 | it('should parse the "all" property in shorthands correctly', function () {
126 | const style = {
127 | background: '#FFF',
128 | height: '50px',
129 | width: '50px',
130 | transition: 'all 1s ease-out 1s, background 2s',
131 | };
132 |
133 | const transitions = TransitionParser.getTransitionValues(style);
134 |
135 | expect(transitions).toEqual({
136 | transitionProperty: ['all', 'background'],
137 | transitionDuration: [1000, 2000],
138 | transitionDelay: [1000, 0],
139 | });
140 | });
141 |
142 | it('should not differentiate uppercase and lowercase letters', function () {
143 | let style = {
144 | background: '#FFF',
145 | height: '50px',
146 | width: '50px',
147 | transition: 'AlL 1S, backgroUnD 2s',
148 | };
149 |
150 | let transitions = TransitionParser.getTransitionValues(style);
151 |
152 | expect(transitions).toEqual({
153 | transitionProperty: ['all', 'background'],
154 | transitionDuration: [1000, 2000],
155 | transitionDelay: [0, 0],
156 | });
157 |
158 | style = {
159 | background: '#FFF',
160 | height: '50px',
161 | width: '50px',
162 | transitionProperty: 'AlL, backgroUnD',
163 | transitionDuration: '1S, 2s',
164 | };
165 |
166 | transitions = TransitionParser.getTransitionValues(style);
167 |
168 | expect(transitions).toEqual({
169 | transitionProperty: ['all', 'background'],
170 | transitionDuration: [1000, 2000],
171 | });
172 | });
173 |
174 | it('should handle "s" and "ms" units', function () {
175 | let style = {
176 | background: '#FFF',
177 | height: '50px',
178 | width: '50px',
179 | transition: 'all 1s, background 20ms',
180 | };
181 |
182 | let transitions = TransitionParser.getTransitionValues(style);
183 |
184 | expect(transitions).toEqual({
185 | transitionProperty: ['all', 'background'],
186 | transitionDuration: [1000, 20],
187 | transitionDelay: [0, 0],
188 | });
189 |
190 | style = {
191 | background: '#FFF',
192 | height: '50px',
193 | width: '50px',
194 | transitionProperty: 'all, background',
195 | transitionDuration: '1s, 20ms',
196 | };
197 |
198 | transitions = TransitionParser.getTransitionValues(style);
199 |
200 | expect(transitions).toEqual({
201 | transitionProperty: ['all', 'background'],
202 | transitionDuration: [1000, 20],
203 | });
204 | });
205 |
206 | it('should handle extra spaces correctly', function () {
207 | let style = {
208 | background: '#FFF',
209 | height: '50px',
210 | width: '50px',
211 | transition: ' all 1s ,background 20ms ',
212 | };
213 |
214 | let transitions = TransitionParser.getTransitionValues(style);
215 |
216 | expect(transitions).toEqual({
217 | transitionProperty: ['all', 'background'],
218 | transitionDuration: [1000, 20],
219 | transitionDelay: [0, 0],
220 | });
221 |
222 | style = {
223 | background: '#FFF',
224 | height: '50px',
225 | width: '50px',
226 | transitionProperty: ' all , background',
227 | transitionDuration: '1s,20ms',
228 | };
229 |
230 | transitions = TransitionParser.getTransitionValues(style);
231 |
232 | expect(transitions).toEqual({
233 | transitionProperty: ['all', 'background'],
234 | transitionDuration: [1000, 20],
235 | });
236 | });
237 |
238 | it('should handle browser prefixes correctly', function () {
239 | let style = {
240 | background: '#FFF',
241 | height: '50px',
242 | width: '50px',
243 | transition: 'all 1s ,background 20ms',
244 | WebkitTransition: 'all 1s ,background 2s',
245 | };
246 |
247 | let transitions = TransitionParser.getTransitionValues(style);
248 |
249 | expect(transitions).toEqual({
250 | transitionProperty: ['all', 'background'],
251 | transitionDuration: [1000, 2000],
252 | transitionDelay: [0, 0],
253 | });
254 |
255 | style = {
256 | background: '#FFF',
257 | height: '50px',
258 | width: '50px',
259 | transition: 'all 1s ,background 20ms',
260 | MozTransition: 'all 1s ,background 2s',
261 | };
262 |
263 | transitions = TransitionParser.getTransitionValues(style);
264 |
265 | expect(transitions).toEqual({
266 | transitionProperty: ['all', 'background'],
267 | transitionDuration: [1000, 2000],
268 | transitionDelay: [0, 0],
269 | });
270 |
271 | style = {
272 | background: '#FFF',
273 | height: '50px',
274 | width: '50px',
275 | transition: 'all 1s ,background 20ms',
276 | msTransition: 'all 1s ,background 2s',
277 | };
278 |
279 | transitions = TransitionParser.getTransitionValues(style);
280 |
281 | expect(transitions).toEqual({
282 | transitionProperty: ['all', 'background'],
283 | transitionDuration: [1000, 2000],
284 | transitionDelay: [0, 0],
285 | });
286 | });
287 |
288 | it('should throw an error when was expected a numeric value', function () {
289 | const style = {
290 | background: '#FFF',
291 | transition: 'background ease-out 1s',
292 | };
293 |
294 | expect(function () {
295 | TransitionParser.getTransitionValues(style);
296 | }).toThrow();
297 | });
298 |
299 | });
--------------------------------------------------------------------------------
/src/__tests__/installMockRAF.js:
--------------------------------------------------------------------------------
1 | module.exports = function () {
2 | global.requestAnimationFrame = function (cb) {
3 | cb();
4 | };
5 |
6 | global.cancelAnimationFrame = function () {};
7 | };
--------------------------------------------------------------------------------
/src/shallowEqual.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Based on the shallowEqual.js code in fbjs.
3 | * Copyright (c) 2013-present, Facebook, Inc.
4 | *
5 | * Copyright (c) 2016, Felipe Thomé
6 | * All rights reserved.
7 | *
8 | * This source code is licensed under the BSD-style license found in the
9 | * LICENSE file in the root directory of this source tree.
10 | */
11 |
12 | var hasOwnProperty = Object.prototype.hasOwnProperty;
13 |
14 | function is(val1, val2) {
15 | // In this project, it's not important the distinction between -0 and +0.
16 | return val1 === val2 || (val1 !== val1 && val2 !== val2);
17 | }
18 |
19 | module.exports = function (o1, o2) {
20 | if (o1 === o2) {
21 | return true;
22 | }
23 |
24 | if (o1 === undefined || o1 === null || o2 === undefined || o2 === null) {
25 | return false;
26 | }
27 |
28 | var k1 = Object.keys(o1);
29 | var k2 = Object.keys(o2);
30 |
31 | if (k1.length !== k2.length) {
32 | return false;
33 | }
34 |
35 | for (var i = 0; i < k1.length; i++) {
36 | if (!hasOwnProperty.call(o2, k1[i]) || !is(o1[k1[i]], o2[k1[i]])) {
37 | return false;
38 | }
39 | }
40 |
41 | return true;
42 | };
--------------------------------------------------------------------------------