import { useState, forwardRef, useEffect, useLayoutEffect, useRef } from 'react' import { useSpring, animated, SpringConfig, SpringValue } from 'react-spring' import { useMeasure } from 'react-use' export type AnimationFunction = (value: any, props?: object) => object export interface FadeProps { initialOpacity?: number } export interface TranslateProps { vert?: number horiz?: number } export type Side = 'top' | 'left' | 'bottom' | 'right' export interface SlideProps { side?: Side distance?: number } const origins = { center: '50% 50%', top: '50% 0', bottom: '50% 100%', left: '0 50%', right: '100% 50%', 'top-left': '0 0', 'top-right': '100% 0', 'bottom-left': '0 100%', 'bottom-right': '100% 100%' } export type ScaleOrigin = keyof typeof origins export interface ScaleProps { initialScale?: number origin?: keyof typeof origins } const fade: AnimationFunction = ( value, { initialOpacity = 0 }: FadeProps = {} ) => { return { opacity: value.to([0, 1], [initialOpacity, 1]) } } const translate: AnimationFunction = ( value, { vert = 0, horiz = 0 }: TranslateProps = {} ) => ({ translateX: value.to([0, 1], [horiz, 0]), translateY: value.to([0, 1], [vert, 0]) }) const slide: AnimationFunction = ( value, { side = 'top', distance = 10 }: SlideProps = {} ) => { const axis = side === 'left' || side === 'right' ? 'horiz' : 'vert' const dir = side === 'right' || side === 'bottom' ? 1 : -1 return translate(value, { [axis]: distance * dir }) } const scale: AnimationFunction = ( value, { initialScale = 0, origin = 'center' }: ScaleProps = {} ) => ({ transformOrigin: origins[origin], transform: value .to([0, 1], [initialScale, 1]) .to((v: number) => `scale(${v})`) }) const compose = (fns: AnimationFunction[], props?: object): AnimationFunction => (value: any) => { const res = {} fns.forEach((fn) => Object.assign(res, fn(value, props))) return res } const animationFunctions = { fade, translate, slide, scale, compose } const animationPresets = { slideDown: compose([fade, translate], { vert: -10 }), slideUp: compose([fade, translate], { vert: 10 }), slideRight: compose([fade, translate], { horiz: -20 }), slideLeft: compose([fade, translate], { horiz: 20 }), scale: compose([fade, scale], { initialScale: 0.66 }) } const springConfigs = { normal: { tension: 180, friction: 26 }, stiffer: { tension: 240, friction: 26 }, stiff: { tension: 300, friction: 26 }, stiffest: { tension: 450, friction: 26 } } interface AnimatedValueProps { config?: SpringConfig } const useAnimatedValue = ( to: any, { config = springConfigs.stiff }: AnimatedValueProps = {} ) => { const [isRest, setIsRest] = useState(true) const [prevValue, setPrevValue] = useState(to) const [props, api] = useSpring(() => ({ value: to, onRest: () => setIsRest(true), config })) useLayoutEffect(() => { if (prevValue !== to) { setPrevValue(to) setIsRest(false) api.start({ value: to }) } }, [to]) return [props.value, isRest] } interface AppearProps extends React.ComponentProps { animation?: AnimationFunction config?: SpringConfig children?: React.ReactNode delay?: number } const appearDefaultProps = { animation: fade, delay: 0, config: springConfigs.normal } const Appear = forwardRef( (_props: AppearProps, ref) => { const props = _props as AppearProps & typeof appearDefaultProps const { children, animation, config, delay, style, ...restProps } = props const [spring, api] = useSpring(() => ({ value: 0, config })) const timeoutRef = useRef>() useEffect(() => { timeoutRef.current = setTimeout( () => api.start({ value: 1 }), delay ) return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current) } }, []) const animatedStyle = animation(spring.value) return ( {children} ) } ) Appear.defaultProps = appearDefaultProps Appear.displayName = 'Appear' export interface CollapseAnimationProps extends React.HTMLAttributes { openValue: SpringValue children?: React.ReactNode } const CollapseAnimation = forwardRef( (props: CollapseAnimationProps, ref) => { const { openValue, children, ...restProps } = props const [measureRef, { height }] = 'ResizeObserver' in window ? useMeasure() : [null, { height: -1 }] const style: any = { overflowY: 'hidden', ...restProps.style } if ('ResizeObserver' in window) { style.height = openValue.to({ output: [0, height] }) } return ( {/* use display flex, so it measures margins of the children */}
{children}
) } ) CollapseAnimation.displayName = 'Collapse' export interface OpenAnimationProps extends React.HTMLAttributes { openValue: SpringValue fn: AnimationFunction props?: object children?: React.ReactNode } const OpenAnimation = forwardRef( (props, ref) => { const { children, openValue, fn, style, props: animationProps, ...restProps } = props return ( {children} ) } ) export { useAnimatedValue, animationFunctions, animationPresets, Appear, OpenAnimation, CollapseAnimation }