From 3ccfa3b51975c3ae364da95c87a0c7437ed90ba7 Mon Sep 17 00:00:00 2001 From: p-sw Date: Wed, 7 Aug 2024 18:32:13 +0900 Subject: [PATCH] feat: add useAnimatedMount --- packages/react/lib/index.ts | 1 + packages/react/lib/useAnimatedMount.ts | 84 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 packages/react/lib/useAnimatedMount.ts diff --git a/packages/react/lib/index.ts b/packages/react/lib/index.ts index db93e6f..8076ccb 100644 --- a/packages/react/lib/index.ts +++ b/packages/react/lib/index.ts @@ -1,3 +1,4 @@ export * from "./vcn"; export * from "./Slot"; export * from "./useDocument"; +export * from "./useAnimatedMount"; diff --git a/packages/react/lib/useAnimatedMount.ts b/packages/react/lib/useAnimatedMount.ts new file mode 100644 index 0000000..e4be31d --- /dev/null +++ b/packages/react/lib/useAnimatedMount.ts @@ -0,0 +1,84 @@ +import { type MutableRefObject, useCallback, useEffect, useState } from "react"; + +function getCalculatedTransitionDuration( + ref: MutableRefObject, +): number { + let transitionDuration: { + value: number; + unit: string; + } | null; + if (ref.current.computedStyleMap !== undefined) { + transitionDuration = ref.current + .computedStyleMap() + .get("transition-duration") as { value: number; unit: string }; + } else { + const style = /(\d+(\.\d+)?)(.+)/.exec( + window.getComputedStyle(ref.current).transitionDuration, + ); + if (!style) return 0; + transitionDuration = { + value: Number.parseFloat(style[1] ?? "0"), + unit: style[3] ?? style[2] ?? "s", + }; + } + + return ( + transitionDuration.value * + ({ + s: 1000, + ms: 1, + }[transitionDuration.unit] ?? 1) + ); +} + +/* + * Component Mount Component Appear Component Disappear Component Unmount + * v v v v + * |-|=================|------------------------|======================|-| + */ + +function useAnimatedMount( + visible: boolean, + ref: MutableRefObject, + callbacks?: { onMount: () => void; onUnmount: () => void }, +) { + const [state, setState] = useState<{ + isMounted: boolean; + isRendered: boolean; + }>({ isMounted: visible, isRendered: visible }); + + const umountCallback = useCallback(() => { + setState((p) => ({ ...p, isRendered: false })); + + const calculatedTransitionDuration = ref.current + ? getCalculatedTransitionDuration(ref as MutableRefObject) + : 0; + + setTimeout(() => { + setState((p) => ({ ...p, isMounted: false })); + callbacks?.onUnmount?.(); + }, calculatedTransitionDuration); + }, [ref, callbacks]); + + const mountCallback = useCallback(() => { + setState((p) => ({ ...p, isMounted: true })); + callbacks?.onMount?.(); + requestAnimationFrame(function onMount() { + if (!ref.current) return requestAnimationFrame(onMount); + setState((p) => ({ ...p, isRendered: true })); + }); + }, [ref.current, callbacks]); + + useEffect(() => { + console.log(state); + if (!visible && state.isRendered) { + umountCallback(); + } else if (visible && !state.isMounted) { + mountCallback(); + } + }, [state, visible, mountCallback, umountCallback]); + + return state; +} + +export { useAnimatedMount };