├── .babelrc ├── example ├── favicon.png ├── index.html ├── main.scss └── index.jsx ├── src ├── index.js ├── Collapse.jsx ├── utils │ └── ChildMapping.js ├── Fluid.jsx ├── TransitionGroup.js ├── Toggle.jsx └── Transition.js ├── .gitignore ├── webpack.config.js ├── README.md ├── CHANGELOG.md ├── LICENSE ├── webpack.prod.config.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react", "react-native"], 3 | } 4 | -------------------------------------------------------------------------------- /example/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/souporserious/animated-ui/HEAD/example/favicon.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Collapse from './Collapse' 2 | import Fluid from './Fluid' 3 | import Toggle from './Toggle' 4 | import Transition from './Transition' 5 | import TransitionGroup from './TransitionGroup' 6 | 7 | export { Collapse, Fluid, Toggle, Transition, TransitionGroup } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OSX Files 2 | .DS_Store 3 | .Trashes 4 | .Spotlight-V100 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # NPM 9 | node_modules 10 | npm-debug.log 11 | dist 12 | lib 13 | 14 | # General Files 15 | .sass-cache 16 | .hg 17 | .idea 18 | .svn 19 | .cache 20 | .project 21 | .tmp 22 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Animated UI 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var nodeModulesDir = path.resolve(__dirname, 'node_modules'); 3 | 4 | module.exports = { 5 | entry: { 6 | index: ['webpack/hot/dev-server', './example/index.jsx'] 7 | }, 8 | output: { 9 | path: './example', 10 | filename: 'bundle.js' 11 | }, 12 | module: { 13 | loaders: [ 14 | { test: /\.(js|jsx)/, loader: 'babel-loader' }, 15 | { test: /\.scss$/, loader: 'style!css!postcss!sass?sourceMap' } 16 | ] 17 | }, 18 | resolve: { 19 | extensions: ['', '.js', '.jsx'] 20 | }, 21 | devServer: { 22 | contentBase: './example', 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Animated UI 2 | 3 | [![npm version](https://badge.fury.io/js/animated-ui.svg)](https://badge.fury.io/js/animated-ui) 4 | [![Dependency Status](https://david-dm.org/souporserious/animated-ui.svg)](https://david-dm.org/souporserious/animated-ui) 5 | 6 | Animated components for easier UI animations in React. 7 | 8 | ## Install 9 | 10 | `npm install animated-ui --save` 11 | 12 | ```html 13 | 14 | (UMD library exposed as `AnimatedUI`) 15 | ``` 16 | 17 | ## Running Locally 18 | 19 | clone repo 20 | 21 | `git clone git@github.com:souporserious/animated-ui.git` 22 | 23 | move into folder 24 | 25 | `cd ~/animated-ui` 26 | 27 | install dependencies 28 | 29 | `npm install` 30 | 31 | run dev mode 32 | 33 | `npm run dev` 34 | 35 | open your browser and visit: `http://localhost:8080/` 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ### 0.3.2 4 | Upgrade dependencies 5 | 6 | Move `prop-types` from peerDependencies to dependencies 7 | 8 | ### 0.3.1 9 | Remove `isAnimating` logic from `Toggle` until we can figure out why inputs can't stay focused. 10 | 11 | Added the ability to start with an `auto` width/height value or animate to an `auto` width/height value. 12 | 13 | ### 0.3.0 14 | Replace `Animate` with `Toggle` component to keep API simple and allow lazy rendering. 15 | 16 | ### 0.2.2 17 | Added child function with `isAnimating` state to `Animate` component 18 | 19 | ### 0.2.1 20 | Added the ability to animate any color property in `Animate` component 21 | 22 | ### 0.2.0 23 | Added `Animate` component 24 | 25 | Added `render` prop to `Collapse` to allow custom child component 26 | 27 | ### 0.1.1 28 | Fix `package.json` for NPM 29 | 30 | ### 0.1.0 31 | Initial release 32 | -------------------------------------------------------------------------------- /example/main.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | padding: 30px; 7 | font-family: 'Helvetica'; 8 | } 9 | 10 | h1 { 11 | margin: 0; 12 | } 13 | 14 | // Header 15 | .site-header { 16 | background: #b4da55; 17 | } 18 | 19 | // Tabs 20 | .tab-list { 21 | display: flex; 22 | margin-bottom: -1px; 23 | } 24 | 25 | .tab-list-item { 26 | padding: 12px; 27 | border: 1px solid #ccc; 28 | background-color: #ccc; 29 | cursor: pointer; 30 | 31 | &.is-active { 32 | border-bottom-color: #fff; 33 | background-color: transparent; 34 | } 35 | } 36 | 37 | .tab-panels { 38 | border: 1px solid #ccc; 39 | } 40 | 41 | .tab-panel { 42 | > * { 43 | padding: 12px; 44 | } 45 | } 46 | 47 | // Accordion 48 | .accordion-group { 49 | border: 1px solid #ccc; 50 | } 51 | 52 | .accordion { 53 | & + & { 54 | border-top: 1px solid #ccc; 55 | } 56 | } 57 | 58 | .accordion-tab { 59 | padding: 12px; 60 | cursor: pointer; 61 | user-select: none; 62 | } 63 | 64 | .accordion-panel { 65 | border-top: 1px solid #ccc; 66 | background: #f1f1f1; 67 | } 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Animated UI authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var TARGET = process.env.TARGET || null 4 | 5 | const externals = { 6 | react: { 7 | root: 'React', 8 | commonjs2: 'react', 9 | commonjs: 'react', 10 | amd: 'react', 11 | }, 12 | } 13 | 14 | var config = { 15 | entry: { 16 | index: './src/index.js', 17 | }, 18 | output: { 19 | path: path.join(__dirname, 'dist'), 20 | publicPath: 'dist/', 21 | filename: 'animated-ui.js', 22 | sourceMapFilename: 'animated-ui.sourcemap.js', 23 | library: 'AnimatedUI', 24 | libraryTarget: 'umd', 25 | }, 26 | module: { 27 | loaders: [{ test: /\.(js|jsx)/, loader: 'babel-loader' }], 28 | }, 29 | plugins: [], 30 | resolve: { 31 | extensions: ['', '.js', '.jsx'], 32 | }, 33 | externals: externals, 34 | } 35 | 36 | if (TARGET === 'minify') { 37 | config.output.filename = 'animated-ui.min.js' 38 | config.output.sourceMapFilename = 'animated-ui.min.js' 39 | config.plugins.push( 40 | new webpack.optimize.UglifyJsPlugin({ 41 | compress: { 42 | warnings: false, 43 | }, 44 | mangle: { 45 | except: ['React'], 46 | }, 47 | }) 48 | ) 49 | } 50 | 51 | module.exports = config 52 | -------------------------------------------------------------------------------- /src/Collapse.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Children, cloneElement } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Fluid from './Fluid' 4 | import Animated from 'animated/lib/targets/react-dom' 5 | 6 | class Collapse extends Component { 7 | static propTypes = { 8 | open: PropTypes.bool, 9 | lazy: PropTypes.bool, 10 | render: PropTypes.func, 11 | } 12 | 13 | static defaultProps = { 14 | lazy: true, 15 | } 16 | 17 | state = { 18 | renderComponent: this.props.lazy && this.props.open, 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | if (this.props.lazy && this.props.open !== nextProps.open) { 23 | this.setState({ renderComponent: true }) 24 | } 25 | } 26 | 27 | handleComplete = ({ finished }) => { 28 | if (finished && this.props.lazy && !this.props.open) { 29 | this.setState({ renderComponent: false }) 30 | } 31 | } 32 | 33 | render() { 34 | const { open, lazy, style, render, children, ...props } = this.props 35 | return ( 36 | { 40 | const collapseStyles = { 41 | ...animatedStyles, 42 | ...style, 43 | } 44 | 45 | if (isAnimating || !open) { 46 | collapseStyles.overflow = 'hidden' 47 | } 48 | 49 | return ( 50 | this.state.renderComponent && 51 | 52 | {typeof render === 'function' 53 | ? render({ childRef, isAnimating }) 54 | : cloneElement(Children.only(children), { ref: childRef })} 55 | 56 | ) 57 | }} 58 | /> 59 | ) 60 | } 61 | } 62 | 63 | export default Collapse 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animated-ui", 3 | "version": "0.3.2", 4 | "description": "Animated UI components.", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "dist", 8 | "lib" 9 | ], 10 | "scripts": { 11 | "build:lib": "babel src --out-dir lib", 12 | "build": "npm run build:lib && NODE_ENV=production webpack --config webpack.prod.config.js", 13 | "dev": "webpack-dev-server --devtool eval --hot --progress --colors --host 0.0.0.0", 14 | "postbuild": "NODE_ENV=production TARGET=minify webpack --config webpack.prod.config.js", 15 | "prebuild": "rm -rf dist && mkdir dist", 16 | "prepublish": "npm run build" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/souporserious/react-fluid-container" 21 | }, 22 | "keywords": [ 23 | "react", 24 | "reactjs", 25 | "react-component", 26 | "animation", 27 | "ui", 28 | "collapse", 29 | "fluid", 30 | "toggle" 31 | ], 32 | "author": "Travis Arnold (http://souporserious.com)", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/souporserious/animated-ui/issues" 36 | }, 37 | "homepage": "https://github.com/souporserious/animated-ui", 38 | "peerDependencies": { 39 | "react": ">0.13.0", 40 | "react-dom": ">0.13.0" 41 | }, 42 | "dependencies": { 43 | "animated": "0.2.1", 44 | "prop-types": "^15.6.0", 45 | "resize-observer-polyfill": "1.5.0" 46 | }, 47 | "devDependencies": { 48 | "babel-cli": "^6.16.0", 49 | "babel-core": "^6.17.0", 50 | "babel-loader": "^6.2.5", 51 | "babel-plugin-add-module-exports": "^0.2.1", 52 | "babel-preset-es2015": "^6.16.0", 53 | "babel-preset-react": "^6.16.0", 54 | "babel-preset-react-native": "^2.0.0", 55 | "babel-preset-stage-0": "^6.16.0", 56 | "chokidar": "^1.6.1", 57 | "create-styled-element": "^0.4.0", 58 | "css-loader": "^0.25.0", 59 | "glamor": "^2.20.40", 60 | "http-server": "^0.9.0", 61 | "node-libs-browser": "^1.0.0", 62 | "node-sass": "^3.2.0", 63 | "polished": "^1.2.1", 64 | "postcss-loader": "^0.13.0", 65 | "react": "^15.6.1", 66 | "react-aria": "^0.4.0", 67 | "react-dom": "^15.6.1", 68 | "sass-loader": "^4.0.2", 69 | "style-loader": "^0.13.1", 70 | "webpack": "^1.13.2", 71 | "webpack-dev-server": "^1.9.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/utils/ChildMapping.js: -------------------------------------------------------------------------------- 1 | // copied from: https://github.com/reactjs/react-transition-group/blob/master/src/utils/ChildMapping.js 2 | import { Children, isValidElement } from 'react' 3 | 4 | /** 5 | * Given `this.props.children`, return an object mapping key to child. 6 | * 7 | * @param {*} children `this.props.children` 8 | * @return {object} Mapping of key to child 9 | */ 10 | export function getChildMapping(children, mapFn) { 11 | let mapper = child => (mapFn && isValidElement(child) ? mapFn(child) : child) 12 | 13 | let result = Object.create(null) 14 | if (children) 15 | Children.map(children, c => c).forEach(child => { 16 | // run the map function here instead so that the key is the computed one 17 | result[child.key] = mapper(child) 18 | }) 19 | return result 20 | } 21 | 22 | /** 23 | * When you're adding or removing children some may be added or removed in the 24 | * same render pass. We want to show *both* since we want to simultaneously 25 | * animate elements in and out. This function takes a previous set of keys 26 | * and a new set of keys and merges them with its best guess of the correct 27 | * ordering. In the future we may expose some of the utilities in 28 | * ReactMultiChild to make this easy, but for now React itself does not 29 | * directly have this concept of the union of prevChildren and nextChildren 30 | * so we implement it here. 31 | * 32 | * @param {object} prev prev children as returned from 33 | * `ReactTransitionChildMapping.getChildMapping()`. 34 | * @param {object} next next children as returned from 35 | * `ReactTransitionChildMapping.getChildMapping()`. 36 | * @return {object} a key set that contains all keys in `prev` and all keys 37 | * in `next` in a reasonable order. 38 | */ 39 | export function mergeChildMappings(prev, next) { 40 | prev = prev || {} 41 | next = next || {} 42 | 43 | function getValueForKey(key) { 44 | return key in next ? next[key] : prev[key] 45 | } 46 | 47 | // For each key of `next`, the list of keys to insert before that key in 48 | // the combined list 49 | let nextKeysPending = Object.create(null) 50 | 51 | let pendingKeys = [] 52 | for (let prevKey in prev) { 53 | if (prevKey in next) { 54 | if (pendingKeys.length) { 55 | nextKeysPending[prevKey] = pendingKeys 56 | pendingKeys = [] 57 | } 58 | } else { 59 | pendingKeys.push(prevKey) 60 | } 61 | } 62 | 63 | let i 64 | let childMapping = {} 65 | for (let nextKey in next) { 66 | if (nextKeysPending[nextKey]) { 67 | for (i = 0; i < nextKeysPending[nextKey].length; i++) { 68 | let pendingNextKey = nextKeysPending[nextKey][i] 69 | childMapping[nextKeysPending[nextKey][i]] = getValueForKey( 70 | pendingNextKey 71 | ) 72 | } 73 | } 74 | childMapping[nextKey] = getValueForKey(nextKey) 75 | } 76 | 77 | // Finally, add the keys which didn't appear before any key in `next` 78 | for (i = 0; i < pendingKeys.length; i++) { 79 | childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]) 80 | } 81 | 82 | return childMapping 83 | } 84 | -------------------------------------------------------------------------------- /src/Fluid.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Children, cloneElement } from 'react' 2 | import PropTypes from 'prop-types' 3 | import ResizeObserver from 'resize-observer-polyfill' 4 | import Animated from 'animated/lib/targets/react-dom' 5 | 6 | class Fluid extends Component { 7 | static propTypes = { 8 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['auto'])]), 9 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['auto'])]), 10 | onComplete: PropTypes.func, 11 | render: PropTypes.func, 12 | } 13 | 14 | state = { 15 | animatedWidth: new Animated.Value( 16 | isNaN(this.props.width) ? -1 : this.props.width 17 | ), 18 | animatedHeight: new Animated.Value( 19 | isNaN(this.props.height) ? -1 : this.props.height 20 | ), 21 | isAnimating: false, 22 | } 23 | 24 | measuredWidth = -1 25 | measuredHeight = -1 26 | 27 | componentWillMount() { 28 | this.resizeObserver = new ResizeObserver(this.measure) 29 | } 30 | 31 | componentDidUpdate(lastProps) { 32 | const { width, height } = this.props 33 | 34 | if (width !== lastProps.width) { 35 | this.animate({ 36 | animated: this.state.animatedWidth, 37 | toValue: width === 'auto' ? this.measuredWidth : width, 38 | }) 39 | } 40 | 41 | if (height !== lastProps.height) { 42 | this.animate({ 43 | animated: this.state.animatedHeight, 44 | toValue: height === 'auto' ? this.measuredHeight : height, 45 | }) 46 | } 47 | } 48 | 49 | handleRef = node => { 50 | if (this.resizeObserver) { 51 | if (node) { 52 | this.resizeObserver.observe(node) 53 | } else { 54 | this.resizeObserver.disconnect(this._node) 55 | } 56 | } 57 | this._node = node 58 | } 59 | 60 | measure = () => { 61 | const { animatedWidth, animatedHeight } = this.state 62 | const { scrollWidth, scrollHeight } = this._node 63 | 64 | if (this.measuredWidth !== scrollWidth) { 65 | if (this.props.width === 'auto') { 66 | if (this.measuredWidth === -1 && animatedWidth._value === -1) { 67 | animatedWidth.setValue(scrollWidth) 68 | } else { 69 | this.animate({ 70 | animated: animatedWidth, 71 | toValue: scrollWidth, 72 | }) 73 | } 74 | } 75 | this.measuredWidth = scrollWidth 76 | this.forceUpdate() 77 | } 78 | 79 | if (this.measuredHeight !== scrollHeight) { 80 | if (this.props.height === 'auto') { 81 | if (this.measuredHeight === -1 && animatedHeight._value === -1) { 82 | animatedHeight.setValue(scrollHeight) 83 | } else { 84 | this.animate({ 85 | animated: animatedHeight, 86 | toValue: scrollHeight, 87 | }) 88 | } 89 | } 90 | this.measuredHeight = scrollHeight 91 | this.forceUpdate() 92 | } 93 | } 94 | 95 | animate = ({ animated, toValue }) => { 96 | this.setState({ isAnimating: true }) 97 | 98 | Animated.spring(animated, { toValue }).start(({ finished }) => { 99 | if (finished) { 100 | this.setState({ isAnimating: false }) 101 | } 102 | if (typeof this.props.onComplete === 'function') { 103 | this.props.onComplete({ finished }) 104 | } 105 | }) 106 | } 107 | 108 | render() { 109 | const { 110 | width, 111 | height, 112 | style, 113 | children, 114 | render, 115 | onComplete, 116 | ...props 117 | } = this.props 118 | const { animatedWidth, animatedHeight, isAnimating } = this.state 119 | const animatedStyles = {} 120 | 121 | if ( 122 | (width === 'auto' && isAnimating) || 123 | (width !== 'auto' && typeof width !== 'undefined') 124 | ) { 125 | animatedStyles.width = animatedWidth 126 | } 127 | 128 | if ( 129 | (height === 'auto' && isAnimating) || 130 | (height !== 'auto' && typeof height !== 'undefined') 131 | ) { 132 | animatedStyles.height = animatedHeight 133 | } 134 | 135 | if (typeof render === 'function') { 136 | return render({ childRef: this.handleRef, animatedStyles, isAnimating }) 137 | } 138 | 139 | return ( 140 | 147 | {cloneElement(Children.only(children), { ref: this.handleRef })} 148 | 149 | ) 150 | } 151 | } 152 | 153 | export default Fluid 154 | -------------------------------------------------------------------------------- /src/TransitionGroup.js: -------------------------------------------------------------------------------- 1 | // reimplemented from: https://github.com/reactjs/react-transition-group/blob/master/src/TransitionGroup.js 2 | import React, { cloneElement, isValidElement } from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | import { getChildMapping, mergeChildMappings } from './utils/ChildMapping' 6 | 7 | const values = Object.values || (obj => Object.keys(obj).map(k => obj[k])) 8 | 9 | class TransitionGroup extends React.Component { 10 | static childContextTypes = { 11 | transitionGroup: PropTypes.object.isRequired, 12 | } 13 | 14 | static propTypes = { 15 | /** 16 | * `` renders a `
` by default. You can change this 17 | * behavior by providing a `component` prop. 18 | */ 19 | component: PropTypes.any, 20 | 21 | /** 22 | * A set of `` components, that are toggled `in` and out as they 23 | * leave. the `` will inject specific transition props, so 24 | * remember to spread them throguh if you are wrapping the `` as 25 | * with our `` example. 26 | */ 27 | children: PropTypes.node, 28 | 29 | /** 30 | * A convenience prop that enables or disabled appear animations 31 | * for all children. Note that specifiying this will override any defaults set 32 | * on individual children Transitions. 33 | */ 34 | 35 | appear: PropTypes.object, 36 | 37 | /** 38 | * A convenience prop that enables or disabled enter animations 39 | * for all children. Note that specifiying this will override any defaults set 40 | * on individual children Transitions. 41 | */ 42 | enter: PropTypes.object, 43 | 44 | /** 45 | * A convenience prop that enables or disabled exit animations 46 | * for all children. Note that specifiying this will override any defaults set 47 | * on individual children Transitions. 48 | */ 49 | exit: PropTypes.object, 50 | } 51 | 52 | static defaultProps = { 53 | component: 'div', 54 | } 55 | 56 | constructor(props, context) { 57 | super(props, context) 58 | 59 | // Initial children should all be entering, dependent on appear 60 | this.state = { 61 | children: getChildMapping(props.children, child => 62 | cloneElement(child, { 63 | in: true, 64 | appear: this.getProp(child, 'appear', props), 65 | enter: this.getProp(child, 'enter', props), 66 | exit: this.getProp(child, 'exit', props), 67 | onExited: () => { 68 | if (child.props.onExited) { 69 | child.props.onExited() 70 | } 71 | this.handleExited(child.key) 72 | }, 73 | }) 74 | ), 75 | } 76 | } 77 | 78 | getChildContext() { 79 | return { 80 | transitionGroup: { isMounting: !this.appeared }, 81 | } 82 | } 83 | 84 | getProp(child, prop, props) { 85 | // use child config unless explictly set by the Group 86 | return props[prop] != null ? props[prop] : child.props[prop] 87 | } 88 | 89 | componentDidMount() { 90 | this.appeared = true 91 | } 92 | 93 | componentWillReceiveProps(nextProps) { 94 | const prevChildMapping = this.state.children 95 | const nextChildMapping = getChildMapping(nextProps.children) 96 | const children = mergeChildMappings(prevChildMapping, nextChildMapping) 97 | 98 | Object.keys(children).forEach(key => { 99 | let child = children[key] 100 | 101 | if (!isValidElement(child)) return 102 | 103 | const onExited = () => this.handleExited(key) 104 | 105 | const hasPrev = key in prevChildMapping 106 | const hasNext = key in nextChildMapping 107 | 108 | const prevChild = prevChildMapping[key] 109 | const isLeaving = isValidElement(prevChild) && !prevChild.props.in 110 | 111 | // item is new (entering) 112 | if (hasNext && (!hasPrev || isLeaving)) { 113 | children[key] = cloneElement(child, { 114 | onExited, 115 | in: true, 116 | exit: this.getProp(child, 'exit', nextProps), 117 | enter: this.getProp(child, 'enter', nextProps), 118 | }) 119 | } else if (!hasNext && hasPrev && !isLeaving) { 120 | // item is old (exiting) 121 | children[key] = cloneElement(child, { in: false }) 122 | } else if (hasNext && hasPrev && isValidElement(prevChild)) { 123 | // item hasn't changed transition states 124 | // copy over the last transition props; 125 | children[key] = cloneElement(child, { 126 | onExited, 127 | in: prevChild.props.in, 128 | exit: this.getProp(child, 'exit', nextProps), 129 | enter: this.getProp(child, 'enter', nextProps), 130 | }) 131 | } 132 | }) 133 | this.setState({ children }) 134 | } 135 | 136 | handleExited = key => { 137 | let currentChildMapping = getChildMapping(this.props.children) 138 | 139 | if (key in currentChildMapping) return 140 | 141 | this.setState(state => { 142 | const children = { ...state.children } 143 | delete children[key] 144 | return { children } 145 | }) 146 | } 147 | 148 | render() { 149 | const { component: Component, appear, enter, exit, ...props } = this.props 150 | const { children } = this.state 151 | return ( 152 | 153 | {values(children)} 154 | 155 | ) 156 | } 157 | } 158 | 159 | export default TransitionGroup 160 | -------------------------------------------------------------------------------- /src/Toggle.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createElement } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Animated from 'animated/lib/targets/react-dom' 4 | 5 | const UNIT_TRANSFORMS = [ 6 | 'translateX', 7 | 'translateY', 8 | 'translateZ', 9 | 'transformPerspective', 10 | ] 11 | 12 | const DEGREE_TRANFORMS = [ 13 | 'rotate', 14 | 'rotateX', 15 | 'rotateY', 16 | 'rotateZ', 17 | 'skewX', 18 | 'skewY', 19 | 'scaleZ', 20 | ] 21 | 22 | const COLOR_PROPS = [ 23 | 'backgroundColor', 24 | 'borderColor', 25 | 'borderBottomColor', 26 | 'borderLeftColor', 27 | 'borderRightColor', 28 | 'borderTopColor', 29 | 'color', 30 | 'fill', 31 | 'stroke', 32 | ] 33 | 34 | class Toggle extends Component { 35 | static propTypes = { 36 | component: PropTypes.any, 37 | config: PropTypes.object, 38 | type: PropTypes.string, 39 | isOn: PropTypes.bool, 40 | onStyles: PropTypes.object, 41 | offStyles: PropTypes.object, 42 | staticStyles: PropTypes.object, 43 | } 44 | 45 | static defaultProps = { 46 | component: 'div', 47 | config: {}, 48 | type: 'spring', 49 | isOn: false, 50 | lazy: false, 51 | } 52 | 53 | state = { 54 | renderComponent: this.props.lazy ? this.props.isOn : true, 55 | } 56 | 57 | animatingKeys = {} 58 | 59 | animatedStyles = {} 60 | 61 | colorDrivers = {} 62 | 63 | height = null 64 | 65 | node = null 66 | 67 | transformDrivers = [] 68 | 69 | width = null 70 | 71 | componentDidMount() { 72 | this.animatedStyles = this.createAnimatedStyles() 73 | this.animateStyles(this.props.isOn, true) 74 | } 75 | 76 | componentWillReceiveProps(nextProps) { 77 | if (this.props.lazy && this.props.isOn !== nextProps.isOn) { 78 | this.setState({ renderComponent: true }) 79 | } 80 | } 81 | 82 | componentDidUpdate(lastProps) { 83 | if (lastProps.isOn !== this.props.isOn) { 84 | this.animateStyles(this.props.isOn) 85 | } 86 | } 87 | 88 | createAnimatedStyles() { 89 | return Object.keys(this.props.offStyles).reduce((acc, key) => { 90 | const animatedStyles = { ...acc } 91 | if (key === 'transform') { 92 | const offTransform = this.props.offStyles.transform 93 | const onTransform = this.props.onStyles.transform 94 | // create a driver for each transform 95 | this.transformDrivers = offTransform.map((prop, index) => { 96 | const key = Object.keys(prop)[0] 97 | return { 98 | key, 99 | driver: new Animated.Value(0), 100 | offValue: offTransform[index][key], 101 | onValue: onTransform[index][key], 102 | } 103 | }) 104 | // interpolate each driver for the real transform value 105 | animatedStyles.transform = this.transformDrivers.map( 106 | ({ driver, key, offValue, onValue }) => { 107 | if (UNIT_TRANSFORMS.indexOf(key) > -1) { 108 | offValue += 'px' 109 | onValue += 'px' 110 | } else if (DEGREE_TRANFORMS.indexOf(key) > -1) { 111 | offValue += 'deg' 112 | onValue += 'deg' 113 | } 114 | return { 115 | [key]: driver.interpolate({ 116 | inputRange: [0, 1], 117 | outputRange: [offValue, onValue], 118 | }), 119 | } 120 | } 121 | ) 122 | } else if (COLOR_PROPS.indexOf(key) > -1) { 123 | const driver = new Animated.Value(0) 124 | const offColor = this.props.offStyles[key] 125 | const onColor = this.props.onStyles[key] 126 | this.colorDrivers[key] = driver 127 | animatedStyles[key] = driver.interpolate({ 128 | inputRange: [0, 1], 129 | outputRange: [offColor, onColor], 130 | }) 131 | } else { 132 | const offValue = this.props.offStyles[key] 133 | if (key === 'width' || key === 'height') { 134 | animatedStyles[key] = new Animated.Value( 135 | offValue === 'auto' ? this[key] : offValue 136 | ) 137 | } else { 138 | animatedStyles[key] = new Animated.Value(offValue) 139 | } 140 | } 141 | return animatedStyles 142 | }, {}) 143 | } 144 | 145 | animate = (key, instant) => { 146 | this.animatingKeys[key] = true 147 | return ({ driver, toValue }) => { 148 | if (instant) { 149 | driver.setValue(toValue) 150 | } else { 151 | Animated[this.props.type](driver, { 152 | ...this.props.config, 153 | toValue, 154 | }).start(({ finished }) => { 155 | if (finished) { 156 | delete this.animatingKeys[key] 157 | if ( 158 | Object.keys(this.animatingKeys).length === 0 && 159 | this.props.lazy && 160 | !this.props.isOn 161 | ) { 162 | this.setState({ renderComponent: false }) 163 | } 164 | } 165 | }) 166 | } 167 | } 168 | } 169 | 170 | animateStyles(isOn, instant) { 171 | Object.keys(this.animatedStyles).forEach(key => { 172 | const runAnimation = this.animate(key, instant) 173 | if (key === 'transform') { 174 | this.transformDrivers.forEach(({ driver }) => { 175 | runAnimation({ 176 | driver, 177 | toValue: isOn ? 1 : 0, 178 | }) 179 | }) 180 | } else if (COLOR_PROPS.indexOf(key) > -1) { 181 | runAnimation({ 182 | driver: this.colorDrivers[key], 183 | toValue: isOn ? 1 : 0, 184 | }) 185 | } else { 186 | const nextValue = isOn 187 | ? this.props.onStyles[key] 188 | : this.props.offStyles[key] 189 | let toValue = nextValue 190 | if (nextValue === 'auto' && (key === 'width' || key === 'height')) { 191 | toValue = this[key] 192 | } 193 | runAnimation({ 194 | driver: this.animatedStyles[key], 195 | toValue, 196 | }) 197 | } 198 | }) 199 | } 200 | 201 | setRef = component => { 202 | if (component && component.refs.node) { 203 | this.node = component.refs.node 204 | if (this.node) { 205 | const width = this.node.scrollWidth 206 | const height = this.node.scrollHeight 207 | if (!this.width) { 208 | this.width = width 209 | } 210 | if (!this.height) { 211 | this.height = height 212 | } 213 | } 214 | } 215 | } 216 | 217 | render() { 218 | const { 219 | component, 220 | config, 221 | isOn, 222 | lazy, 223 | offStyles, 224 | onStyles, 225 | type, 226 | staticStyles, 227 | ...props 228 | } = this.props 229 | return ( 230 | this.state.renderComponent && 231 | createElement(Animated.createAnimatedComponent(component), { 232 | ref: this.setRef, 233 | style: { ...staticStyles, ...this.animatedStyles }, 234 | ...props, 235 | }) 236 | ) 237 | } 238 | } 239 | 240 | export default Toggle 241 | -------------------------------------------------------------------------------- /example/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | PureComponent, 4 | Children, 5 | createElement, 6 | cloneElement, 7 | } from 'react' 8 | import PropTypes from 'prop-types' 9 | import ReactDOM, { findDOMNode } from 'react-dom' 10 | import { parseToRgb } from 'polished' 11 | 12 | import { Collapse, Fluid, Transition, TransitionGroup, Toggle } from '../src' 13 | 14 | // // enter, appear, leave 15 | // {child}}> 16 | //
Cool
17 | //
Cool
18 | //
Cool
19 | //
20 | 21 | function toRgbaString(color) { 22 | const { red, green, blue, alpha = 1 } = parseToRgb(color) 23 | return `rgba(${red}, ${green}, ${blue}, ${alpha})` 24 | } 25 | 26 | function MyComponent({ innerRef, children }) { 27 | return ( 28 |
36 | {children} 37 |
38 | ) 39 | } 40 | 41 | class TodoList extends React.Component { 42 | constructor(props) { 43 | super(props) 44 | this.state = { items: ['hello', 'world', 'click', 'me'] } 45 | } 46 | handleAdd() { 47 | const newItems = this.state.items.concat([prompt('Enter some text')]) 48 | this.setState({ items: newItems }) 49 | } 50 | handleRemove(i) { 51 | let newItems = this.state.items.slice() 52 | newItems.splice(i, 1) 53 | this.setState({ items: newItems }) 54 | } 55 | render() { 56 | return ( 57 |
58 | 59 | 60 | {this.state.items.map((item, i) => ( 61 | 62 |
63 | {item}{' '} 64 | 65 |
66 |
67 | ))} 68 |
69 |
70 | ) 71 | } 72 | } 73 | 74 | class App extends Component { 75 | state = { 76 | isOpen: false, 77 | isAuto: false, 78 | height: 0, 79 | } 80 | 81 | setInputRef = c => (this.input = c) 82 | 83 | render() { 84 | const { height, isAuto, isOpen } = this.state 85 | return ( 86 |
87 | this.setState({ height: +e.target.value })} 90 | value={height} 91 | /> 92 | 93 | 96 | 97 | 106 | 107 | 108 |
109 |

Collapse

110 | 114 |
115 | This is some really long text that might brek if it is long 116 | enough. This is some more really long text that might brek if it 117 | is long enough. 118 |
119 |
120 |
121 |
122 | 123 | ( 126 | 127 | This is a custom wrapped component using the render prop to pass 128 | the childRef down properly. 129 | 130 | )} 131 | /> 132 | 133 | 147 | Animated 💫 148 | 149 | 150 | 163 | 168 | 169 | 170 | 187 | Color interpolation 188 | 189 | 190 | 204 | 205 | {/* 211 |
It works!!!!!!
212 |
213 | 214 | 218 | {isOpen && ( 219 | 220 | It works!!!!!! 221 | 222 | )} 223 | 224 | 225 | */} 226 | 227 | {/* 234 | 0}> 235 |
236 | {children} 237 |
238 |
} 239 | /> */} 240 | 241 | {/* 245 | 0}> 246 |
247 | {children} 248 |
249 |
} 250 | > 251 |
Cool Beans
252 |
What
253 |
*/} 254 |
255 | ) 256 | } 257 | } 258 | ReactDOM.render(, document.getElementById('app')) 259 | -------------------------------------------------------------------------------- /src/Transition.js: -------------------------------------------------------------------------------- 1 | // reimplemented from: https://github.com/reactjs/react-transition-group/blob/master/src/Transition.js 2 | import Animated from 'animated/lib/targets/react-dom' 3 | import PropTypes from 'prop-types' 4 | import React, { Component } from 'react' 5 | 6 | export const UNMOUNTED = 'unmounted' 7 | export const EXITED = 'exited' 8 | export const ENTERING = 'entering' 9 | export const ENTERED = 'entered' 10 | export const EXITING = 'exiting' 11 | 12 | function rehydrateStyles(styles) {} 13 | 14 | function buildAnimatedStyles(styles) { 15 | return Object.keys(styles).reduce( 16 | (acc, key) => ({ 17 | ...acc, 18 | [key]: new Animated.Value(styles[key]), 19 | }), 20 | {} 21 | ) 22 | } 23 | 24 | class Transition extends Component { 25 | static contextTypes = { 26 | transitionGroup: PropTypes.object, 27 | } 28 | 29 | static childContextTypes = { 30 | transitionGroup: () => {}, 31 | } 32 | 33 | constructor(props, context) { 34 | super(props, context) 35 | 36 | let parentGroup = context.transitionGroup 37 | // In the context of a TransitionGroup all enters are really appears 38 | let appear = 39 | parentGroup && !parentGroup.isMounting ? props.enter : props.appear 40 | 41 | let initialStatus 42 | this.nextStatus = null 43 | 44 | if (props.in) { 45 | if (appear) { 46 | initialStatus = EXITED 47 | this.nextStatus = ENTERING 48 | } else { 49 | initialStatus = ENTERED 50 | } 51 | } else { 52 | if (props.lazy) { 53 | initialStatus = UNMOUNTED 54 | } else { 55 | initialStatus = EXITED 56 | } 57 | } 58 | 59 | this.state = { 60 | status: initialStatus, 61 | driver: new Animated.Value(0), 62 | } 63 | 64 | this.nextCallback = null 65 | } 66 | 67 | getChildContext() { 68 | return { transitionGroup: null } // allows for nested Transitions 69 | } 70 | 71 | componentDidMount() { 72 | this.updateStatus(true) 73 | } 74 | 75 | componentWillReceiveProps(nextProps) { 76 | const { status } = this.state 77 | 78 | if (nextProps.in) { 79 | if (status === UNMOUNTED) { 80 | this.setState({ status: EXITED }) 81 | } 82 | if (status !== ENTERING && status !== ENTERED) { 83 | this.nextStatus = ENTERING 84 | } 85 | } else { 86 | if (status === ENTERING || status === ENTERED) { 87 | this.nextStatus = EXITING 88 | } 89 | } 90 | } 91 | 92 | componentDidUpdate() { 93 | this.updateStatus() 94 | } 95 | 96 | updateStatus(mounting = false) { 97 | if (this.nextStatus !== null) { 98 | // nextStatus will always be ENTERING or EXITING. 99 | 100 | if (this.nextStatus === ENTERING) { 101 | this.performEnter(mounting) 102 | } else { 103 | this.performExit() 104 | } 105 | 106 | this.nextStatus = null 107 | } else if (this.props.unmountOnExit && this.state.status === EXITED) { 108 | this.setState({ status: UNMOUNTED }) 109 | } 110 | } 111 | 112 | performEnter(mounting) { 113 | const { appear, enter, exit } = this.props 114 | const appearing = this.context.transitionGroup 115 | ? this.context.transitionGroup.isMounting 116 | : mounting 117 | const styles = {} 118 | 119 | // no enter animation skip right to ENTERED 120 | // if we are mounting and running this it means appear _must_ be set 121 | if (!mounting && !enter) { 122 | this.setState({ status: ENTERED }) 123 | return 124 | } 125 | 126 | // set up interpolations 127 | Object.keys(enter).forEach(key => { 128 | const enterStyle = enter[key] 129 | const initialStyle = appearing ? appear[key] : exit[key] 130 | styles[key] = this.state.driver.interpolate({ 131 | inputRange: [0, 1], 132 | outputRange: [ 133 | Math.min(enterStyle, initialStyle), 134 | Math.max(enterStyle, initialStyle), 135 | ], 136 | }) 137 | }) 138 | 139 | // once interpolations have been set, we can now drive the animation 140 | this.state.driver.stopAnimation(() => { 141 | this.setState({ status: ENTERING, styles }, () => { 142 | Animated.spring(this.state.driver, { 143 | toValue: 1, 144 | }).start(({ finished }) => { 145 | if (finished) { 146 | this.setState({ status: ENTERED }) 147 | } 148 | }) 149 | }) 150 | }) 151 | } 152 | 153 | performExit() { 154 | const { appear, enter, exit } = this.props 155 | const styles = {} 156 | 157 | // no exit animation skip right to EXITED 158 | if (!exit) { 159 | this.setState({ status: EXITED }) 160 | return 161 | } 162 | 163 | // set up interpolations 164 | Object.keys(exit).forEach(key => { 165 | const exitStyle = exit[key] 166 | const initialStyle = appear ? appear[key] : enter[key] 167 | styles[key] = this.state.driver.interpolate({ 168 | inputRange: [0, 1], 169 | outputRange: [ 170 | Math.min(exitStyle, initialStyle), 171 | Math.max(exitStyle, initialStyle), 172 | ], 173 | }) 174 | }) 175 | 176 | // once interpolations have been set, we can now drive the animation 177 | this.state.driver.stopAnimation(() => { 178 | this.setState({ status: EXITING, styles }, () => { 179 | Animated.spring(this.state.driver, { 180 | toValue: 0, 181 | }).start(({ finished }) => { 182 | if (finished) { 183 | this.setState({ status: EXITED }, () => { 184 | this.props.onExited && this.props.onExited() 185 | }) 186 | } 187 | }) 188 | }) 189 | }) 190 | } 191 | 192 | render() { 193 | const { 194 | in: inProp, 195 | lazy, 196 | appear, 197 | enter, 198 | exit, 199 | style, 200 | children, 201 | onEntered, 202 | onExited, 203 | ...props 204 | } = this.props 205 | const { status, styles } = this.state 206 | 207 | if (status === UNMOUNTED) { 208 | return null 209 | } 210 | 211 | return ( 212 | 213 | {children} 214 | 215 | ) 216 | } 217 | } 218 | 219 | Transition.propTypes = { 220 | /** 221 | * Generally a React element to animate, all unknown props on Transition are 222 | * transfered to the **single** child element. 223 | * 224 | * For advanced uses, a `function` child can be used instead of a React element. 225 | * This function is called with the current transition status 226 | * ('entering', 'entered', 'exiting', 'exited', 'unmounted'), which can used 227 | * to apply context specific props to a component. 228 | * 229 | * ```jsx 230 | * 231 | * {(status) => ( 232 | * 233 | * )} 234 | * 235 | * ``` 236 | */ 237 | // children: PropTypes.oneOfType([ 238 | // PropTypes.func.isRequired, 239 | // PropTypes.element.isRequired, 240 | // ]).isRequired, 241 | 242 | /** 243 | * Show the component; triggers the enter or exit states 244 | */ 245 | in: PropTypes.bool, 246 | 247 | /** 248 | * Unmount the component (remove it from the DOM) when it is not shown 249 | */ 250 | lazy: PropTypes.bool, 251 | 252 | /** 253 | * Styles applied when component appears in DOM 254 | */ 255 | appear: PropTypes.object, 256 | 257 | /** 258 | * Styles applied on enter. 259 | */ 260 | enter: PropTypes.object, 261 | 262 | /** 263 | * Styles applied on exit. 264 | */ 265 | exit: PropTypes.object, 266 | 267 | /** 268 | * Callback fired before the "entering" status is applied. 269 | * 270 | * @type Function(node: HtmlElement, isAppearing: bool) 271 | */ 272 | onEnter: PropTypes.func, 273 | 274 | /** 275 | * Callback fired after the "entering" status is applied. 276 | * 277 | * @type Function(node: HtmlElement, isAppearing: bool) 278 | */ 279 | onEntering: PropTypes.func, 280 | 281 | /** 282 | * Callback fired after the "enter" status is applied. 283 | * 284 | * @type Function(node: HtmlElement, isAppearing: bool) 285 | */ 286 | onEntered: PropTypes.func, 287 | 288 | /** 289 | * Callback fired before the "exiting" status is applied. 290 | * 291 | * @type Function(node: HtmlElement) 292 | */ 293 | onExit: PropTypes.func, 294 | 295 | /** 296 | * Callback fired after the "exiting" status is applied. 297 | * 298 | * @type Function(node: HtmlElement) 299 | */ 300 | onExiting: PropTypes.func, 301 | 302 | /** 303 | * Callback fired after the "exited" status is applied. 304 | * 305 | * @type Function(node: HtmlElement) 306 | */ 307 | onExited: PropTypes.func, 308 | } 309 | 310 | // Name the function so it is clearer in the documentation 311 | function noop() {} 312 | 313 | Transition.defaultProps = { 314 | in: false, 315 | 316 | lazy: false, 317 | 318 | // onEnter: noop, 319 | // onEntering: noop, 320 | // onEntered: noop, 321 | // 322 | // onExit: noop, 323 | // onExiting: noop, 324 | // onExited: noop, 325 | } 326 | 327 | export default Transition 328 | --------------------------------------------------------------------------------