diff --git a/package.json b/package.json
index ce867d2a..2ff456c7 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,8 @@
"prop-types": "^15.5.8",
"react": "^15.5.4",
"react-dom": "^15.5.4",
- "react-prop-types": "^0.4.0"
+ "react-prop-types": "^0.4.0",
+ "react-transition-group": "^1.1.3"
},
"devDependencies": {
"babel-cli": "^6.23.0",
diff --git a/src/components/animate/animate.jsx b/src/components/animate/animate.jsx
new file mode 100644
index 00000000..97ac28eb
--- /dev/null
+++ b/src/components/animate/animate.jsx
@@ -0,0 +1,76 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import cn from 'classnames';
+import { createStyleSheet } from 'jss-theme-reactor';
+import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
+import modifierHeight from './modifierHeight';
+import easings from '../../styles/transitions/easings';
+
+class Animate extends React.PureComponent {
+ constructor(props, context) {
+ super(props, context);
+ this.idNbr = Math.round(Math.random() * 1000);
+ this.animationName = `${this.props.type}-animation-${this.idNbr}`;
+ this.classes = this.context.styleManager.render(
+ createStyleSheet(`Animate_${this.animationName}`, () => {
+ switch (this.props.type) {
+ case 'height':
+ return {
+ root: {
+ ...modifierHeight({
+ classPrefixSpace: true,
+ name: this.animationName,
+ estimatedHeight: this.props.estimatedHeight,
+ transitionEnterTimeout: this.props.enterTime,
+ transitionLeaveTimeout: this.props.leaveTime,
+ easingEnterFunction: this.props.easingEnterFunction,
+ easingLeaveFunction: this.props.easingLeaveFunction,
+ }),
+ },
+ };
+ default:
+ return { root: {} };
+ }
+ }),
+ );
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+Animate.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ type: PropTypes.oneOf(['height']).isRequired,
+ enterTime: PropTypes.number.isRequired,
+ leaveTime: PropTypes.number.isRequired,
+ easingEnterFunction: PropTypes.string,
+ easingLeaveFunction: PropTypes.string,
+ estimatedHeight: PropTypes.number,
+};
+
+Animate.defaultProps = {
+ type: 'height',
+ enterTime: 200,
+ leaveTime: 200,
+ easingEnterFunction: easings.easeIn,
+ easingLeaveFunction: easings.easeOut,
+ estimatedHeight: 500,
+};
+
+Animate.contextTypes = {
+ styleManager: PropTypes.object.isRequired,
+};
+
+export default Animate;
diff --git a/src/components/animate/animate.md b/src/components/animate/animate.md
new file mode 100644
index 00000000..baf90be9
--- /dev/null
+++ b/src/components/animate/animate.md
@@ -0,0 +1,73 @@
+This is how you should use the Animate component.
+
+```html
+
+ { variableToDecideIfShown ? (
+ Something inside the box in Animate component.
+ ) : null }
+
+```
+
+Below is an example of how it behaves when animating.
+
+ // This is a sample component to illustrate how to use the Animate
+ // component. This was needed to have a toggle button with state.
+ const React = require('react');
+ const PropTypes = require('prop-types');
+ const { createStyleSheet } = require('jss-theme-reactor');
+
+ const divStyle = { height: 50, padding: 10 };
+ const outerDivStyle = { backgroundColor: 'tomato', height: 100, padding: 10, color: 'white' };
+
+ class AnimateMarkdownSample extends React.PureComponent {
+ constructor(props, context) {
+ super(props, context);
+ this.state = { show: true, toggleCount: 0 };
+ this.toggleShow = function() { this.setState({ show: !this.state.show, toggleCount: this.state.toggleCount += 1 }); }.bind(this);
+ this.classes = this.context.styleManager.render(createStyleSheet('AnimateMarkdownSample', () => ({
+ box: { color: 'white', backgroundColor: '#07B' }
+ })));
+ }
+
+ render() {
+ return (
+
+
+
+ {this.state.show && this.state.toggleCount % 3 === 2 ? (
+
+ Even more content inside what will be animated.
+
+ ) : null}
+ {this.state.show ? (
+
+
+ Something inside the box in Animate component.
+
+
+ Some more content inside what will be animated.
+
+ {this.state.toggleCount % 3 === 1 ? (
+
+ Even more content inside what will be animated.
+
+ ) : null}
+
+ ) : nullĀ }
+ {this.state.show && this.state.toggleCount % 3 === 0 ? (
+
+ Even more content inside what will be animated.
+
+ ) : null}
+
+
Some content after the animated stuff.
+
+ );
+ }
+ }
+
+ AnimateMarkdownSample.contextTypes = {
+ styleManager: PropTypes.object.isRequired,
+ };
+
+
diff --git a/src/components/animate/index.js b/src/components/animate/index.js
new file mode 100644
index 00000000..82976206
--- /dev/null
+++ b/src/components/animate/index.js
@@ -0,0 +1 @@
+export { default } from './animate';
diff --git a/src/components/animate/modifierHeight.js b/src/components/animate/modifierHeight.js
new file mode 100644
index 00000000..ce854b16
--- /dev/null
+++ b/src/components/animate/modifierHeight.js
@@ -0,0 +1,38 @@
+import easings from '../../styles/transitions/easings';
+import durations from '../../styles/transitions/durations';
+
+export default ({
+ name,
+ estimatedHeight = 500,
+ classPrefixSpace = false,
+ transitionEnterTimeout = durations.faster,
+ transitionLeaveTimeout = durations.fastest,
+ easingEnterFunction = easings.easeIn,
+ easingLeaveFunction = easings.easeOut,
+}) => ({
+ overflow: 'hidden',
+ maxHeight: 'auto',
+
+ [`&${classPrefixSpace ? ' ' : ''}.${name}`]: {
+ '&-enter': {
+ maxHeight: 0,
+
+ [`&.${name}-enter-active`]: {
+ maxHeight: estimatedHeight,
+ transitionTimingFunction: easingEnterFunction,
+ transitionProperty: 'max-height',
+ transitionDuration: transitionEnterTimeout,
+ },
+ },
+ '&-leave': {
+ maxHeight: estimatedHeight,
+ transitionTimingFunction: easingLeaveFunction,
+ transitionProperty: 'max-height',
+ transitionDuration: transitionLeaveTimeout,
+
+ [`&.${name}-leave-active`]: {
+ maxHeight: 0,
+ },
+ },
+ },
+});
diff --git a/src/index.js b/src/index.js
index b35b9041..f63eb9fc 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,5 @@
import Alert from './components/alert/alert';
+import Animate from './components/animate/animate';
import Avatar from './components/avatar';
import Badge from './components/badge/badge';
import Button from './components/button/button';
@@ -33,6 +34,7 @@ const theme = createTheme();
export {
Alert,
+ Animate,
Avatar,
Badge,
Button,
diff --git a/src/styles/transitions.js b/src/styles/transitions.js
deleted file mode 100644
index 0efa857e..00000000
--- a/src/styles/transitions.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// Easings and durations from material design, see their docs for usage information:
-// https://material.google.com/motion/duration-easing.html#duration-easing-natural-easing-curves
-// https://material.io/guidelines/motion/duration-easing.html#duration-easing-common-durations
-
-const easingInternal = {
- easeInOut: 'cubic-bezier(.4, 0, .2, 1)',
- easeOut: 'cubic-bezier(0, 0, .2, 1)',
- easeIn: 'cubic-bezier(.4, 0, 1, 1)',
- sharp: 'cubic-bezier(.4, 0, .6, 1)',
-};
-
-const durationInternal = {
- shortest: 150,
- shorter: 200,
- short: 250,
-
- standard: 300,
- complex: 375,
-
- enteringScreen: 225,
- leavingScreen: 195,
-};
-
-export default {
- easing: easingInternal,
- duration: durationInternal,
-
- create(props = ['all'], { easing = easingInternal.easeInOut, duration = durationInternal.standard, delay = 0 } = {}) {
- return props.map(prop => `${prop} ${duration}ms ${easing} ${delay}ms`).join(',');
- },
-
- getAutoHeightDuration(height) {
- if (!height) {
- return 0;
- }
-
- const constant = height / 36;
- const duration = (4 + 15 * constant ** 0.25 + constant / 5) * 10;
-
- return Math.round(duration);
- },
-};
diff --git a/src/styles/transitions/durations.js b/src/styles/transitions/durations.js
new file mode 100644
index 00000000..3d293669
--- /dev/null
+++ b/src/styles/transitions/durations.js
@@ -0,0 +1,14 @@
+// Easings and durations from material design, see their docs for usage information:
+// https://material.google.com/motion/duration-easing.html#duration-easing-natural-easing-curves
+// https://material.io/guidelines/motion/duration-easing.html#duration-easing-common-durations
+export default {
+ shortest: 150,
+ shorter: 200,
+ short: 250,
+
+ standard: 300,
+ complex: 400,
+
+ enteringScreen: 225,
+ leavingScreen: 195,
+};
diff --git a/src/styles/transitions/easings.js b/src/styles/transitions/easings.js
new file mode 100644
index 00000000..ffc5f189
--- /dev/null
+++ b/src/styles/transitions/easings.js
@@ -0,0 +1,9 @@
+// Easings and durations from material design, see their docs for usage information:
+// https://material.google.com/motion/duration-easing.html#duration-easing-natural-easing-curves
+// https://material.io/guidelines/motion/duration-easing.html#duration-easing-common-durations
+export default {
+ easeInOut: 'cubic-bezier(.4, 0, .2, 1)',
+ easeOut: 'cubic-bezier(0, 0, .2, 1)',
+ easeIn: 'cubic-bezier(.4, 0, 1, 1)',
+ sharp: 'cubic-bezier(.4, 0, .6, 1)',
+};
diff --git a/src/styles/transitions/index.js b/src/styles/transitions/index.js
new file mode 100644
index 00000000..15fcb173
--- /dev/null
+++ b/src/styles/transitions/index.js
@@ -0,0 +1,22 @@
+import easingInternal from './easings';
+import durationInternal from './durations';
+
+export default {
+ easing: easingInternal,
+ duration: durationInternal,
+
+ create(props = ['all'], { easing = easingInternal.easeInOut, duration = durationInternal.standard, delay = 0 } = {}) {
+ return props.map(prop => `${prop} ${duration}ms ${easing} ${delay}ms`).join(',');
+ },
+
+ getAutoHeightDuration(height) {
+ if (!height) {
+ return 0;
+ }
+
+ const constant = height / 36;
+ const duration = (4 + 15 * constant ** 0.25 + constant / 5) * 10;
+
+ return Math.round(duration);
+ },
+};
diff --git a/test/components/animate/animate.test.js b/test/components/animate/animate.test.js
new file mode 100644
index 00000000..248f33ff
--- /dev/null
+++ b/test/components/animate/animate.test.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
+import { expect } from 'chai';
+import { shallow as enzymeShallow } from 'enzyme';
+import { createShallow } from '../../../src/test-utils';
+import Animate from '../../../src/components/animate/animate';
+
+describe('', () => {
+ const inputText = 'ISK';
+ const shallow = createShallow(enzymeShallow);
+ let topWrapper;
+ let wrapper;
+
+ beforeEach(() => {
+ topWrapper = shallow(
+
+ {inputText}
+ ,
+ );
+ wrapper = topWrapper.childAt(0);
+ });
+
+ it('should render a CSSTransitionGroup', () => {
+ expect(topWrapper.type()).to.equal(CSSTransitionGroup);
+ });
+
+ it(`should render the text ${inputText}`, () => {
+ expect(wrapper.text()).to.equal(inputText);
+ });
+
+ it('should give a unique identifier to each Animate instance', () => {
+ const transitionName = topWrapper.prop('transitionName');
+ const otherWrapper = shallow(
+
+ {inputText}
+ ,
+ );
+
+ const otherTransitionName = otherWrapper.prop('transitionName');
+ expect(transitionName).to.not.equal(otherTransitionName);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index 87b7b2cc..b1912650 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1248,6 +1248,10 @@ chai@^3.5.0:
deep-eql "^0.1.3"
type-detect "^1.0.0"
+chain-function@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc"
+
chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -2040,6 +2044,10 @@ dom-converter@~0.1:
dependencies:
utila "~0.3"
+dom-helpers@^3.2.0:
+ version "3.2.1"
+ resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a"
+
dom-serializer@0, dom-serializer@~0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@@ -5434,7 +5442,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
-prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7:
+prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7:
version "15.5.8"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394"
dependencies:
@@ -5689,6 +5697,15 @@ react-test-renderer@^15.5.4:
fbjs "^0.8.9"
object-assign "^4.1.0"
+react-transition-group@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.1.3.tgz#5e02cf6e44a863314ff3c68a0c826c2d9d70b221"
+ dependencies:
+ chain-function "^1.0.0"
+ dom-helpers "^3.2.0"
+ prop-types "^15.5.6"
+ warning "^3.0.0"
+
react@^15.5.4:
version "15.5.4"
resolved "https://registry.npmjs.org/react/-/react-15.5.4.tgz#fa83eb01506ab237cdc1c8c3b1cea8de012bf047"