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"