├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo ├── app.js ├── components │ ├── box.jsx │ ├── emoji-span.jsx │ └── loading-crossfade-component.jsx ├── css │ ├── flexbox.css │ ├── loading-placeholder.css │ ├── polyfill.css │ └── reset.css ├── examples │ ├── crossfade-example.jsx │ ├── flap-box.jsx │ ├── scrolling-group.jsx │ ├── toggle-box.jsx │ └── trigger-box.jsx ├── index.html ├── main.jsx └── webpack.config.js ├── package-lock.json ├── package.json └── src ├── index.js ├── lib └── velocity-animate-shim.js ├── velocity-component.js ├── velocity-helpers.js └── velocity-transition-group.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | 'presets': ['react', 'es2015'], 3 | 'plugins': [ 4 | 'transform-object-rest-spread', 5 | 'transform-class-properties' 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /index.js 3 | /velocity-component.js 4 | /velocity-helpers.js 5 | /velocity-transition-group.js 6 | /lib 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "plugins": [ 9 | "import", 10 | "react", 11 | "prettier" 12 | ], 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:react/recommended", 16 | "prettier", 17 | "prettier/react" 18 | ], 19 | "rules": { 20 | "prettier/prettier": ["error", { 21 | "singleQuote": true, 22 | "trailingComma": "es5" 23 | }] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | *~ 5 | npm-debug.log 6 | yarn.lock 7 | 8 | /index.js 9 | /velocity-component.js 10 | /velocity-transition-group.js 11 | /velocity-helpers.js 12 | /lib 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /demo 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.4.3 (2019-05-09): 2 | 3 | Adds `timeout` prop to `Transition` in test to avoid a PropTypes warning. 4 | 5 | ### v1.4.2 (2018-11-14): 6 | 7 | Uses a `` element in test/server mode to keep react-dom from warning 8 | about TransitionGroup’s props placed on HTML children. 9 | 10 | ### v1.4.1 (2018-04-17): 11 | 12 | Fixes false children bug #240. (Thanks for the report, @tiffanytangt!) 13 | 14 | ### v1.4.0 (2018-04-12): 15 | 16 | Dependency upgrades! No new features. 17 | 18 | - Changes our `componentWillUpdate` call to `componentDidUpdate` to eliminate a 19 | React 16.3 StrictMode warning 20 | - Migration to [react-transition-group](https://github.com/reactjs/react-transition-group) v2 21 | - Switches from lodash 3 to lodash 4 22 | - Changes demo to use React 16.3 23 | 24 | There still are StrictMode warnings due to `react-transition-group` still using 25 | the older APIs. 26 | 27 | ### v1.3.3 (2017-05-19): 28 | 29 | Fixes incorrect timer clearing. (Thanks, @dmeiz1) 30 | 31 | ### v1.3.2 (2017-05-11): 32 | 33 | Fix for calling clearTimeout in IE 8. (Thanks, @prchaoz!) 34 | 35 | Additional updates to peerDependencies around the new React 15.5-compatible 36 | libraries. (Thanks, @MichaelDeBoey!) 37 | 38 | ### v1.3.1 (2017-05-09): 39 | 40 | Fixes React peer dependencies to specify versions that work with 41 | `prop-types`. (Thanks, @MichaelDeBoey!) 42 | 43 | ### v1.3.0 (2017-05-08): 44 | 45 | Removes warnings from React 15.5 for React 16.0 compatibility. 46 | 47 | Converts to ES2015 classes, adds compilation on distribution. 48 | 49 | Minor version bump due to changes around removal of isMounted checks, so there 50 | may be some regressions. 51 | 52 | ### v1.2.2 (2017-04-19): 53 | 54 | Adds `interruptBehavior` prop to `VelocityComponent`. (Thanks, @Robinfr!) 55 | 56 | Changes to remove warnings in React 15.5 will come shortly in v1.3. 57 | 58 | ### v1.2.1 (2017-01-19): 59 | 60 | This is a minor version bump that updates the version of babel to the latest (v6). 61 | 62 | ### v1.2.0 (2017-01-12): 63 | 64 | This is a minor version bump due to the `velocity-animate` dependency getting a minor version 65 | bump to 1.4 that may contain timing changes. See `velocity-animate` changes here: 66 | 67 | https://github.com/julianshapiro/velocity/compare/1.3.0...1.4.0 68 | 69 | Thanks to @matthewjf for helping track this down as an issue with how Velocity before 1.4 70 | fired `complete` callbacks on multiple elements animating. 71 | 72 | #### Bug fixes: 73 | * Check for presence of `navigator` before using it to determine if we’re running 74 | in a browser. (Thanks, @alampros!) 75 | * Fix undefined `forEach` method call when doing recursive cache-cleaning without 76 | jQuery. (Thanks, @kennygwang!) 77 | 78 | ### v1.1.11 (2016-10-20): 79 | 80 | Bump `velocity-animate` to v1.3.1 once more now that it's published to npm! 81 | 82 | ### v1.1.10 (2016-10-17): 83 | 84 | Reverts `velocity-animate` bump to v1.3.1, it's apparently not published to npm yet :( 85 | 86 | ### v1.1.9 (2016-10-17): 87 | 88 | Bump `velocity-animate` to v1.3.1 (fixes https://github.com/julianshapiro/velocity/issues/305) 89 | 90 | ### v1.1.8 (2016-10-07): 91 | 92 | #### Bug fixes 93 | * Fix for jsdom check in IE (Thanks, @stephenleicht!) 94 | 95 | ### v1.1.7 (2016-10-03): 96 | 97 | #### Bug fixes 98 | * Fix for removeData in shim mode (Issue #127) 99 | 100 | ### v1.1.6 (2016-09-28): 101 | 102 | #### Bug fixes 103 | * Prevent Velocity memory leaks when not using jQuery (Thanks, @eirikurn!) 104 | * Compatibility with SystemJS's "require" parsing (Thanks, @luisherranz!) 105 | * Enable Velocity shim even when JSDom is present by testing process.env (Thanks, @Tomyail!) 106 | 107 | ### v1.1.5 (2016-04-11): 108 | 109 | Updates to allow React 15. (Thanks, @StevenLangbroek!) 110 | 111 | #### Bug fixes 112 | * Passes context into VelocityTransitionGroup animations' “complete” callbacks 113 | so that they know what elements finished animating. 114 | 115 | 116 | ### v1.1.4 (2016-03-09): 117 | 118 | #### Bug fixes 119 | * Keeps leave’s "begin" callback from firing on enter. (Thanks, @SethTompkins!) 120 | 121 | 122 | ### v1.1.3 (2016-02-16): 123 | 124 | #### Bug fixes 125 | * Fixes “Invalid calling object” in rAF shim for IE. (Thanks, @restlessbit!) 126 | 127 | 128 | ### v1.1.2 (2016-02-08): 129 | 130 | #### Bug fixes 131 | * `complete` callbacks for `VelocityTransitionGroup` now fire correctly. 132 | * `velocityHelpers`’s `registerEffect` can be called (as a no-op) on the server. 133 | * Fallback to `setTimeout` if `requestAnimationFrame` is not defined. 134 | 135 | #### Features 136 | * `VelocityTransitionGroup` can take `enterHideStyle` and `enterShowStyle` props 137 | to customize how entering elements are kept from being shown before their 138 | animations start. 139 | 140 | 141 | ### v1.1.1 (2015-10-21): 142 | 143 | #### Bug fixes 144 | * Fix for `VelocityTransitionGroup` not animating when no `leave` prop is set. 145 | * Better detection of existing `Velocity` instances (thanks, @arush!). 146 | 147 | ### v1.1.0 (2015-10-08): 148 | 149 | Updated peerDependencies and requires to React 0.14. 150 | 151 | If you need to work with React 0.13, use v1.0.1. 152 | 153 | Special thanks to @jmfurlott for help updating everything that needed updating. 154 | 155 | ### v1.0.1 (2015-10-05): 156 | 157 | #### Bug fixes 158 | * Shims out Velocity when running on the server so that the components all no-op 159 | 160 | ### v1.0.0 (2015-09-29): 161 | 162 | Initial release! Contains `VelocityComponent` and `VelocityTransitionGroup`. 163 | 164 | Read the [blog post](https://fabric.io/blog/introducing-the-velocityreact-library) and 165 | enjoy the demos. 166 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests from everyone. By participating in this project, you 4 | agree to abide by the Twitter Open Source [code of conduct]. 5 | 6 | [code of conduct]: https://engineering.twitter.com/opensource/code-of-conduct 7 | 8 | It's recommended that you open an issue to discuss your feature and approach 9 | before sending the PR. 10 | 11 | Please adhere to the ES5 style present in the code. Use clear naming and add 12 | comments for any code or behavior that is not clear. 13 | 14 | Use the examples to make sure that your new code doesn't affect the existing 15 | behavior. 16 | 17 | If you're adding a new feature, please add to an existing example or make a 18 | new one to show it off. 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Google Inc. and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # velocity-react 2 | 3 | [React](http://facebook.github.io/react/) components for interacting with the 4 | [Velocity](http://velocityjs.org/) DOM animation library. 5 | 6 | Read our [announcement blog post](https://fabric.io/blog/introducing-the-velocityreact-library) for 7 | details about why and how we built this. 8 | 9 | See the [live demo](http://google-fabric.github.io/velocity-react/). 10 | 11 | **Latest version:** v1.4.2 Avoids react-dom warnings in test for `Transition` props. 12 | 13 | *Note: v1.1.0 and later require React 0.14.x* 14 | *Note: v1.3.0 and later require React 15.3.x and should work with React 16* 15 | 16 | ## Running the demo 17 | 18 | ``` 19 | $ git clone https://github.com/twitter-fabric/velocity-react.git 20 | $ cd velocity-react 21 | $ npm install 22 | $ npm run demo 23 | ``` 24 | 25 | Visit in your browser. Hot reloading is enabled, if you 26 | want to tweak the code in main.jsx. 27 | 28 | ## Installation 29 | 30 | The velocity-react library is provided as an [NPM package](https://www.npmjs.com/package/velocity-react): 31 | 32 | ``` 33 | $ npm install --save velocity-react 34 | ``` 35 | 36 | The `VelocityComponent` and `VelocityTransitionGroup` components, as well as the `velocityHelpers` 37 | utilities, are distributed as ES5-compatible JavaScript files with [CommonJS](http://www.commonjs.org/) 38 | `require` statements. You will need a dependency tool such as [Browserify](http://browserify.org/), 39 | [RequireJS](http://requirejs.org/), or [webpack](https://webpack.github.io/) to use them on the web. 40 | 41 | This package depends directly on Velocity, as well as [lodash](https://lodash.com/) for a handful 42 | of utility functions (which are required individually to try and keep bundle size down). 43 | 44 | To use the Velocity UI Pack, which includes a library of transitions and support for the `stagger`, 45 | `drag`, and `backwards` options, require it along with Velocity at an entry point to your app: 46 | ```JS 47 | require('velocity-animate'); 48 | require('velocity-animate/velocity.ui'); 49 | ``` 50 | 51 | **Note**: Depending upon where your version of NPM places dependencies, you may need to explicitly 52 | require `velocity-animate` in your package.json to be able to require `velocity-animate/velocity.ui`. 53 | 54 | It is assumed that your application already depends on `react` and `react-dom` at v15. If you're 55 | still at React 0.13, use v1.0.1 of this package. Other than dependencies, it is the same as v1.1.0. 56 | 57 | ## Usage 58 | 59 | ### `VelocityComponent` 60 | 61 | Component to add Velocity animations to a child node or one or more of its descendants. Wraps a single 62 | child without adding additional DOM nodes. 63 | 64 | #### Example 65 | ```JSX 66 | 67 | 68 | 69 | ``` 70 | 71 | #### Details 72 | 73 | The API attempts to be as declarative as possible. A single `animation` property declares what 74 | animation the child should have. If that property changes, this component applies the new animation 75 | to the child on the next tick. 76 | 77 | By default, the animation is not run when the component is mounted. Instead, Velocity's `finish` 78 | command is used to jump to the animation's end state. For a component that animates out of and 79 | back in to a default state, this allows the parent to specify the "animate into" animation as 80 | the default, and therefore not have to distinguish between the initial state and the state to 81 | return to after animating away. 82 | 83 | #### Properties 84 | 85 | `animation`: Either an animation key or hash defining the animation. See Velocity's documentation 86 | for what this can be. (It is passed to Velocity exactly.) 87 | 88 | `runOnMount`: If true, runs the animation even when the component is first mounted. 89 | 90 | `targetQuerySelector`: By default, this component's single child is animated. If `targetQuerySelector` 91 | is provided, it is used to select descendants to apply the animation to. Use with caution, only 92 | when you're confident that React's reconciliation will preserve these nodes during animation. 93 | Also note `querySelectorAll`'s [silly behavior](http://ejohn.org/blog/thoughts-on-queryselectorall/) w.r.t. pruning results when being called on a node. 94 | A special value of "children" will use the direct children of the node, since there isn't a 95 | great way to specify that to `querySelectorAll`. 96 | 97 | `interruptBehavior`: Specifies what should happen to an in-progress animation if the `animation` 98 | property changes. By default is `stop`, which stops it at its current position, letting the new 99 | animation use that as a starting point. This generally gives the smoothest results. Other options 100 | are `finish`, which jumps the animation to its end state, and `queue`, which will proceed with 101 | the new animation only after the old one has finished. These options may be necessary to avoid 102 | bad behavior when certain built-in effects like "slideUp" and "slideDown" are toggled rapidly. 103 | 104 | Unrecognized properties are passed as options to Velocity (e.g. `duration`, `delay`, `loop`). 105 | 106 | #### Methods 107 | 108 | `runAnimation`: Triggers the animation immediately. Useful for when you want an animation that 109 | corresponds to an event but not a particular model state change (e.g. a "bump" when a click 110 | occurs). 111 | 112 | 113 | ### `VelocityTransitionGroup` 114 | 115 | Component to add Velocity animations around React transitions. Delegates to the React `TransitionGroup` 116 | addon. 117 | 118 | #### Example 119 | ```JSX 120 | 121 | {this.state.renderSubComponent ? : undefined} 122 | 123 | ``` 124 | 125 | #### Properties 126 | `enter`: Animation to run on a child component being added 127 | 128 | `leave`: Animation to run on a child component leaving 129 | 130 | `runOnMount`: if true, runs the `enter` animation on the elements that exist as children when this 131 | component is mounted. 132 | 133 | Any additional properties (e.g. `className`, `component`) will be passed to the internal 134 | `TransitionGroup`. 135 | 136 | `enter` and `leave` should either be a string naming an animation registered with UI Pack, or a hash 137 | with an `animation` key that can either be a string itself, or a hash of style attributes to animate 138 | (this value is passed to Velocity its the first arg). 139 | 140 | **Note:** To make it easier to write consistent “enter” animations, the “leave” animation is applied 141 | to elements before the “enter” animation is run. So, for something like opacity, you can set it at 142 | 0 in “leave” and 1 in “enter,” rather than having to specify that “enter” should start at 0. 143 | 144 | If `enter` or `leave` is a hash, it can additionally have a `style` value that is applied the tick 145 | before the Velocity animation starts. Use this for non-animating properties (like `position`) that 146 | are prerequisites for correct animation. The style value is applied using Velocity's JS -> CSS 147 | routines, which may differ from React's. 148 | 149 | Any hash entries beyond `animation` and `style` are passed in an options hash to Velocity. Use this 150 | for options like `stagger`, `reverse`, *&c.* 151 | 152 | #### Statics 153 | 154 | `disabledForTest`: Set this to true globally to turn off all custom animation logic. Instead, this 155 | component will behave like a vanilla `TransitionGroup`. 156 | 157 | ### `velocityHelpers` 158 | 159 | #### `registerEffect` 160 | 161 | Takes a Velocity "UI pack effect" definition and registers it with a unique key, returning that 162 | key (to later pass as a value for the `animation` property). Takes an optional `suffix`, which can 163 | be "In" or "Out" to modify UI Pack's behavior. 164 | 165 | Unlike what you get from passing a style hash to `VelocityComponent`'s `animation` property, 166 | Velocity "UI pack effects" can have chained animation calls and specify a `defaultDuration`, and 167 | also can take advantage of `stagger` and `reverse` properties on the `VelocityComponent`. 168 | 169 | You will need to manually register the UI Pack with the global Velocity in your application with: 170 | ```JS 171 | require('velocity-animate'); 172 | require('velocity-animate/velocity.ui'); 173 | ``` 174 | 175 | If, even with the above statements, you see errors like `Velocity: First argument 176 | (transition.shrinkIn) was not a property map, a known action, or a registered redirect. Aborting.` 177 | it is likely that there are 2 copies of `velocity-animate` in your `node_modules`. Use `npm dedupe` 178 | to collapse them down to one. 179 | 180 | It might also be necessary to require the `velocity-animate` package explicitly in your package.json. 181 | 182 | See: http://velocityjs.org/#uiPack 183 | 184 | ### Server-side rendering 185 | 186 | The `VelocityComponent` and `VelocityTransitionGroup` components are (as of v1.0.1) 187 | tolerant of being rendered on the server: they will no-op and render their children 188 | naturally. If your initial animation end states match 189 | natural rendering this will be exactly what you want. Otherwise, you may notice a 190 | flash when the JS is applied and the initial animations are resolved. 191 | 192 | ## Bugs 193 | Please report any bugs to: 194 | 195 | **We welcome contributions!** Note that when testing local changes against local projects you’ll 196 | need to avoid `npm link` since it typically will cause duplicate instances of `React` in the client. 197 | (You’ll often see this manifest as `firstChild undefined` errors.) 198 | 199 | ## Acknowledgments 200 | Thanks to Julian Shapiro and Ken Wheeler for creating and maintaining Velocity, respectively, 201 | and for working with us to release this library. 202 | 203 | Thanks to Kevin Robinson and Sam Phillips for all of the discussions and code reviews. 204 | 205 | ## License 206 | Copyright 2017 Google Inc. 207 | 208 | Licensed under the MIT License: https://opensource.org/licenses/MIT 209 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ReactDOM = require('react-dom'); 3 | var MainComponent = require('./main'); 4 | 5 | ReactDOM.render( 6 | React.createElement(MainComponent), 7 | document.getElementById('app') 8 | ); 9 | -------------------------------------------------------------------------------- /demo/components/box.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var React = require('react'); 3 | var PropTypes = require('prop-types'); 4 | 5 | require('../css/polyfill.css'); 6 | 7 | class Box extends React.Component { 8 | static COLORS = { 9 | frontColor: '#3f83b7', 10 | backColor: '#5797c0', 11 | underneathColor: '#255175', 12 | }; 13 | 14 | static propTypes = { 15 | style: PropTypes.object, 16 | underneath: PropTypes.bool, 17 | instruction: PropTypes.string, 18 | }; 19 | 20 | static defaultProps = { 21 | style: {}, 22 | underneath: false, 23 | instruction: '', 24 | }; 25 | 26 | render() { 27 | var style = _.extend({}, { 28 | height: 130, 29 | width: 130, 30 | backgroundColor: this.constructor.COLORS[this.props.underneath ? 'underneathColor' : 'frontColor'], 31 | textAlign: 'center', 32 | fontSize: 80, 33 | fontWeight: 'bold', 34 | cursor: 'pointer', 35 | position: this.props.underneath ? '' : 'relative', 36 | }, this.props.style); 37 | 38 | var instructionStyle = { 39 | position: 'absolute', 40 | bottom: 3, 41 | left: 0, 42 | right: 0, 43 | fontSize: '11px', 44 | fontWeight: 'normal', 45 | textTransform: 'uppercase', 46 | color: '#eee', 47 | opacity: 0.4, 48 | }; 49 | 50 | // Pass any props that are not our own on through. 51 | var restProps = _.omit(this.props, _.keys(this.constructor.propTypes)); 52 | 53 | // outer div below absorbs Velocity's display: block behavior, keeping it from overwriting 54 | // the display: flex 55 | return ( 56 |
57 |
58 | {this.props.children} 59 | {this.props.instruction ?
{this.props.instruction}
: null} 60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | module.exports = Box; 67 | -------------------------------------------------------------------------------- /demo/components/emoji-span.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var PropTypes = require('prop-types'); 3 | var twemoji = require('twemoji'); 4 | var s = require('underscore.string'); 5 | 6 | // Component that uses the Twitter twemoji library to turn emoji-rich strings into spans with 7 | // tags in them (for cross-browser/os compatibility). 8 | class EmojiSpan extends React.Component { 9 | static propTypes = { 10 | size: PropTypes.number, 11 | children: PropTypes.string, 12 | }; 13 | 14 | static defaultProps = { 15 | size: 36, 16 | }; 17 | 18 | render() { 19 | let {children, size, ...attrs} = this.props; 20 | 21 | return ( 22 | 23 | ); 24 | } 25 | } 26 | 27 | module.exports = EmojiSpan; 28 | -------------------------------------------------------------------------------- /demo/components/loading-crossfade-component.jsx: -------------------------------------------------------------------------------- 1 | // Component for a VelocityTransitionGroup that crossfades between its children. 2 | // 3 | // To use this component, render with a single child that contains the "loading" version of your 4 | // UI. When data has loaded, switch the "key" of this child so that React considers it a brand 5 | // new element and triggers the enter / leave effects. The two versions of the UI are expected to 6 | // have identical heights. 7 | // 8 | // Properties on this component (such as "style") are applied to the VelocityTransitionGroup 9 | // component that this delegates to. We set the VelocityTransitionGroup's container to a
by 10 | // default, and provide enter and leave animations, though these could be overridden if it makes 11 | // sense for your use case. A position: 'relative' style is also applied by default since the loading 12 | // effect requires position: 'absolute' on the child. 13 | // 14 | // This component defines a "duration" property that is used for both the enter and leave animation 15 | // durations. 16 | // 17 | // Use the property "opaque" if the children have opaque backgrounds. This will make the new element 18 | // come in 100% opacity and fade the old element out from on top of it. (Without this, opaque 19 | // elements end up bleeding the background behind the LoadingCrossfadeComponent through.) 20 | 21 | var React = require('react'); 22 | var PropTypes = require('prop-types'); 23 | var _ = require('lodash'); 24 | var VelocityTransitionGroup = require('../../src/velocity-transition-group'); 25 | 26 | class LoadingCrossfadeComponent extends React.Component { 27 | static propTypes = { 28 | opaque: PropTypes.bool, 29 | duration: PropTypes.number, 30 | // At most 1 child should be supplied at a time, though the animation does correctly handle 31 | // elements moving in and out faster than the duration (so you can have 2 leaving elements 32 | // simultaneously, for example). 33 | children: PropTypes.element, 34 | }; 35 | 36 | static defaultProps = { 37 | duration: 350, 38 | }; 39 | 40 | render() { 41 | // We pull style out explicitly so that we can merge the position: 'relative' over any provided 42 | // value. position: 'relative' lets us absolutely-position the leaving child during the fade. 43 | var style = _.defaults((this.props.style || {}), { position: 'relative' }); 44 | 45 | var transitionGroupProps = _.defaults(_.omit(this.props, _.keys(this.constructor.propTypes), 'style'), { 46 | component: 'div', 47 | style: style, 48 | 49 | enter: { 50 | animation: { opacity: 1 }, 51 | duration: this.props.duration, 52 | style: { 53 | // If we're animating opaque backgrounds then we just render the new element under the 54 | // old one and fade out the old one. Without this, at e.g. the crossfade midpoint of 55 | // 50% opacity for old and 50% opacity for new, the parent background ends up bleeding 56 | // through 25%, which makes things look not smooth at all. 57 | opacity: this.props.opaque ? 1 : 0, 58 | 59 | // We need to clear out all the styles that "leave" puts on the element. 60 | position: 'relative', 61 | top: '', 62 | left: '', 63 | bottom: '', 64 | right: '', 65 | zIndex: '', 66 | }, 67 | }, 68 | 69 | leave: { 70 | animation: { opacity: 0 }, 71 | duration: this.props.duration, 72 | style: { 73 | // 'absolute' so the 2 elements overlap for a crossfade 74 | position: 'absolute', 75 | top: 0, 76 | left: 0, 77 | bottom: 0, 78 | right: 0, 79 | zIndex: 1, 80 | }, 81 | }, 82 | }); 83 | 84 | return React.createElement(VelocityTransitionGroup, transitionGroupProps, this.props.children); 85 | } 86 | } 87 | 88 | module.exports = LoadingCrossfadeComponent; 89 | -------------------------------------------------------------------------------- /demo/css/flexbox.css: -------------------------------------------------------------------------------- 1 | .flex-box { 2 | display: -webkit-box; 3 | display: -moz-box; 4 | display: box; 5 | display: -webkit-flex; 6 | display: -moz-flex; 7 | display: -ms-flexbox; 8 | display: flex 9 | } 10 | 11 | .flex-inline { 12 | display: -webkit-inline-box; 13 | display: -moz-inline-box; 14 | display: inline-box; 15 | display: -webkit-inline-flex; 16 | display: -moz-inline-flex; 17 | display: -ms-inline-flexbox; 18 | display: inline-flex 19 | } 20 | 21 | .flex-row { 22 | -webkit-box-orient: horizontal; 23 | -moz-box-orient: horizontal; 24 | box-orient: horizontal; 25 | -webkit-flex-direction: row; 26 | -moz-flex-direction: row; 27 | flex-direction: row; 28 | -ms-flex-direction: row 29 | } 30 | 31 | .flex-row-reverse { 32 | -webkit-box-orient: horizontal; 33 | -moz-box-orient: horizontal; 34 | box-orient: horizontal; 35 | -webkit-box-direction: reverse; 36 | -moz-box-direction: reverse; 37 | box-direction: reverse; 38 | -webkit-flex-direction: row-reverse; 39 | -moz-flex-direction: row-reverse; 40 | flex-direction: row-reverse; 41 | -ms-flex-direction: row-reverse 42 | } 43 | 44 | .flex-column { 45 | -webkit-box-orient: vertical; 46 | -moz-box-orient: vertical; 47 | box-orient: vertical; 48 | -webkit-flex-direction: column; 49 | -moz-flex-direction: column; 50 | flex-direction: column; 51 | -ms-flex-direction: column 52 | } 53 | 54 | .flex-column-reverse { 55 | -webkit-box-orient: vertical; 56 | -moz-box-orient: vertical; 57 | box-orient: vertical; 58 | -webkit-box-direction: reverse; 59 | -moz-box-direction: reverse; 60 | box-direction: reverse; 61 | -webkit-flex-direction: column-reverse; 62 | -moz-flex-direction: column-reverse; 63 | flex-direction: column-reverse; 64 | -ms-flex-direction: column-reverse 65 | } 66 | 67 | .flex-nowrap { 68 | -webkit-box-lines: single; 69 | -moz-box-lines: single; 70 | box-lines: single; 71 | -webkit-flex-wrap: nowrap; 72 | -moz-flex-wrap: nowrap; 73 | -ms-flex-wrap: nowrap; 74 | flex-wrap: nowrap 75 | } 76 | 77 | .flex-wrap { 78 | -webkit-box-lines: multiple; 79 | -moz-box-lines: multiple; 80 | box-lines: multiple; 81 | -webkit-flex-wrap: wrap; 82 | -moz-flex-wrap: wrap; 83 | -ms-flex-wrap: wrap; 84 | flex-wrap: wrap 85 | } 86 | 87 | .flex-wrap-reverse { 88 | -webkit-box-lines: multiple; 89 | -moz-box-lines: multiple; 90 | box-lines: multiple; 91 | -webkit-flex-wrap: wrap-reverse; 92 | -moz-flex-wrap: wrap-reverse; 93 | -ms-flex-wrap: wrap-reverse; 94 | flex-wrap: wrap-reverse 95 | } 96 | 97 | .justify-content-flex-start { 98 | -webkit-box-pack: start; 99 | -moz-box-pack: start; 100 | box-pack: start; 101 | -webkit-justify-content: flex-start; 102 | -moz-justify-content: flex-start; 103 | -ms-justify-content: flex-start; 104 | -o-justify-content: flex-start; 105 | justify-content: flex-start; 106 | -ms-flex-pack: start 107 | } 108 | 109 | .justify-content-flex-end { 110 | -webkit-box-pack: end; 111 | -moz-box-pack: end; 112 | box-pack: end; 113 | -webkit-justify-content: flex-end; 114 | -moz-justify-content: flex-end; 115 | -ms-justify-content: flex-end; 116 | -o-justify-content: flex-end; 117 | justify-content: flex-end; 118 | -ms-flex-pack: end 119 | } 120 | 121 | .justify-content-center { 122 | -webkit-box-pack: center; 123 | -moz-box-pack: center; 124 | box-pack: center; 125 | -webkit-justify-content: center; 126 | -moz-justify-content: center; 127 | -ms-justify-content: center; 128 | -o-justify-content: center; 129 | justify-content: center; 130 | -ms-flex-pack: center 131 | } 132 | 133 | .justify-content-space-between { 134 | -webkit-box-pack: justify; 135 | -moz-box-pack: justify; 136 | box-pack: justify; 137 | -webkit-justify-content: space-between; 138 | -moz-justify-content: space-between; 139 | -ms-justify-content: space-between; 140 | -o-justify-content: space-between; 141 | justify-content: space-between; 142 | -ms-flex-pack: justify 143 | } 144 | 145 | .justify-content-space-around { 146 | -webkit-box-pack: center; 147 | -moz-box-pack: center; 148 | box-pack: center; 149 | -webkit-justify-content: space-around; 150 | -moz-justify-content: space-around; 151 | -ms-justify-content: space-around; 152 | -o-justify-content: space-around; 153 | justify-content: space-around; 154 | -ms-flex-pack: center 155 | } 156 | 157 | .align-items-flex-start { 158 | -webkit-box-align: start; 159 | -moz-box-align: start; 160 | box-align: start; 161 | -webkit-align-items: flex-start; 162 | -moz-align-items: flex-start; 163 | -ms-align-items: flex-start; 164 | -o-align-items: flex-start; 165 | align-items: flex-start; 166 | -ms-flex-align: start 167 | } 168 | 169 | .align-items-flex-end { 170 | -webkit-box-align: end; 171 | -moz-box-align: end; 172 | box-align: end; 173 | -webkit-align-items: flex-end; 174 | -moz-align-items: flex-end; 175 | -ms-align-items: flex-end; 176 | -o-align-items: flex-end; 177 | align-items: flex-end; 178 | -ms-flex-align: end 179 | } 180 | 181 | .align-items-center { 182 | -webkit-box-align: center; 183 | -moz-box-align: center; 184 | box-align: center; 185 | -webkit-align-items: center; 186 | -moz-align-items: center; 187 | -ms-align-items: center; 188 | -o-align-items: center; 189 | align-items: center; 190 | -ms-flex-align: center 191 | } 192 | 193 | .align-items-baseline { 194 | -webkit-box-align: baseline; 195 | -moz-box-align: baseline; 196 | box-align: baseline; 197 | -webkit-align-items: baseline; 198 | -moz-align-items: baseline; 199 | -ms-align-items: baseline; 200 | -o-align-items: baseline; 201 | align-items: baseline; 202 | -ms-flex-align: baseline 203 | } 204 | 205 | .align-items-stretch { 206 | -webkit-box-align: stretch; 207 | -moz-box-align: stretch; 208 | box-align: stretch; 209 | -webkit-align-items: stretch; 210 | -moz-align-items: stretch; 211 | -ms-align-items: stretch; 212 | -o-align-items: stretch; 213 | align-items: stretch; 214 | -ms-flex-align: stretch 215 | } 216 | 217 | .align-content-flex-start { 218 | -webkit-align-content: flex-start; 219 | -moz-align-content: flex-start; 220 | align-content: flex-start; 221 | -ms-flex-line-pack: start 222 | } 223 | 224 | .align-content-flex-end { 225 | -webkit-align-content: flex-end; 226 | -moz-align-content: flex-end; 227 | align-content: flex-end; 228 | -ms-flex-line-pack: end 229 | } 230 | 231 | .align-content-center { 232 | -webkit-align-content: center; 233 | -moz-align-content: center; 234 | align-content: center; 235 | -ms-flex-line-pack: center 236 | } 237 | 238 | .align-content-space-between { 239 | -webkit-align-content: space-between; 240 | -moz-align-content: space-between; 241 | align-content: space-between; 242 | -ms-flex-line-pack: justify 243 | } 244 | 245 | .align-content-space-around { 246 | -webkit-align-content: space-around; 247 | -moz-align-content: space-around; 248 | align-content: space-around; 249 | -ms-flex-line-pack: distribute 250 | } 251 | 252 | .align-content-stretch { 253 | -webkit-align-content: stretch; 254 | -moz-align-content: stretch; 255 | align-content: stretch; 256 | -ms-flex-line-pack: stretch 257 | } 258 | 259 | .order-1 { 260 | -webkit-box-ordinal-group: 1; 261 | -moz-box-ordinal-group: 1; 262 | box-ordinal-group: 1; 263 | -webkit-order: 1; 264 | -moz-order: 1; 265 | order: 1; 266 | -ms-flex-order: 1 267 | } 268 | 269 | .order-2 { 270 | -webkit-box-ordinal-group: 2; 271 | -moz-box-ordinal-group: 2; 272 | box-ordinal-group: 2; 273 | -webkit-order: 2; 274 | -moz-order: 2; 275 | order: 2; 276 | -ms-flex-order: 2 277 | } 278 | 279 | .order-3 { 280 | -webkit-box-ordinal-group: 3; 281 | -moz-box-ordinal-group: 3; 282 | box-ordinal-group: 3; 283 | -webkit-order: 3; 284 | -moz-order: 3; 285 | order: 3; 286 | -ms-flex-order: 3 287 | } 288 | 289 | .order-4 { 290 | -webkit-box-ordinal-group: 4; 291 | -moz-box-ordinal-group: 4; 292 | box-ordinal-group: 4; 293 | -webkit-order: 4; 294 | -moz-order: 4; 295 | order: 4; 296 | -ms-flex-order: 4 297 | } 298 | 299 | .order-5 { 300 | -webkit-box-ordinal-group: 5; 301 | -moz-box-ordinal-group: 5; 302 | box-ordinal-group: 5; 303 | -webkit-order: 5; 304 | -moz-order: 5; 305 | order: 5; 306 | -ms-flex-order: 5 307 | } 308 | 309 | .order-6 { 310 | -webkit-box-ordinal-group: 6; 311 | -moz-box-ordinal-group: 6; 312 | box-ordinal-group: 6; 313 | -webkit-order: 6; 314 | -moz-order: 6; 315 | order: 6; 316 | -ms-flex-order: 6 317 | } 318 | 319 | .order-7 { 320 | -webkit-box-ordinal-group: 7; 321 | -moz-box-ordinal-group: 7; 322 | box-ordinal-group: 7; 323 | -webkit-order: 7; 324 | -moz-order: 7; 325 | order: 7; 326 | -ms-flex-order: 7 327 | } 328 | 329 | .order-8 { 330 | -webkit-box-ordinal-group: 8; 331 | -moz-box-ordinal-group: 8; 332 | box-ordinal-group: 8; 333 | -webkit-order: 8; 334 | -moz-order: 8; 335 | order: 8; 336 | -ms-flex-order: 8 337 | } 338 | 339 | .order-9 { 340 | -webkit-box-ordinal-group: 9; 341 | -moz-box-ordinal-group: 9; 342 | box-ordinal-group: 9; 343 | -webkit-order: 9; 344 | -moz-order: 9; 345 | order: 9; 346 | -ms-flex-order: 9 347 | } 348 | 349 | .order-10 { 350 | -webkit-box-ordinal-group: 10; 351 | -moz-box-ordinal-group: 10; 352 | box-ordinal-group: 10; 353 | -webkit-order: 10; 354 | -moz-order: 10; 355 | order: 10; 356 | -ms-flex-order: 10 357 | } 358 | 359 | .flex-grow-0 { 360 | -webkit-flex-grow: 0; 361 | -moz-flex-grow: 0; 362 | flex-grow: 0; 363 | -ms-flex-positive: 0 364 | } 365 | 366 | .flex-grow-1 { 367 | -webkit-flex-grow: 1; 368 | -moz-flex-grow: 1; 369 | flex-grow: 1; 370 | -ms-flex-positive: 1 371 | } 372 | 373 | .flex-grow-2 { 374 | -webkit-flex-grow: 2; 375 | -moz-flex-grow: 2; 376 | flex-grow: 2; 377 | -ms-flex-positive: 2 378 | } 379 | 380 | .flex-grow-3 { 381 | -webkit-flex-grow: 3; 382 | -moz-flex-grow: 3; 383 | flex-grow: 3; 384 | -ms-flex-positive: 3 385 | } 386 | 387 | .flex-grow-4 { 388 | -webkit-flex-grow: 4; 389 | -moz-flex-grow: 4; 390 | flex-grow: 4; 391 | -ms-flex-positive: 4 392 | } 393 | 394 | .flex-grow-5 { 395 | -webkit-flex-grow: 5; 396 | -moz-flex-grow: 5; 397 | flex-grow: 5; 398 | -ms-flex-positive: 5 399 | } 400 | 401 | .flex-grow-6 { 402 | -webkit-flex-grow: 6; 403 | -moz-flex-grow: 6; 404 | flex-grow: 6; 405 | -ms-flex-positive: 6 406 | } 407 | 408 | .flex-grow-7 { 409 | -webkit-flex-grow: 7; 410 | -moz-flex-grow: 7; 411 | flex-grow: 7; 412 | -ms-flex-positive: 7 413 | } 414 | 415 | .flex-grow-8 { 416 | -webkit-flex-grow: 8; 417 | -moz-flex-grow: 8; 418 | flex-grow: 8; 419 | -ms-flex-positive: 8 420 | } 421 | 422 | .flex-grow-9 { 423 | -webkit-flex-grow: 9; 424 | -moz-flex-grow: 9; 425 | flex-grow: 9; 426 | -ms-flex-positive: 9 427 | } 428 | 429 | .flex-grow-10 { 430 | -webkit-flex-grow: 10; 431 | -moz-flex-grow: 10; 432 | flex-grow: 10; 433 | -ms-flex-positive: 10 434 | } 435 | 436 | .flex-shrink-0 { 437 | -webkit-flex-shrink: 0; 438 | -moz-flex-shrink: 0; 439 | flex-shrink: 0; 440 | -ms-flex-negative: 0 441 | } 442 | 443 | .flex-shrink-1 { 444 | -webkit-flex-shrink: 1; 445 | -moz-flex-shrink: 1; 446 | flex-shrink: 1; 447 | -ms-flex-negative: 1 448 | } 449 | 450 | .flex-shrink-2 { 451 | -webkit-flex-shrink: 2; 452 | -moz-flex-shrink: 2; 453 | flex-shrink: 2; 454 | -ms-flex-negative: 2 455 | } 456 | 457 | .flex-shrink-3 { 458 | -webkit-flex-shrink: 3; 459 | -moz-flex-shrink: 3; 460 | flex-shrink: 3; 461 | -ms-flex-negative: 3 462 | } 463 | 464 | .flex-shrink-4 { 465 | -webkit-flex-shrink: 4; 466 | -moz-flex-shrink: 4; 467 | flex-shrink: 4; 468 | -ms-flex-negative: 4 469 | } 470 | 471 | .flex-shrink-5 { 472 | -webkit-flex-shrink: 5; 473 | -moz-flex-shrink: 5; 474 | flex-shrink: 5; 475 | -ms-flex-negative: 5 476 | } 477 | 478 | .flex-shrink-6 { 479 | -webkit-flex-shrink: 6; 480 | -moz-flex-shrink: 6; 481 | flex-shrink: 6; 482 | -ms-flex-negative: 6 483 | } 484 | 485 | .flex-shrink-7 { 486 | -webkit-flex-shrink: 7; 487 | -moz-flex-shrink: 7; 488 | flex-shrink: 7; 489 | -ms-flex-negative: 7 490 | } 491 | 492 | .flex-shrink-8 { 493 | -webkit-flex-shrink: 8; 494 | -moz-flex-shrink: 8; 495 | flex-shrink: 8; 496 | -ms-flex-negative: 8 497 | } 498 | 499 | .flex-shrink-9 { 500 | -webkit-flex-shrink: 9; 501 | -moz-flex-shrink: 9; 502 | flex-shrink: 9; 503 | -ms-flex-negative: 9 504 | } 505 | 506 | .flex-shrink-10 { 507 | -webkit-flex-shrink: 10; 508 | -moz-flex-shrink: 10; 509 | flex-shrink: 10; 510 | -ms-flex-negative: 10 511 | } 512 | 513 | .flex-0 { 514 | -webkit-box-flex: 0; 515 | -moz-box-flex: 0; 516 | box-flex: 0; 517 | -webkit-flex: 0; 518 | -moz-flex: 0; 519 | -ms-flex: 0; 520 | flex: 0; 521 | min-width: 0; 522 | min-height: 0 523 | } 524 | 525 | .flex-1 { 526 | -webkit-box-flex: 1; 527 | -moz-box-flex: 1; 528 | box-flex: 1; 529 | -webkit-flex: 1; 530 | -moz-flex: 1; 531 | -ms-flex: 1; 532 | flex: 1; 533 | min-width: 0; 534 | min-height: 0 535 | } 536 | 537 | .flex-2 { 538 | -webkit-box-flex: 2; 539 | -moz-box-flex: 2; 540 | box-flex: 2; 541 | -webkit-flex: 2; 542 | -moz-flex: 2; 543 | -ms-flex: 2; 544 | flex: 2; 545 | min-width: 0; 546 | min-height: 0 547 | } 548 | 549 | .flex-3 { 550 | -webkit-box-flex: 3; 551 | -moz-box-flex: 3; 552 | box-flex: 3; 553 | -webkit-flex: 3; 554 | -moz-flex: 3; 555 | -ms-flex: 3; 556 | flex: 3; 557 | min-width: 0; 558 | min-height: 0 559 | } 560 | 561 | .flex-4 { 562 | -webkit-box-flex: 4; 563 | -moz-box-flex: 4; 564 | box-flex: 4; 565 | -webkit-flex: 4; 566 | -moz-flex: 4; 567 | -ms-flex: 4; 568 | flex: 4; 569 | min-width: 0; 570 | min-height: 0 571 | } 572 | 573 | .flex-5 { 574 | -webkit-box-flex: 5; 575 | -moz-box-flex: 5; 576 | box-flex: 5; 577 | -webkit-flex: 5; 578 | -moz-flex: 5; 579 | -ms-flex: 5; 580 | flex: 5; 581 | min-width: 0; 582 | min-height: 0 583 | } 584 | 585 | .flex-6 { 586 | -webkit-box-flex: 6; 587 | -moz-box-flex: 6; 588 | box-flex: 6; 589 | -webkit-flex: 6; 590 | -moz-flex: 6; 591 | -ms-flex: 6; 592 | flex: 6; 593 | min-width: 0; 594 | min-height: 0 595 | } 596 | 597 | .flex-7 { 598 | -webkit-box-flex: 7; 599 | -moz-box-flex: 7; 600 | box-flex: 7; 601 | -webkit-flex: 7; 602 | -moz-flex: 7; 603 | -ms-flex: 7; 604 | flex: 7; 605 | min-width: 0; 606 | min-height: 0 607 | } 608 | 609 | .flex-8 { 610 | -webkit-box-flex: 8; 611 | -moz-box-flex: 8; 612 | box-flex: 8; 613 | -webkit-flex: 8; 614 | -moz-flex: 8; 615 | -ms-flex: 8; 616 | flex: 8; 617 | min-width: 0; 618 | min-height: 0 619 | } 620 | 621 | .flex-9 { 622 | -webkit-box-flex: 9; 623 | -moz-box-flex: 9; 624 | box-flex: 9; 625 | -webkit-flex: 9; 626 | -moz-flex: 9; 627 | -ms-flex: 9; 628 | flex: 9; 629 | min-width: 0; 630 | min-height: 0 631 | } 632 | 633 | .flex-10 { 634 | -webkit-box-flex: 10; 635 | -moz-box-flex: 10; 636 | box-flex: 10; 637 | -webkit-flex: 10; 638 | -moz-flex: 10; 639 | -ms-flex: 10; 640 | flex: 10; 641 | min-width: 0; 642 | min-height: 0 643 | } 644 | 645 | .align-self-auto { 646 | -webkit-align-self: auto; 647 | -moz-align-self: auto; 648 | align-self: auto; 649 | -ms-flex-item-align: auto 650 | } 651 | 652 | .align-self-flex-start { 653 | -webkit-align-self: flex-start; 654 | -moz-align-self: flex-start; 655 | align-self: flex-start; 656 | -ms-flex-item-align: start 657 | } 658 | 659 | .align-self-flex-end { 660 | -webkit-align-self: flex-end; 661 | -moz-align-self: flex-end; 662 | align-self: flex-end; 663 | -ms-flex-item-align: end 664 | } 665 | 666 | .align-self-center { 667 | -webkit-align-self: center; 668 | -moz-align-self: center; 669 | align-self: center; 670 | -ms-flex-item-align: center 671 | } 672 | 673 | .align-self-baseline { 674 | -webkit-align-self: baseline; 675 | -moz-align-self: baseline; 676 | align-self: baseline; 677 | -ms-flex-item-align: baseline 678 | } 679 | 680 | .align-self-stretch { 681 | -webkit-align-self: stretch; 682 | -moz-align-self: stretch; 683 | align-self: stretch; 684 | -ms-flex-item-align: stretch 685 | } 686 | -------------------------------------------------------------------------------- /demo/css/loading-placeholder.css: -------------------------------------------------------------------------------- 1 | .loading-placeholder-light, 2 | .loading-placeholder-dark { 3 | position: relative; 4 | } 5 | 6 | /* Gray, translucent bar to show "loading." Renders as 86% of the parent's height, vertically 7 | centered, and 100% of its width. */ 8 | .loading-placeholder-light:before, 9 | .loading-placeholder-dark:before { 10 | content: ' '; 11 | display: block; 12 | position: absolute; 13 | top: 7%; 14 | height: 86%; 15 | width: 100%; 16 | } 17 | 18 | .loading-placeholder-full:before { 19 | top: 0; 20 | height: 100%; 21 | } 22 | 23 | /* Inserts a non-breaking space into the parent to guarantee that it has at least one line's 24 | worth of height. (Necessary for the 'before' block to do a relatively-sized height.) */ 25 | .loading-placeholder-light:after, 26 | .loading-placeholder-dark:after { 27 | content: '\00a0'; 28 | } 29 | 30 | .loading-placeholder-light:before { 31 | background-color: rgba(235, 237, 241, 0.1); 32 | } 33 | 34 | .loading-placeholder-dark:before { 35 | background-color: rgba(235, 237, 241, 0.5); 36 | } 37 | 38 | span.loading-placeholder-light, 39 | span.loading-placeholder-dark { 40 | /* Useful for being able to set an explicit width on the placeholder. */ 41 | display: inline-block; 42 | } 43 | -------------------------------------------------------------------------------- /demo/css/polyfill.css: -------------------------------------------------------------------------------- 1 | .user-select-none { 2 | -webkit-touch-callout: none; 3 | -webkit-user-select: none; 4 | -khtml-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | } 9 | -------------------------------------------------------------------------------- /demo/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | /* apply a natural box layout model to all elements, but allowing components to change */ 51 | html { 52 | box-sizing: border-box; 53 | } 54 | *, *:before, *:after { 55 | box-sizing: inherit; 56 | } 57 | -------------------------------------------------------------------------------- /demo/examples/crossfade-example.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var React = require('react'); 4 | var VelocityComponent = require('../../src/velocity-component'); 5 | var VelocityTransitionGroup = require('../../src/velocity-transition-group'); 6 | 7 | var Box = require('../components/box'); 8 | var EmojiSpan = require('../components/emoji-span'); 9 | var LoadingCrossfadeComponent = require('../components/loading-crossfade-component'); 10 | 11 | require('../css/loading-placeholder.css'); 12 | 13 | var LOCATION_COUNT = 4; 14 | 15 | var BUILDINGS = ['🏠', '🏡', '🏢', '🏬', '🏭', '🏣', '🏤', '🏥', '🏦', '🏨', '🏩', '💒', '⛪', '🏪', '🏫']; 16 | var CAPITALS = ['Montgomery', 'Juneau', 'Phoenix', 'Little Rock', 'Sacramento', 'Denver', 'Hartford', 'Dover', 17 | 'Tallahassee', 'Atlanta', 'Honolulu', 'Boise', 'Springfield', 'Indianapolis', 'Des Moines', 'Topeka', 'Frankfort', 18 | 'Baton Rouge', 'Augusta', 'Annapolis', 'Boston', 'Lansing', 'St. Paul', 'Jackson', 'Jefferson City', 'Helena', 19 | 'Lincoln', 'Carson City', 'Concord', 'Trenton', 'Santa Fe', 'Albany', 'Raleigh', 'Bismarck', 'Columbus', 20 | 'Oklahoma City', 'Salem', 'Harrisburg', 'Providence', 'Columbia', 'Pierre', 'Nashville', 'Austin', 'Salt Lake City', 21 | 'Montpelier', 'Richmond', 'Olympia', 'Charleston', 'Madison', 'Cheyenne']; 22 | 23 | class CrossfadeExample extends React.Component { 24 | state = { 25 | expanded: false, 26 | items: null, 27 | duration: 500, 28 | }; 29 | 30 | componentWillUnmount() { 31 | window.clearTimeout(this.locationTimeout); 32 | } 33 | 34 | whenToggleClicked = () => { 35 | if (this.state.expanded) { 36 | this.setState({ 37 | expanded: false, 38 | items: null, 39 | }); 40 | 41 | window.clearTimeout(this.locationTimeout); 42 | } else { 43 | this.setState({ 44 | expanded: true, 45 | items: null, 46 | }); 47 | 48 | this.locationTimeout = window.setTimeout(this.loadLocations, this.state.duration * 1.5); 49 | } 50 | }; 51 | 52 | loadLocations = () => { 53 | this.setState({ 54 | items: Array.apply(null, Array(LOCATION_COUNT)).map(function () { 55 | return { 56 | building: _.sample(BUILDINGS), 57 | city: _.sample(CAPITALS), 58 | }; 59 | }), 60 | }); 61 | }; 62 | 63 | whenOptionClicked = (event) => { 64 | this.setState({ duration: parseInt(event.target.value) }); 65 | }; 66 | 67 | render() { 68 | var groupStyle = { 69 | borderBottom: '1px solid #3f83b7', 70 | padding: '0 1px', 71 | }; 72 | 73 | var boxStyle = { 74 | margin: '-10px 0 0 0', 75 | width: '100%', 76 | }; 77 | 78 | var toggleStyle = { 79 | backgroundColor: '#3f83b7', 80 | color: 'white', 81 | padding: 8, 82 | fontSize: 13, 83 | lineHeight: '18px', 84 | cursor: 'pointer', 85 | }; 86 | 87 | var arrowStyle = { 88 | display: 'block', 89 | fontSize: 18, 90 | }; 91 | 92 | return ( 93 |
94 |
95 | Points of Interest 96 | 97 | 👇 98 | 99 |
100 | 101 |
102 | 105 | {this.state.expanded ? this.renderLocations() : null} 106 | 107 |
108 | 109 |
110 | 113 |   114 | 117 |
118 |
119 | ); 120 | } 121 | 122 | renderLocations = () => { 123 | var boxStyle = { 124 | backgroundColor: '#fefefe', 125 | padding: '5px 10px', 126 | width: 198, 127 | }; 128 | 129 | var locations = this.state.items != null ? this.state.items : Array.apply(null, Array(LOCATION_COUNT)); 130 | 131 | return ( 132 | 133 |
{locations.map(this.renderLocation)}
134 |
135 | ); 136 | }; 137 | 138 | renderLocation = (location, i) => { 139 | location = location || {building: '', city: ''}; 140 | 141 | var emojiStyle = { 142 | fontSize: 30, 143 | display: 'block', 144 | width: 34.5, 145 | height: 31, 146 | }; 147 | 148 | var rowStyle = { 149 | padding: '5px 0', 150 | }; 151 | 152 | var cityStyle = { 153 | fontSize: 16, 154 | margin: '0 0 0 5px', 155 | }; 156 | 157 | return ( 158 |
159 | {location.building} 162 |
163 | {location.city} 164 |
165 |
166 | ); 167 | }; 168 | } 169 | 170 | module.exports = CrossfadeExample; 171 | -------------------------------------------------------------------------------- /demo/examples/flap-box.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var VelocityComponent = require('../../src/velocity-component'); 3 | var velocityHelpers = require('../../src/velocity-helpers'); 4 | 5 | var Box = require('../components/box'); 6 | var EmojiSpan = require('../components/emoji-span'); 7 | 8 | /* 9 | 10 | This demo shows how to use custom Velocity UI pack effects to achieve an animation based 11 | on a state value. 12 | 13 | */ 14 | 15 | var FlipAnimations = { 16 | // Brings the box from flipped up to down. Also the default state that the box starts in. When 17 | // this animates, includes a little swing at the end so it feels more like a flap. 18 | down: velocityHelpers.registerEffect({ 19 | // longer due to spring timing 20 | defaultDuration: 1100, 21 | calls: [ 22 | [{ 23 | transformPerspective: [ 800, 800 ], 24 | transformOriginX: [ '50%', '50%' ], 25 | transformOriginY: [ 0, 0 ], 26 | rotateX: [0, 'spring'], 27 | // We step this back immediately; you don't notice and it means we're not fading in as 28 | // the spring swings rotateX back and forth. 29 | backgroundColor: [Box.COLORS.frontColor, Box.COLORS.frontColor], 30 | }, 1, { 31 | delay: 100, 32 | easing: 'ease-in', 33 | }] 34 | ], 35 | }), 36 | 37 | // Flips the box up nearly 180°. 38 | up: velocityHelpers.registerEffect({ 39 | defaultDuration: 200, 40 | calls: [ 41 | [{ 42 | transformPerspective: [ 800, 800 ], 43 | transformOriginX: [ '50%', '50%' ], 44 | transformOriginY: [ 0, 0 ], 45 | rotateX: 160, 46 | backgroundColor: Box.COLORS.backColor, 47 | }] 48 | ], 49 | }), 50 | }; 51 | 52 | // Animations to blur the number within the box for when it's flipped up. 53 | // 54 | // Blur animations each have a delay to make them change roughly when the flip is halfway up, 55 | // to capture the transition from front to back (and vice versa). They flip over their values 56 | // immediately with no tweening, since that doesn't make sense for the effect. We're using 57 | // Velocity here only to co-ordinate the timing of the change. 58 | var BlurAnimations = { 59 | blur: velocityHelpers.registerEffect({ 60 | defaultDuration: 200, 61 | calls: [ 62 | [{ blur: [3, 3], opacity: [.4, .4] }, 1, { delay: 50 }], 63 | ], 64 | }), 65 | 66 | unblur: velocityHelpers.registerEffect({ 67 | defaultDuration: 200, 68 | calls: [ 69 | [{ blur: [0, 0], opacity: [1, 1] }, 1, { delay: 150 }], 70 | ], 71 | }) 72 | }; 73 | 74 | class FlapBox extends React.Component { 75 | state = { 76 | hovering: false, 77 | }; 78 | 79 | whenMouseEntered = () => { 80 | this.setState({ hovering: true }); 81 | }; 82 | 83 | whenMouseLeft = () => { 84 | this.setState({ hovering: false }); 85 | }; 86 | 87 | render() { 88 | var containerStyle = { 89 | // Neded since the "top" is absolutely positioned 90 | position: 'relative', 91 | cursor: 'default', 92 | }; 93 | 94 | return ( 95 |
96 |
97 | 98 |
99 | 100 |
103 | {this.renderTop()} 104 | {this.renderUnderneath()} 105 |
106 |
107 | ); 108 | } 109 | 110 | renderTop = () => { 111 | var flipAnimation; 112 | var contentsAnimation; 113 | 114 | if (this.state.hovering) { 115 | flipAnimation = FlipAnimations.up; 116 | contentsAnimation = BlurAnimations.blur; 117 | } else { 118 | flipAnimation = FlipAnimations.down; 119 | contentsAnimation = BlurAnimations.unblur; 120 | } 121 | 122 | var boxStyle = { 123 | position: 'absolute', 124 | }; 125 | 126 | return ( 127 | 128 | 129 | 130 | 🙈 131 | 132 | 133 | 134 | ); 135 | }; 136 | 137 | renderUnderneath = () => { 138 | return (🐵); 139 | }; 140 | } 141 | 142 | module.exports = FlapBox; 143 | -------------------------------------------------------------------------------- /demo/examples/scrolling-group.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var React = require('react'); 3 | var VelocityTransitionGroup = require('../../src/velocity-transition-group'); 4 | var velocityHelpers = require('../../src/velocity-helpers'); 5 | 6 | var Box = require('../components/box'); 7 | var EmojiSpan = require('../components/emoji-span'); 8 | 9 | var CATS = ['😸', '😹', '😺', '😻', '😼', '😽', '😾', '😿', '🙀']; 10 | var FOODS = [ 11 | '🍅', 12 | '🍆', 13 | '🍇', 14 | '🍈', 15 | '🍉', 16 | '🍊', 17 | '🍌', 18 | '🍍', 19 | '🍎', 20 | '🍏', 21 | '🍑', 22 | '🍒', 23 | '🍓', 24 | '🍔', 25 | '🍕', 26 | '🍖', 27 | '🍗', 28 | ]; 29 | 30 | var Animations = { 31 | // Register these with UI Pack so that we can use stagger later. 32 | In: velocityHelpers.registerEffect({ 33 | calls: [ 34 | [ 35 | { 36 | transformPerspective: [800, 800], 37 | transformOriginX: ['50%', '50%'], 38 | transformOriginY: ['100%', '100%'], 39 | marginBottom: 0, 40 | opacity: 1, 41 | rotateX: [0, 130], 42 | }, 43 | 1, 44 | { 45 | easing: 'ease-out', 46 | display: 'block', 47 | }, 48 | ], 49 | ], 50 | }), 51 | 52 | Out: velocityHelpers.registerEffect({ 53 | calls: [ 54 | [ 55 | { 56 | transformPerspective: [800, 800], 57 | transformOriginX: ['50%', '50%'], 58 | transformOriginY: ['0%', '0%'], 59 | marginBottom: -30, 60 | opacity: 0, 61 | rotateX: -70, 62 | }, 63 | 1, 64 | { 65 | easing: 'ease-out', 66 | display: 'block', 67 | }, 68 | ], 69 | ], 70 | }), 71 | }; 72 | 73 | const newItem = i => { 74 | return { 75 | title: [_.sample(CATS)].concat(_.sample(FOODS, _.random(1, 4))).join(' '), 76 | i, 77 | }; 78 | }; 79 | 80 | class ScrollingGroup extends React.Component { 81 | state = { 82 | itemCount: 1, 83 | items: [newItem(0)], 84 | duration: 500, 85 | }; 86 | 87 | whenAddButtonClicked = () => { 88 | this.addRows(1); 89 | }; 90 | 91 | whenAdd5ButtonClicked = () => { 92 | this.addRows(5); 93 | }; 94 | 95 | whenOptionClicked = event => { 96 | this.setState({ duration: parseInt(event.target.value) }); 97 | }; 98 | 99 | addRows = count => { 100 | var items = this.state.items; 101 | 102 | for (var i = 0; i < count; ++i) { 103 | var item = newItem(i + this.state.itemCount); 104 | items = [item].concat(items); 105 | } 106 | 107 | this.setState({ 108 | items: items.slice(0, 6), 109 | itemCount: this.state.itemCount + count, 110 | }); 111 | }; 112 | 113 | render() { 114 | var rows = this.state.items.map(function(item, i, arr) { 115 | var itemStyle = { 116 | width: 150, 117 | padding: '0 10px', 118 | lineHeight: '30px', 119 | backgroundColor: 120 | item.i % 2 == 0 ? Box.COLORS.backColor : Box.COLORS.underneathColor, 121 | }; 122 | 123 | return ( 124 |
125 | {item.title} 126 |
127 | ); 128 | }); 129 | 130 | var groupStyle = { 131 | margin: '10px 0', 132 | }; 133 | 134 | var enterAnimation = { 135 | animation: Animations.In, 136 | stagger: this.state.duration, 137 | duration: this.state.duration, 138 | backwards: true, 139 | display: 'block', 140 | style: { 141 | // Since we're staggering, we want to keep the display at "none" until Velocity runs 142 | // the display attribute at the start of the animation. 143 | display: 'none', 144 | }, 145 | }; 146 | 147 | var leaveAnimation = { 148 | animation: Animations.Out, 149 | stagger: this.state.duration, 150 | duration: this.state.duration, 151 | backwards: true, 152 | }; 153 | 154 | return ( 155 |
156 |
157 | 158 | 159 |
160 | 161 | 168 | {rows} 169 | 170 | 171 |
172 | 182 |   183 | 193 |
194 |
195 | ); 196 | } 197 | } 198 | 199 | module.exports = ScrollingGroup; 200 | -------------------------------------------------------------------------------- /demo/examples/toggle-box.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var createReactClass = require('create-react-class'); 3 | var VelocityComponent = require('../../src/velocity-component'); 4 | var s = require('underscore.string'); 5 | 6 | var Box = require('../components/box'); 7 | 8 | var EFFECTS = [ 9 | 'fade', 10 | 'flipX', 11 | 'flipY', 12 | 'flipBounceX', 13 | 'flipBounceY', 14 | 'swoop', 15 | 'whirl', 16 | 'shrink', 17 | 'expand', 18 | 'bounce', 19 | 'bounceUp', 20 | 'bounceDown', 21 | 'bounceLeft', 22 | 'bounceRight', 23 | 'slideUp', 24 | 'slideDown', 25 | 'slideLeft', 26 | 'slideRight', 27 | 'slideUpBig', 28 | 'slideDownBig', 29 | 'slideLeftBig', 30 | 'slideRightBig', 31 | 'perspectiveUp', 32 | 'perspectiveDown', 33 | 'perspectiveLeft', 34 | 'perspectiveRight', 35 | ]; 36 | 37 | var ToggleBox = createReactClass({ 38 | displayName: 'ToggleBox', 39 | 40 | getInitialState: function() { 41 | return { 42 | effect: EFFECTS[0], 43 | isIn: true, 44 | counter: 0, 45 | interruptBehavior: 'stop', 46 | }; 47 | }, 48 | 49 | componentDidMount: function() { 50 | // Tweening shows how animations work given re-renders 51 | this.startTweening(); 52 | }, 53 | 54 | componentWillUnmount: function() { 55 | this.stopTweening(); 56 | }, 57 | 58 | startTweening: function() { 59 | this.tweenInterval = setInterval(() => { 60 | if (this.state.counter === 99) { 61 | this.setState({ counter: 0 }); 62 | } else { 63 | this.setState({ counter: this.state.counter + 1 }); 64 | } 65 | }, 100); 66 | }, 67 | 68 | stopTweening: function() { 69 | clearInterval(this.tweenInterval); 70 | }, 71 | 72 | whenToggleClicked: function() { 73 | this.setState({ 74 | isIn: !this.state.isIn, 75 | }); 76 | }, 77 | 78 | whenSelectChanged: function(evt) { 79 | this.setState({ 80 | effect: evt.target.value, 81 | isIn: true, 82 | }); 83 | }, 84 | 85 | whenOptionChanged: function(evt) { 86 | this.setState({ interruptBehavior: evt.target.value }); 87 | }, 88 | 89 | render: function() { 90 | var animation = 91 | 'transition.' + this.state.effect + (this.state.isIn ? 'In' : 'Out'); 92 | 93 | return ( 94 |
95 |
96 | 99 |
100 |
101 | 107 | {/* 108 | Use of key here keeps the component (and its set styles) from persisting across effects. 109 | Avoids flashing when switching effects. 110 | */} 111 | 116 | {this.state.counter} 117 | 118 | 119 |
120 |
128 | Number counting to show the [non-]effects of rapid re-rendering on the 129 | animation. 130 |
131 |
132 |

135 | Interruption Behavior 136 |

137 | 147 |    148 | 158 |    159 | 169 |
170 |
171 | ); 172 | }, 173 | 174 | renderEffects: function() { 175 | return EFFECTS.map(function(effect) { 176 | return ( 177 | 180 | ); 181 | }); 182 | }, 183 | }); 184 | 185 | module.exports = ToggleBox; 186 | -------------------------------------------------------------------------------- /demo/examples/trigger-box.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var VelocityComponent = require('../../src/velocity-component'); 3 | var s = require('underscore.string'); 4 | 5 | var Box = require('../components/box'); 6 | var EmojiSpan = require('../components/emoji-span'); 7 | 8 | var PALS = ['👫', '👬', '👭']; 9 | 10 | var EFFECTS = ['bounce', 'shake', 'flash', 'pulse', 'swing', 'tada']; 11 | 12 | class TriggerBox extends React.Component { 13 | state = { 14 | effect: EFFECTS[0], 15 | chain: 'stop', 16 | palIndex: 0, 17 | }; 18 | 19 | velocityRef = React.createRef(); 20 | 21 | whenClicked = () => { 22 | this.setState({ 23 | palIndex: (this.state.palIndex + 1) % PALS.length, 24 | }); 25 | 26 | var opts = { 27 | finish: this.state.chain === 'finish', 28 | stop: this.state.chain === 'stop', 29 | }; 30 | 31 | this.velocityRef.current.runAnimation(opts); 32 | }; 33 | 34 | whenSelectChanged = evt => { 35 | this.setState({ 36 | effect: evt.target.value, 37 | isIn: true, 38 | }); 39 | }; 40 | 41 | whenOptionChanged = evt => { 42 | this.setState({ chain: evt.target.value }); 43 | }; 44 | 45 | render() { 46 | var animation = 'callout.' + this.state.effect; 47 | 48 | return ( 49 |
50 |
51 | 54 |
55 |
56 | {/* 57 | Use of key here keeps the component (and its set styles) from persisting across effects. 58 | Avoids flashing when switching effects. 59 | */} 60 | 65 | 66 | {PALS[this.state.palIndex]} 67 | 68 | 69 |
70 |
71 |

74 | Interruption Behavior 75 |

76 | 86 |    87 | 97 |    98 | 108 |
109 |
110 | ); 111 | } 112 | 113 | renderEffects = () => { 114 | return EFFECTS.map(function(effect) { 115 | return ( 116 | 119 | ); 120 | }); 121 | }; 122 | } 123 | 124 | module.exports = TriggerBox; 125 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React + Velocity Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /demo/main.jsx: -------------------------------------------------------------------------------- 1 | require('./css/flexbox.css'); 2 | 3 | var React = require('react'); 4 | 5 | require('velocity-animate'); 6 | require('velocity-animate/velocity.ui'); 7 | 8 | var CrossfadeExample = require('./examples/crossfade-example'); 9 | var FlapBox = require('./examples/flap-box'); 10 | var ScrollingGroup = require('./examples/scrolling-group'); 11 | var ToggleBox = require('./examples/toggle-box'); 12 | var TriggerBox = require('./examples/trigger-box'); 13 | 14 | class Demo extends React.Component { 15 | render() { 16 | var boxStyle = { 17 | backgroundColor: '#efefef', 18 | margin: 10, 19 | padding: '0 0 10px 0', 20 | width: 200, 21 | height: 300, 22 | }; 23 | 24 | var headingStyle = { 25 | margin: '10px 0', 26 | padding: '0 0 10px 0', 27 | width: '100%', 28 | textAlign: 'center', 29 | fontWeight: 200, 30 | fontSize: 14, 31 | borderBottom: '1px solid #e5e5e5', 32 | }; 33 | 34 | return ( 35 |
36 |

{this.props.title}

37 | {this.props.children} 38 |
39 | ); 40 | } 41 | } 42 | 43 | class MainComponent extends React.Component { 44 | render() { 45 | return ( 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | ); 64 | } 65 | } 66 | 67 | module.exports = MainComponent; 68 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: __dirname, 3 | entry: './app.js', 4 | devtool: 'eval', 5 | resolve: { 6 | extensions: ['', '.js', '.jsx'], 7 | }, 8 | output: { 9 | path: __dirname + '/build', 10 | filename: 'bundle.js', 11 | }, 12 | module: { 13 | loaders: [ 14 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 15 | { 16 | test: /\.jsx?$/, 17 | exclude: /(node_modules|bower_components)/, 18 | loaders: ['babel'], 19 | }, 20 | ], 21 | }, 22 | plugins: [], 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "velocity-react", 3 | "version": "1.4.3", 4 | "description": "React components to wrap Velocity animations", 5 | "main": "index.js", 6 | "scripts": { 7 | "compile": "babel src --out-dir .", 8 | "demo": "webpack-dev-server --progress --colors --content-base demo --hot --inline --config demo/webpack.config.js", 9 | "prepare": "npm run compile" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/twitter-fabric/velocity-react.git" 14 | }, 15 | "keywords": [ 16 | "velocity", 17 | "react", 18 | "animation" 19 | ], 20 | "author": "Google Inc.", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/twitter-fabric/velocity-react/issues" 24 | }, 25 | "homepage": "https://github.com/twitter-fabric/velocity-react#readme", 26 | "lint-staged": { 27 | "*.js": "eslint --quiet" 28 | }, 29 | "dependencies": { 30 | "lodash": "^4.17.5", 31 | "prop-types": "^15.5.8", 32 | "react-transition-group": "^2.0.0", 33 | "velocity-animate": "^1.4.0" 34 | }, 35 | "peerDependencies": { 36 | "react": "^15.3.0 || ^16.0.0", 37 | "react-dom": "^15.3.0 || ^16.0.0" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.24.1", 41 | "babel-core": "^6.26.3", 42 | "babel-eslint": "^8.0.1", 43 | "babel-loader": "^6.2.10", 44 | "babel-plugin-transform-class-properties": "^6.24.1", 45 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 46 | "babel-preset-es2015": "^6.24.1", 47 | "babel-preset-react": "^6.16.0", 48 | "create-react-class": "^15.5.2", 49 | "css-loader": "^0.19.0", 50 | "eslint": "^4.7.2", 51 | "eslint-config-prettier": "^2.6.0", 52 | "eslint-plugin-import": "^2.7.0", 53 | "eslint-plugin-prettier": "^2.3.1", 54 | "eslint-plugin-react": "^7.4.0", 55 | "husky": "^0.14.3", 56 | "lint-staged": "^4.2.3", 57 | "prettier": "^1.7.0", 58 | "react": "^16.0.0", 59 | "react-dom": "^16.0.0", 60 | "style-loader": "^0.12.3", 61 | "twemoji": "^1.4.1", 62 | "underscore.string": "^3.2.0", 63 | "webpack": "^1.12.0", 64 | "webpack-dev-server": "^1.10.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Convenience main entrypoint if you are running with destructuring support: 2 | // 3 | // import {VelocityComponent, VelocityTransitionGroup} from 'velocity-react'; 4 | // 5 | // You can also require just the component(s) you're using: 6 | // 7 | // var VelocityComponent = require('velocity-react/velocity-component'); 8 | // 9 | // Note that if you want to use UI Pack you will need to require 'velocity' and 10 | // 'velocity.ui' at a top level in your app: 11 | // 12 | // require('velocity'); 13 | // require('velocity-animate/velocity.ui'); 14 | 15 | module.exports = { 16 | VelocityComponent: require('./velocity-component'), 17 | VelocityTransitionGroup: require('./velocity-transition-group'), 18 | velocityHelpers: require('./velocity-helpers'), 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/velocity-animate-shim.js: -------------------------------------------------------------------------------- 1 | // Shim to avoid requiring Velocity in Node environments, since it 2 | // requires window. Note that this just no-ops the components so 3 | // that they'll render, rather than doing something clever like 4 | // statically rendering the end state of any provided animations. 5 | // 6 | // TODO(finneganh): Circle back on jsdom to see if we can run full 7 | // Velocity with it in place. This come up in: 8 | // https://github.com/twitter-fabric/velocity-react/issues/119 9 | // but there may have been different loading issues in that case, 10 | // not a global incompatibility with jsdom. 11 | if ( 12 | typeof window === 'undefined' || 13 | typeof navigator === 'undefined' || 14 | navigator.userAgent.indexOf('Node.js') !== -1 || 15 | navigator.userAgent.indexOf('jsdom') !== -1 16 | ) { 17 | var Velocity = function() {}; 18 | Velocity.Utilities = {}; 19 | Velocity.Utilities.removeData = function() {}; 20 | Velocity.velocityReactServerShim = true; 21 | module.exports = Velocity; 22 | } else { 23 | // this is how velocity-ui finds the Velocity instance, so lets make sure we find the right instance 24 | var g = window.jQuery || window.Zepto || window; 25 | 26 | // require Velocity if it doesn't already exist 27 | module.exports = g.Velocity ? g.Velocity : require('velocity-animate'); 28 | } 29 | -------------------------------------------------------------------------------- /src/velocity-component.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Twitter, Inc. and other contributors 3 | 4 | Component to add Velocity animations to one or more children. Wraps a single child without adding 5 | additional DOM nodes. 6 | 7 | The API attempts to be as declarative as possible. A single animation property declares what 8 | animation the child should have. If that property changes, this component applies the new animation 9 | to the child on the next tick. 10 | 11 | By default, the animation is not run when the component is mounted. Instead, Velocity's "finish" 12 | command is used to jump to the animation's end state. For a component that animates out of and 13 | back in to a default state, this allows the parent to specify the "animate into" animation as 14 | the default, and therefore not have to distinguish between the initial state and the state to 15 | return to after animating away. 16 | 17 | Properties: 18 | animation: Either an animation key or hash defining the animation. See Velocity's documentation 19 | for what this can be. (It is passed to Velocity exactly.) 20 | runOnMount: If true, runs the animation even when the component is first mounted. 21 | targetQuerySelector: By default, this component's single child is animated. If targetQuerySelector 22 | is provided, it is used to select descendants to apply the animation to. Use with caution, only 23 | when you're confident that React's reconciliation will preserve these nodes during animation. 24 | Also note querySelectorAll's silly behavior w.r.t. pruning results when being called on a node. 25 | A special value of "children" will use the direct children of the node, since there isn't a 26 | great way to specify that to querySelectorAll. 27 | interruptBehavior: Sets how the previous animation should behave when the "animation" prop is 28 | changed before it’s done. Default is "stop", which halts the animation where it is. "finish" 29 | will jump the animation to its completed appearance. "queue" will run the new animation after 30 | the previous one has finished. 31 | 32 | Unrecognized properties are passed as options to Velocity (e.g. "duration", "delay", "loop"). 33 | 34 | Methods: 35 | runAnimation: Triggers the animation immediately. Useful for when you want an animation that 36 | corresponds to an event but not a particular model state change (e.g. a "bump" when a click 37 | occurs). 38 | */ 39 | /* eslint react/no-find-dom-node: 0 */ 40 | 41 | var _ = { 42 | forEach: require('lodash/forEach'), 43 | isEqual: require('lodash/isEqual'), 44 | keys: require('lodash/keys'), 45 | omit: require('lodash/omit'), 46 | }; 47 | 48 | var React = require('react'); 49 | var ReactDOM = require('react-dom'); 50 | var PropTypes = require('prop-types'); 51 | var Velocity = require('./lib/velocity-animate-shim'); 52 | 53 | class VelocityComponent extends React.Component { 54 | constructor(props) { 55 | super(props); 56 | 57 | this._animationTimeout = null; 58 | 59 | // This public method is kept as self-bound to maintain compatibility with the React.createClass 60 | // version of the component. 61 | this.runAnimation = this.runAnimation.bind(this); 62 | } 63 | 64 | componentDidMount() { 65 | this.runAnimation(); 66 | 67 | // Jump to the end so that the component has the visual appearance of the animation having 68 | // been run to completion. 69 | if (this.props.runOnMount !== true) { 70 | this._finishAnimation(); 71 | } 72 | } 73 | 74 | componentDidUpdate(oldProps) { 75 | if (!_.isEqual(oldProps.animation, this.props.animation)) { 76 | if (this.props.interruptBehavior === 'stop') { 77 | this._stopAnimation(); 78 | } else if (this.props.interruptBehavior === 'finish') { 79 | this._finishAnimation(); 80 | } 81 | 82 | this._scheduleAnimation(); 83 | } 84 | } 85 | 86 | componentWillUnmount() { 87 | this._stopAnimation(); 88 | this._clearVelocityCache(this._getDOMTarget()); 89 | 90 | if (this._animationTimeout) { 91 | clearTimeout(this._animationTimeout); 92 | } 93 | } 94 | 95 | // It's ok to call this externally! By default the animation will be queued up. Pass stop: true in 96 | // to stop the current animation before running. Pass finish: true to finish the current animation 97 | // before running. 98 | runAnimation(config) { 99 | config = config || {}; 100 | 101 | this._animationTimeout = null; 102 | 103 | if (this.props.animation == null) { 104 | return; 105 | } 106 | 107 | if (config.stop) { 108 | Velocity(this._getDOMTarget(), 'stop', true); 109 | } else if (config.finish) { 110 | Velocity(this._getDOMTarget(), 'finishAll', true); 111 | } 112 | 113 | // Delegate all props except for the ones that we have specified as our own via propTypes. 114 | var opts = _.omit(this.props, _.keys(VelocityComponent.propTypes)); 115 | Velocity(this._getDOMTarget(), this.props.animation, opts); 116 | } 117 | 118 | // We trigger animations on a new tick because of a Velocity bug where adding a 119 | // multi-step animation from within a complete callback causes the first 2 animations to run 120 | // simultaneously. 121 | _scheduleAnimation() { 122 | if (this._animationTimeout) { 123 | return; 124 | } 125 | 126 | this._animationTimeout = setTimeout(this.runAnimation, 0); 127 | } 128 | 129 | // Returns one or more DOM nodes to apply the animation to. This is checked every time we start 130 | // or stop an animation, which means that if an animation is proceeding but the element is removed 131 | // from the page, it will run its course rather than ever being stopped. (We go this route 132 | // because of difficulty in tracking what animations are currently being animated, due to both 133 | // chained animations and the need to be able to "stop" an animation before it begins.) 134 | _getDOMTarget() { 135 | var node = ReactDOM.findDOMNode(this); 136 | 137 | if (this.props.targetQuerySelector === 'children') { 138 | return node.children; 139 | } else if (this.props.targetQuerySelector != null) { 140 | return node.querySelectorAll(this.props.targetQuerySelector); 141 | } else { 142 | return node; 143 | } 144 | } 145 | 146 | _finishAnimation() { 147 | Velocity(this._getDOMTarget(), 'finishAll', true); 148 | } 149 | 150 | _stopAnimation() { 151 | Velocity(this._getDOMTarget(), 'stop', true); 152 | } 153 | 154 | // Velocity keeps extensive caches for all animated elements to minimize layout thrashing. 155 | // This can cause a serious memory leak, keeping references to unmounted elements as well 156 | // completion handlers and associated react objects. This crudely clears these references. 157 | _clearVelocityCache(target) { 158 | if (target.length) { 159 | _.forEach(target, this._clearVelocityCache); 160 | } else { 161 | Velocity.Utilities.removeData(target, ['velocity', 'fxqueue']); 162 | } 163 | } 164 | 165 | // This component does not include any DOM footprint of its own, so instead we return our 166 | // child out of render(). (Render must only return a single element, which restricts us to 167 | // one child. If you want to animate multiple children, provide your own wrapper element and 168 | // use the "targetQuerySelector" prop to target its children.) 169 | render() { 170 | return this.props.children; 171 | } 172 | } 173 | 174 | VelocityComponent.propTypes = { 175 | animation: PropTypes.any, 176 | children: PropTypes.element.isRequired, 177 | runOnMount: PropTypes.bool, 178 | targetQuerySelector: PropTypes.string, 179 | interruptBehavior: PropTypes.string, 180 | // Any additional properties will be sent as options to Velocity 181 | }; 182 | 183 | VelocityComponent.defaultProps = { 184 | animation: null, 185 | runOnMount: false, 186 | targetQuerySelector: null, 187 | interruptBehavior: 'stop', 188 | }; 189 | 190 | module.exports = VelocityComponent; 191 | -------------------------------------------------------------------------------- /src/velocity-helpers.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Twitter, Inc. and other contributors 2 | 3 | var _ = { 4 | isObject: require('lodash/isObject'), 5 | }; 6 | var Velocity = require('./lib/velocity-animate-shim'); 7 | 8 | var effectCounter = 0; 9 | 10 | // Takes a Velocity "UI pack effect" definition and registers it with a unique key, returning that 11 | // key (to later pass as a value for the "animation" property). Takes an optional suffix, which can 12 | // be "In" or "Out" to modify UI Pack's behavior. 13 | // 14 | // Unlike what you get from passing a style hash to VelocityComponent's "animation" property, 15 | // Velocity "UI pack effects" can have chained animation calls and specify a "defaultDuration", and 16 | // also can take advantage of "stagger" and "reverse" options on the VelocityComponent. 17 | // 18 | // You will need to manually register the UI Pack with the global Velocity in your application with: 19 | // 20 | // require('velocity'); 21 | // require('velocity-animate/velocity.ui'); 22 | // 23 | // See: http://julian.com/research/velocity/#uiPack 24 | // 25 | // Typical usage: 26 | // 27 | // var Animations = { 28 | // down: VelocityHelpers.registerEffect({ 29 | // defaultDuration: 1100, 30 | // calls: [ 31 | // [{ 32 | // transformOriginX: [ '50%', '50%' ], 33 | // transformOriginY: [ 0, 0 ], 34 | // rotateX: [0, 'spring'], 35 | // }, 1, { 36 | // delay: 100, 37 | // easing: 'ease-in', 38 | // }] 39 | // ], 40 | // }), 41 | // 42 | // up: VelocityHelpers.registerEffect({ 43 | // defaultDuration: 200, 44 | // calls: [ 45 | // [{ 46 | // transformOriginX: [ '50%', '50%' ], 47 | // transformOriginY: [ 0, 0 ], 48 | // rotateX: 160, 49 | // }] 50 | // ], 51 | // }), 52 | // }; 53 | // ... 54 | // 55 | // ... 56 | // 57 | function registerEffect(suffix, animation) { 58 | if (_.isObject(suffix)) { 59 | animation = suffix; 60 | suffix = ''; 61 | } 62 | 63 | var key = 'VelocityHelper.animation.' + effectCounter++ + suffix; 64 | 65 | // No-op on the server for now. 66 | if (Velocity.velocityReactServerShim) { 67 | return key; 68 | } 69 | 70 | if (Velocity.RegisterEffect === undefined) { 71 | throw "Velocity.RegisterEffect not found. You need to require 'velocity-animate/velocity.ui' at a top level for UI Pack."; 72 | } 73 | 74 | Velocity.RegisterEffect(key, animation); 75 | return key; 76 | } 77 | 78 | module.exports = { 79 | registerEffect: registerEffect, 80 | }; 81 | -------------------------------------------------------------------------------- /src/velocity-transition-group.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Twitter, Inc. and other contributors 3 | 4 | Component to add Velocity animations around React transitions. Delegates to the React TransitionGroup 5 | addon. 6 | 7 | Properties 8 | enter: Animation to run on a child component being added 9 | leave: Animation to run on a child component leaving 10 | runOnMount: if true, runs the "enter" animation on the elements that exist as children when this 11 | component is mounted. 12 | enterHideStyle/enterShowStyle: see below. 13 | 14 | Any additional properties (e.g. "className", "component") will be passed to the internal 15 | TransitionGroup. 16 | 17 | "enter" and "leave" should either be a string naming an animation, or a hash with an 18 | "animation" key that can either be a string itself, or a hash of style attributes to animate (this 19 | value is passed to Velocity its the first arg). 20 | 21 | If "enter" or "leave" is a hash, it can additionally have a "style" value that is applied the tick 22 | before the Velocity animation starts. Use this for non-animating properties (like "position") that 23 | are prerequisites for correct animation. The style value is applied using Velocity's JS -> CSS 24 | routines, which may differ from React's. 25 | 26 | Any hash entries beyond "animation" and "style" are passed in an options hash to Velocity. Use this 27 | for options like "stagger", "reverse", &tc. 28 | 29 | By default, this component will immediately hide all entering children with display: 'none', and 30 | unhide them one tick later with display: ''. This is done so that we can coalesce multiple enters 31 | into a single animation, and we want to avoid any popping of elements in while they're collected. If 32 | you prefer a different way of hiding these elements so that e.g. geometry can be immediately 33 | calculated, use the enterHideStyle and enterShowStyle props to provide alternate style hashes for 34 | hiding and revealing entering elements. 35 | 36 | Statics 37 | disabledForTest: Set this to true globally to turn off all custom animation logic. Instead, this 38 | component will behave like a vanilla TransitionGroup. 39 | 40 | Inspired by https://gist.github.com/tkafka/0d94c6ec94297bb67091 41 | */ 42 | /* eslint react/no-find-dom-node: 0 */ 43 | 44 | var _ = { 45 | each: require('lodash/each'), 46 | extend: require('lodash/extend'), 47 | forEach: require('lodash/forEach'), 48 | isEqual: require('lodash/isEqual'), 49 | keys: require('lodash/keys'), 50 | omit: require('lodash/omit'), 51 | map: require('lodash/map'), 52 | }; 53 | var React = require('react'); 54 | var ReactDOM = require('react-dom'); 55 | var PropTypes = require('prop-types'); 56 | var TransitionGroup = require('react-transition-group/TransitionGroup'); 57 | var Transition = require('react-transition-group/Transition').default; 58 | var Velocity = require('./lib/velocity-animate-shim'); 59 | 60 | // Shim requestAnimationFrame for browsers that don't support it, in particular IE 9. 61 | var shimRequestAnimationFrame = 62 | typeof window !== 'undefined' && 63 | (window.requestAnimationFrame || 64 | window.webkitRequestAnimationFrame || 65 | window.mozRequestAnimationFrame || 66 | function(callback) { 67 | window.setTimeout(callback, 0); 68 | }); 69 | 70 | // Fix 'Invalid calling object' error in IE 71 | shimRequestAnimationFrame = 72 | typeof window !== 'undefined' && shimRequestAnimationFrame.bind(window); 73 | 74 | var shimCancelAnimationFrame = 75 | typeof window !== 'undefined' && 76 | (window.cancelAnimationFrame || 77 | window.webkitCancelAnimationFrame || 78 | window.mozCancelAnimationFrame || 79 | function(timeout) { 80 | window.clearTimeout(timeout); 81 | }); 82 | 83 | shimCancelAnimationFrame = 84 | typeof window !== 'undefined' && shimCancelAnimationFrame.bind(window); 85 | 86 | // Internal wrapper for the transitioned elements. Delegates all child lifecycle events to the 87 | // parent VelocityTransitionGroup so that it can co-ordinate animating all of the elements at once. 88 | class VelocityTransitionGroupChild extends React.Component { 89 | lastState = 'appear'; 90 | 91 | componentWillEnter = (node, appearing) => { 92 | this.lastState = appearing ? 'appear' : 'enter'; 93 | }; 94 | 95 | componentWillExit = () => { 96 | this.lastState = 'exit'; 97 | }; 98 | 99 | // We trigger our transitions out of endListener because that gives us access to the done callback 100 | // we can use to tell the Transition that the animation has completed. 101 | endListener = (node, done) => { 102 | switch (this.lastState) { 103 | case 'appear': 104 | this.props.willAppearFunc(node, done); 105 | break; 106 | case 'enter': 107 | this.props.willEnterFunc(node, done); 108 | break; 109 | case 'exit': 110 | this.props.willLeaveFunc(node, done); 111 | break; 112 | } 113 | }; 114 | 115 | componentWillUnmount() { 116 | // Clear references from velocity cache. 117 | Velocity.Utilities.removeData(ReactDOM.findDOMNode(this), [ 118 | 'velocity', 119 | 'fxqueue', 120 | ]); 121 | } 122 | 123 | render() { 124 | const transitionProps = _.omit( 125 | this.props, 126 | _.keys(VelocityTransitionGroupChild.propTypes) 127 | ); 128 | 129 | return React.createElement( 130 | Transition, 131 | { 132 | ...transitionProps, 133 | timeout: null, 134 | addEndListener: this.endListener, 135 | appear: true, 136 | onEnter: this.componentWillEnter, 137 | onExit: this.componentWillExit, 138 | }, 139 | this.props.children 140 | ); 141 | } 142 | } 143 | 144 | VelocityTransitionGroupChild.propTypes = { 145 | children: PropTypes.element.isRequired, 146 | willAppearFunc: PropTypes.func.isRequired, 147 | willEnterFunc: PropTypes.func.isRequired, 148 | willLeaveFunc: PropTypes.func.isRequired, 149 | }; 150 | 151 | class VelocityTransitionGroup extends React.Component { 152 | constructor(props) { 153 | super(props); 154 | 155 | this._scheduledAnimationFrame = null; 156 | this._scheduledAnimationRunFrames = []; 157 | this._entering = []; 158 | this._leaving = []; 159 | 160 | this._timers = []; 161 | this._unmounted = false; 162 | 163 | this.childWillAppear = this.childWillAppear.bind(this); 164 | this.childWillEnter = this.childWillEnter.bind(this); 165 | this.childWillLeave = this.childWillLeave.bind(this); 166 | 167 | this._runAnimations = this._runAnimations.bind(this); 168 | this._wrapChild = this._wrapChild.bind(this); 169 | } 170 | 171 | componentWillUnmount() { 172 | if (this._scheduledAnimationFrame) { 173 | shimCancelAnimationFrame(this._scheduledAnimationFrame); 174 | } 175 | 176 | _.forEach(this._timers, function(timer) { 177 | clearTimeout(timer); 178 | }); 179 | 180 | _.forEach(this._scheduledAnimationRunFrames, function(frame) { 181 | shimCancelAnimationFrame(frame); 182 | }); 183 | 184 | // We don't cancel all the in-process animations, so we use this to know if the component 185 | // is gone when an animation is over. 186 | this._unmounted = true; 187 | } 188 | 189 | render() { 190 | // Pass any props that are not our own on into the TransitionGroup delegate. 191 | var transitionGroupProps = _.omit( 192 | this.props, 193 | _.keys(VelocityTransitionGroup.propTypes) 194 | ); 195 | 196 | return React.createElement( 197 | TransitionGroup, 198 | transitionGroupProps, 199 | !this.constructor.disabledForTest && !Velocity.velocityReactServerShim 200 | ? React.Children.map(this.props.children, this._wrapChild) 201 | : React.Children.map( 202 | this.props.children, 203 | // Wrapping in a no-op Transition to consume the props that 204 | // TransitionGroup gives its children. Fixes react-dom warnings 205 | // in test for those props appearing on divs and such. 206 | child => 207 | child && React.createElement(Transition, { timeout: 0 }, child) 208 | ) 209 | ); 210 | } 211 | 212 | childWillAppear(node, doneFn) { 213 | if (this.props.runOnMount) { 214 | this.childWillEnter(node, doneFn); 215 | } else { 216 | this._finishAnimation(node, this.props.enter); 217 | 218 | // Important to tick over so that any callbacks due to finishing the animation complete first. 219 | // 220 | // Using setTimeout so that doneFn gets called even when the tab is hidden. 221 | var t = setTimeout(() => { 222 | var idx = this._timers.indexOf(t); 223 | if (idx >= 0) { 224 | this._timers.splice(idx, 1); 225 | } 226 | 227 | doneFn(); 228 | }, 0); 229 | this._timers.push(t); 230 | } 231 | } 232 | 233 | childWillEnter(node, doneFn) { 234 | if (this._shortCircuitAnimation(this.props.enter, doneFn)) return; 235 | 236 | // By finishing a "leave" on the element, we put it in the right state to be animated in. Useful 237 | // if "leave" includes a rotation or something that we'd like to have as our starting point, for 238 | // symmetry. 239 | // We use overrideOpts to prevent any "begin" or "complete" callback from triggering in this case, since 240 | // it doesn't make a ton of sense. 241 | this._finishAnimation(node, this.props.leave, { 242 | begin: undefined, 243 | complete: undefined, 244 | }); 245 | 246 | // We're not going to start the animation for a tick, so set the node's display to none (or any 247 | // custom "hide" style provided) so that it doesn't flash in. 248 | _.forEach(this.props.enterHideStyle, function(val, key) { 249 | Velocity.CSS.setPropertyValue(node, key, val); 250 | }); 251 | 252 | this._entering.push({ 253 | node: node, 254 | doneFn: doneFn, 255 | }); 256 | 257 | this._schedule(); 258 | } 259 | 260 | childWillLeave(node, doneFn) { 261 | if (this._shortCircuitAnimation(this.props.leave, doneFn)) return; 262 | 263 | this._leaving.push({ 264 | node: node, 265 | doneFn: doneFn, 266 | }); 267 | 268 | this._schedule(); 269 | } 270 | 271 | // document.hidden check is there because animation completion callbacks won't fire (due to 272 | // chaining off of rAF), which would prevent entering / leaving DOM nodes from being cleaned up 273 | // while the tab is hidden. 274 | // 275 | // Returns true if this did short circuit, false if lifecycle methods should continue with 276 | // their animations. 277 | _shortCircuitAnimation(animationProp, doneFn) { 278 | if ( 279 | document.hidden || 280 | this._parseAnimationProp(animationProp).animation == null 281 | ) { 282 | doneFn(); 283 | 284 | return true; 285 | } else { 286 | return false; 287 | } 288 | } 289 | 290 | _schedule() { 291 | if (this._scheduledAnimationFrame) { 292 | return; 293 | } 294 | 295 | // Need rAF to make sure we're in the same event queue as Velocity from here out. Important 296 | // for avoiding getting wrong interleaving with Velocity callbacks. 297 | this._scheduledAnimationFrame = shimRequestAnimationFrame( 298 | this._runAnimations 299 | ); 300 | } 301 | 302 | // arrow function because this is used as an rAF callback 303 | _runAnimations() { 304 | this._scheduledAnimationFrame = null; 305 | 306 | this._runAnimation(true, this._entering, this.props.enter); 307 | this._runAnimation(false, this._leaving, this.props.leave); 308 | 309 | this._entering = []; 310 | this._leaving = []; 311 | } 312 | 313 | // Used to parse out the 'enter' and 'leave' properties. Handles cases where they are omitted 314 | // as well as when they are just strings and not hashes of animation and options. 315 | _parseAnimationProp(animationProp) { 316 | var animation, opts, style; 317 | 318 | if (typeof animationProp === 'string') { 319 | animation = animationProp; 320 | style = null; 321 | opts = {}; 322 | } else { 323 | animation = animationProp != null ? animationProp.animation : null; 324 | style = animationProp != null ? animationProp.style : null; 325 | opts = _.omit(animationProp, 'animation', 'style'); 326 | } 327 | 328 | return { 329 | animation: animation, 330 | style: style, 331 | opts: opts, 332 | }; 333 | } 334 | 335 | _runAnimation(entering, queue, animationProp) { 336 | if (queue.length === 0) { 337 | return; 338 | } 339 | 340 | var nodes = _.map(queue, 'node'); 341 | var doneFns = _.map(queue, 'doneFn'); 342 | 343 | var parsedAnimation = this._parseAnimationProp(animationProp); 344 | var animation = parsedAnimation.animation; 345 | var style = parsedAnimation.style; 346 | var opts = parsedAnimation.opts; 347 | 348 | // Clearing display reverses the behavior from childWillAppear where all elements are added with 349 | // display: none to prevent them from flashing in before the animation starts. We don't do this 350 | // for the fade/slide animations or any animation that ends in "In," since Velocity will handle 351 | // it for us. 352 | // 353 | // If a custom "enterShowStyle" prop is passed, (i.e. not one that just reverses display: none) 354 | // we always run it, regardless of the animation, since it's probably doing something around 355 | // opacity or positioning that Velocity will not necessarily reset. 356 | if (entering) { 357 | if ( 358 | !_.isEqual(this.props.enterShowStyle, { display: '' }) || 359 | !(/^(fade|slide)/.test(animation) || /In$/.test(animation)) 360 | ) { 361 | style = _.extend({}, this.props.enterShowStyle, style); 362 | } 363 | } 364 | 365 | // Because Safari can synchronously repaint when CSS "display" is reset, we set styles for all 366 | // browsers before the rAF tick below that starts the animation. This way you know in all 367 | // cases that you need to support your static styles being visible on the element before 368 | // the animation begins. 369 | if (style != null) { 370 | _.each(style, function(value, key) { 371 | Velocity.hook(nodes, key, value); 372 | }); 373 | } 374 | 375 | var doneFn = () => { 376 | // calling doneFns after the parent has unmounted leads to errors 377 | if (this._unmounted) { 378 | return; 379 | } 380 | 381 | doneFns.map(function(doneFn) { 382 | doneFn(); 383 | }); 384 | }; 385 | 386 | // For nodes that are entering, we tell the TransitionGroup that we're done with them 387 | // immediately. That way, they can be removed later before their entering animations complete. 388 | // If we're leaving, we stop current animations (which may be partially-completed enter 389 | // animations) so that we can then animate out. Velocity typically makes these transitions 390 | // very smooth, correctly animating from whatever state the element is currently in. 391 | if (entering) { 392 | doneFn(); 393 | doneFn = null; 394 | } else { 395 | Velocity(nodes, 'stop'); 396 | } 397 | 398 | var combinedCompleteFn; 399 | if (doneFn && opts.complete) { 400 | var optsCompleteFn = opts.complete; 401 | combinedCompleteFn = function() { 402 | doneFn(); 403 | // preserve this / args from Velocity so the complete function has context for what completed 404 | optsCompleteFn.apply(this, arguments); 405 | }; 406 | } else { 407 | // One or the other or neither. 408 | combinedCompleteFn = doneFn || opts.complete; 409 | } 410 | 411 | // Bit of a hack. Without this rAF, sometimes an enter animation doesn't start running, or is 412 | // stopped before getting anywhere. This should get us on the other side of both completeFn and 413 | // any _finishAnimation that's happening. 414 | var t = shimRequestAnimationFrame(() => { 415 | var idx = this._scheduledAnimationRunFrames.indexOf(t); 416 | if (idx >= 0) { 417 | this._scheduledAnimationRunFrames.splice(idx, 1); 418 | } 419 | 420 | Velocity( 421 | nodes, 422 | animation, 423 | _.extend({}, opts, { 424 | complete: combinedCompleteFn, 425 | }) 426 | ); 427 | }); 428 | 429 | this._scheduledAnimationRunFrames.push(t); 430 | } 431 | 432 | _finishAnimation(node, animationProp, overrideOpts) { 433 | var parsedAnimation = this._parseAnimationProp(animationProp); 434 | var animation = parsedAnimation.animation; 435 | var style = parsedAnimation.style; 436 | var opts = _.extend({}, parsedAnimation.opts, overrideOpts); 437 | 438 | if (style != null) { 439 | _.each(style, function(value, key) { 440 | Velocity.hook(node, key, value); 441 | }); 442 | } 443 | 444 | if (animation != null) { 445 | // Opts are relevant even though we're immediately finishing, since things like "display" 446 | // can affect the animation outcome. 447 | 448 | Velocity(node, animation, opts); 449 | Velocity(node, 'finishAll', true); 450 | } 451 | } 452 | 453 | _wrapChild(child) { 454 | // Need to guard against falsey children, which React will sometimes pass 455 | // in. 456 | if (!child) { 457 | return null; 458 | } 459 | 460 | return React.createElement( 461 | VelocityTransitionGroupChild, 462 | { 463 | key: child.key, 464 | willAppearFunc: this.childWillAppear, 465 | willEnterFunc: this.childWillEnter, 466 | willLeaveFunc: this.childWillLeave, 467 | }, 468 | child 469 | ); 470 | } 471 | } 472 | 473 | VelocityTransitionGroup.disabledForTest = false; // global, mutable, for disabling animations during test 474 | 475 | VelocityTransitionGroup.propTypes = { 476 | runOnMount: PropTypes.bool, 477 | enter: PropTypes.any, 478 | leave: PropTypes.any, 479 | children: PropTypes.any, 480 | enterHideStyle: PropTypes.object, 481 | enterShowStyle: PropTypes.object, 482 | }; 483 | 484 | VelocityTransitionGroup.defaultProps = { 485 | runOnMount: false, 486 | enter: null, 487 | leave: null, 488 | enterHideStyle: { 489 | display: 'none', 490 | }, 491 | enterShowStyle: { 492 | display: '', 493 | }, 494 | }; 495 | 496 | module.exports = VelocityTransitionGroup; 497 | --------------------------------------------------------------------------------