import { defaultTransitionTimeMs } from 'config' import { className } from 'lib/css' import { cloneElement, toChildArray } from 'preact' import { FC } from 'preact/compat' import { useEffect, useMemo, useRef, useState } from 'preact/hooks' import useTimeout from 'ui/hooks/use-timeout' import { ValueOf, childIsVNode } from 'ui/utils/general-utils' const transitionClasses = { visible: className('transition--visible'), in: className('transition--in'), visuallyHidden: className('visually-hidden'), } const transitionClassesArray = Object.values(transitionClasses) export const useStableCallback = (callback) => { const callbackRef = useRef(null) callbackRef.current = callback const isFunction = typeof callback === 'function' return useMemo(() => { // @ts-ignore return isFunction ? (...args) => callbackRef.current(...args) : undefined }, [isFunction]) } export const transitionStartStates = { notRendered: 'notRendered', rendered: 'rendered', visuallyHidden: 'visuallyHidden', } as const type InOutTransitionProps = { isActive: boolean timeout?: number transitionStartState?: keyof typeof transitionStartStates onInTransitionComplete?: Function onOutTransitionComplete?: Function exitAfter?: number enterDelay?: number } const InOutTransition: FC = ({ children, isActive, timeout = defaultTransitionTimeMs, transitionStartState = transitionStartStates.notRendered, onInTransitionComplete = () => {}, onOutTransitionComplete = () => {}, exitAfter, enterDelay = 0, }) => { const prevIsActive = useRef(false) const activeTimeoutRef = useRef | null>(null) const activeRafRef = useRef | null>( null, ) const isVisuallyHidden = transitionStartState === transitionStartStates.visuallyHidden const [activeTransitionClasses, setActiveTransitionClasses] = useState< ValueOf[] >(() => (isVisuallyHidden ? [transitionClasses.visuallyHidden] : [])) const [inState, setInState] = useState(() => enterDelay === 0) const transitionOutAfter = useMemo( () => (exitAfter ? exitAfter + enterDelay : exitAfter), [enterDelay, exitAfter], ) useTimeout(() => setInState(false), isActive ? transitionOutAfter : undefined) useTimeout(() => setInState(true), isActive ? enterDelay : undefined) const transitionIn = useMemo(() => inState && isActive, [isActive, inState]) const onInTransitionCompleteHandler = useStableCallback( onInTransitionComplete, ) const onOutTransitionCompleteHandler = useStableCallback( onOutTransitionComplete, ) const renderChildren = transitionStartState !== 'notRendered' || activeTransitionClasses.length > 0 useEffect(() => { if (prevIsActive.current && !transitionIn) { setActiveTransitionClasses([transitionClasses.visible]) activeTimeoutRef.current = setTimeout(() => { setActiveTransitionClasses([ ...(isVisuallyHidden ? [transitionClasses.visuallyHidden] : []), ]) if (onOutTransitionCompleteHandler) { activeRafRef.current = requestAnimationFrame(() => { onOutTransitionCompleteHandler() }) } }, timeout) } if (!prevIsActive.current && transitionIn) { setActiveTransitionClasses([transitionClasses.visible]) // Doubling up on rAF as a single rAF can be too slow for the // animation transition to be resolved. activeRafRef.current = requestAnimationFrame(() => { activeRafRef.current = requestAnimationFrame(() => { setActiveTransitionClasses([ transitionClasses.visible, transitionClasses.in, ]) if (onInTransitionCompleteHandler) { activeTimeoutRef.current = setTimeout(() => { onInTransitionCompleteHandler() }, timeout) } }) }) } prevIsActive.current = transitionIn return () => { if (activeTimeoutRef.current) clearTimeout(activeTimeoutRef.current) if (activeRafRef.current) cancelAnimationFrame(activeRafRef.current) } }, [ isVisuallyHidden, onInTransitionCompleteHandler, onOutTransitionCompleteHandler, timeout, transitionIn, ]) return ( <> {renderChildren && toChildArray(children) .filter(childIsVNode) .map((child) => { const { className: childClassName = '' } = child.props const cleanClasses = childClassName .split(' ') .filter((cl) => !transitionClassesArray.includes(cl)) return cloneElement(child, { className: [...cleanClasses, ...activeTransitionClasses].join( ' ', ), }) })} ) } export default InOutTransition