├── .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 | [![Build Status](https://travis-ci.org/felipethome/react-inline-transition-group.svg?branch=master)](https://travis-ci.org/felipethome/react-inline-transition-group) [![Coverage Status](https://coveralls.io/repos/github/felipethome/react-inline-transition-group/badge.svg)](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 | 93 | 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 |
169 |
170 | 176 | ReactInlineTransitionGroup + ReactTransitionHooks 177 |
178 |
179 | 184 | ReactCSSTransitionGroup + ReactTransitionGroup 185 |
186 |
187 |
188 | Press the Switch Image button before the transition finishes to see 189 | the difference between the components. 190 |
191 |
192 | 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 | 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 | 43 | , 44 | 45 | 46 | , 47 | 48 | 49 | , 50 | 51 | 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 | 142 | 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 |
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 |
159 |
160 | 166 | ReactInlineTransitionGroup + ReactTransitionHooks 167 |
168 |
169 | 174 | ReactCSSTransitionGroup + ReactTransitionGroup 175 |
176 |
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 | }; --------------------------------------------------------------------------------