Skip to content

Instantly share code, notes, and snippets.

@wldcordeiro
Created August 8, 2018 00:10
Show Gist options
  • Save wldcordeiro/53cc307596f9c4f198e79593a652a76c to your computer and use it in GitHub Desktop.
Save wldcordeiro/53cc307596f9c4f198e79593a652a76c to your computer and use it in GitHub Desktop.
Transitions and Stuffz
import React, { Component } from 'react'
import { bool, func, number, oneOf, oneOfType, shape, string } from 'prop-types'
import { Transition } from 'react-transition-group'
import stringify from 'json-stable-stringify'
function parseAmount(amount) {
if (typeof amount === 'object') {
return {
in: amount.in != null ? amount.in : 100,
out: amount.out != null ? amount.out : 0,
}
}
return { in: amount, out: 0 }
}
// 25ms is a decent enough amout of time to wait after the CSS transition for
// minor latency.
const BUFFER = 25
// Transition uses four states, for this kind of transition we essentially have two.
const isEnd = state => ['entering', 'exited'].includes(state)
const isHorizontal = dir => ['left', 'right'].includes(dir)
const adjustForAxis = (dir, amount) =>
['left', 'top'].includes(dir) ? -amount : amount
/*
* The idea is that this function is called for prop changes the user controls
* and we can avoid calling it by using component state. The rest of the styling
* relies upon the state provided by `Transition` and will update with that value.
*/
function getStaticStyle(amount, curve, dir, duration) {
const transform = isHorizontal(dir) ? 'translateX' : 'translateY'
const { in: inAmount, out: outAmount } = parseAmount(amount)
const adjustedAmount = adjustForAxis(dir, inAmount)
return {
transition: `all ${duration}ms ${curve}`,
transformOrigin: `${dir} center`,
transform: {
end: `${transform}(${adjustedAmount}%)`,
start: `${transform}(${outAmount}%)`,
},
}
}
export class WithSlide extends Component {
static propTypes = {
// Function that receives `{ style, state }` where `style` is the transition
// style for the start or end state, and `state` is the string state `start`
// or `end` to allow consumers to add additional styles for states.
children: func.isRequired,
// The boolean value to run the transition
in: bool.isRequired,
// Amount can be provided in one of two ways. If you provide a number you
// adjust only the `in` amount of the transition. Providing an object with
// the `in` and `out` values gives you more control.
amount: oneOfType([number, shape({ in: number, out: number })]),
// the easing curve function for the transition to use.
curve: string,
// Direction of exit, this just follows box model.
dir: oneOf(['top', 'left', 'bottom', 'right']),
// Duration in milliseconds of animation.
duration: number,
}
static defaultProps = {
amount: 100,
dir: 'left',
duration: 250,
curve: 'cubic-bezier(0.07, 0.95, 0, 1)',
}
state = { baseStyles: null }
static getDerivedStateFromProps({ amount, curve, dir, duration }, prevState) {
const newStyle = getStaticStyle(amount, curve, dir, duration)
const newStyleStr = stringify(newStyle)
const oldStyleStr = stringify(prevState.baseStyles)
if (newStyleStr !== oldStyleStr) {
return { baseStyles: newStyle }
}
return null
}
getTransitionStyle = transitionState => {
const {
baseStyles: { transform, ...baseStyles },
} = this.state
return {
...baseStyles,
transform: isEnd(transitionState) ? transform.end : transform.start,
}
}
render() {
const { children, duration, in: inProp } = this.props
return (
<Transition timeout={duration + BUFFER} in={inProp}>
{transitionState =>
children({
style: this.getTransitionStyle(transitionState),
state: isEnd(transitionState) ? 'end' : 'start',
})
}
</Transition>
)
}
}
export default WithSlide
import React from 'react'
import { css } from 'glamor'
import { storiesOf } from '@storybook/react'
import {
withKnobs,
boolean,
number,
object,
select,
text,
} from '@storybook/addon-knobs/react'
import WithSlide from './with-slide'
const containerStyle = css({
width: 600,
height: 600,
display: 'flex',
flexWrap: 'nowrap',
border: '2px solid grey',
overflow: 'hidden',
})
const blockStyle = css({
width: 600,
height: 600,
background: 'yellow',
overflow: 'auto',
fontSize: '3rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
})
const defaultTransitionAmounts = { in: 100, out: 0 }
const transitionDirections = ['top', 'right', 'bottom', 'left']
const defaultUserStyles = {
start: { background: 'red' },
end: { height: '20%' },
}
storiesOf('WithSlide', module)
.addDecorator(withKnobs)
.add('Kitchen Sink', () => {
const inProp = boolean('Toggle Transition', true)
const duration = number('Transition Duration', 250)
const dir = select('Transition Direction', transitionDirections, 'left')
const amount = object('Transition Amounts', defaultTransitionAmounts)
const curve = text(
'Transition easing function',
'cubic-bezier(0.07, 0.95, 0, 1)'
)
const useStyle = boolean('Use Custom state style', false)
const userStyle = object('Custom User State Style', defaultUserStyles)
return (
<div {...containerStyle}>
<WithSlide
in={inProp}
curve={curve}
duration={duration}
dir={dir}
amount={amount}
>
{({ style, state }) => (
<div {...css(blockStyle, style, useStyle && userStyle[state])}>
Stuff
</div>
)}
</WithSlide>
</div>
)
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment