import React, { useState } from 'react' import { useDebouncedCallback } from 'use-debounce' type AnimationProps = { isAnimIn?: boolean isAnimOut?: boolean prevIsOpen?: boolean animationDuration?: number isVisible: boolean } const ANIM_DURATION_MS = 400 export const AnimationContext = React.createContext({ isVisible: false, isAnimIn: false, isAnimOut: false, }) /** * Simply applies a css animation to transition a component in and out. * When the component is no longer needed, it will no longer be rendered to the * dom. */ export const AnimationProvider = ({ isVisible, animationDuration = ANIM_DURATION_MS, ...otherProps }: AnimationProps & { children?: React.ReactNode }): JSX.Element => { const [isAnimIn, setIsAnimIn] = useState(false) const [isAnimOut, setIsAnimOut] = useState(false) const [prevIsOpen, setPrevIsOpen] = useState(false) // Keeps the modal visible while the css animation is completing const trackAnimOutCompleted = useDebouncedCallback( () => { setIsAnimOut(false) }, animationDuration, { leading: false }, ) // Allows us to flash the component in an "invisible" state, for one frame. // Then set it to "visible". This allows us to make sure the css transition // actually works. const trackAnimInCompleted = useDebouncedCallback( () => { setIsAnimIn(false) }, 0, { leading: false }, ) if (isVisible !== prevIsOpen) { setPrevIsOpen(isVisible) if (!isVisible) { trackAnimInCompleted.cancel() setIsAnimOut(true) trackAnimOutCompleted() } else { trackAnimOutCompleted.cancel() setIsAnimIn(true) trackAnimInCompleted() } } return ( ) } export const useAnimation = (): AnimationProps => { const context = React.useContext(AnimationContext) if (!context) { throw new Error('useAnimation must be used within a AnimationProvider') } return context }