Skip to content
This repository has been archived by the owner on Oct 20, 2022. It is now read-only.

Commit

Permalink
feat(Animate): Added new Animate component (#230)
Browse files Browse the repository at this point in the history
* New Animate component. For now it can only animate the height
of the content, but in future maybe more alternatives.

* test(Animate): SHEL-165 Added unit tests for Animate

* refactor(Animate): Change inner workings of Animate

* refactor(Animate): Move modifierHeight into Animate component folder

* feat(Animate): Possibility to change easing functions as props to Animate

* fix(Animate): Review comments fix
  • Loading branch information
willeeklund authored and Andreas Broström committed May 29, 2017
1 parent 11f5f41 commit 76eba45
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 44 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
76 changes: 76 additions & 0 deletions src/components/animate/animate.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<CSSTransitionGroup
className={cn(this.classes.root, this.props.className)}
transitionName={this.animationName}
transitionEnterTimeout={this.props.enterTime}
transitionLeaveTimeout={this.props.leaveTime}
>
{this.props.children}
</CSSTransitionGroup>
);
}
}

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;
73 changes: 73 additions & 0 deletions src/components/animate/animate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
This is how you should use the Animate component.

```html
<Animate type="height" enterTime={300} leaveTime={200} estimatedHeight={200}>
{ variableToDecideIfShown ? (
<div>Something inside the box in Animate component.</div>
) : null }
</Animate>
```

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 (
<div>
<button style={{marginBottom: 10}} onClick={this.toggleShow}>Toggle animation</button>
<Animate enterTime={300} leaveTime={200} estimatedHeight={210}>
{this.state.show && this.state.toggleCount % 3 === 2 ? (
<div style={outerDivStyle}>
Even more content inside what will be animated.
</div>
) : null}
{this.state.show ? (
<div className={this.classes.box}>
<div style={divStyle}>
Something inside the box in Animate component.
</div>
<div style={divStyle}>
Some more content inside what will be animated.
</div>
{this.state.toggleCount % 3 === 1 ? (
<div style={divStyle}>
Even more content inside what will be animated.
</div>
) : null}
</div>
) : null }
{this.state.show && this.state.toggleCount % 3 === 0 ? (
<div style={outerDivStyle}>
Even more content inside what will be animated.
</div>
) : null}
</Animate>
<div>Some content after the animated stuff.</div>
</div>
);
}
}

AnimateMarkdownSample.contextTypes = {
styleManager: PropTypes.object.isRequired,
};

<AnimateMarkdownSample />
1 change: 1 addition & 0 deletions src/components/animate/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './animate';
38 changes: 38 additions & 0 deletions src/components/animate/modifierHeight.js
Original file line number Diff line number Diff line change
@@ -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,
},
},
},
});
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -33,6 +34,7 @@ const theme = createTheme();

export {
Alert,
Animate,
Avatar,
Badge,
Button,
Expand Down
42 changes: 0 additions & 42 deletions src/styles/transitions.js

This file was deleted.

14 changes: 14 additions & 0 deletions src/styles/transitions/durations.js
Original file line number Diff line number Diff line change
@@ -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,
};
9 changes: 9 additions & 0 deletions src/styles/transitions/easings.js
Original file line number Diff line number Diff line change
@@ -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)',
};
22 changes: 22 additions & 0 deletions src/styles/transitions/index.js
Original file line number Diff line number Diff line change
@@ -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);
},
};
42 changes: 42 additions & 0 deletions test/components/animate/animate.test.js
Original file line number Diff line number Diff line change
@@ -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('<Animate />', () => {
const inputText = 'ISK';
const shallow = createShallow(enzymeShallow);
let topWrapper;
let wrapper;

beforeEach(() => {
topWrapper = shallow(
<Animate>
{inputText}
</Animate>,
);
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(
<Animate>
{inputText}
</Animate>,
);

const otherTransitionName = otherWrapper.prop('transitionName');
expect(transitionName).to.not.equal(otherTransitionName);
});
});
19 changes: 18 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 76eba45

Please sign in to comment.