├── .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 | [](https://badge.fury.io/js/animated-ui)
4 | [](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 |
this.handleAdd()}>Add Item
59 |
60 | {this.state.items.map((item, i) => (
61 |
62 |
63 | {item}{' '}
64 | this.handleRemove(i)}>remove
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 |
this.setState({ isAuto: !isAuto })}>
94 | Auto Height {isAuto ? 'Off' : 'On'}
95 |
96 |
97 |
99 | this.setState(
100 | { isOpen: !isOpen },
101 | () => this.input && this.input.focus()
102 | )}
103 | >
104 | Collapse Toggle
105 |
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 |
--------------------------------------------------------------------------------