UNPKG

@navikt/ds-react

Version:

React components from the Norwegian Labour and Welfare Administration.

146 lines (127 loc) 4.73 kB
"use client"; import { useMemo, useState } from "react"; import ReactDOM from "react-dom"; import { useClientLayoutEffect } from "../../../util"; type TransitionStatus = "entering" | "exiting" | "idle" | undefined; /** * Transition status state machine for components that animate between an * `open` boolean and actual mount/unmount. Keeps the element mounted while * exit animations run and introduces an optional stable `idle` phase. * * Adapted from MUI Base + Floating UI examples: * - Source: https://github.com/mui/base-ui/blob/6fd69008d83561dbe75ff89acf270f0fac3e0049/packages/react/src/utils/useTransitionStatus.ts * - Originally based on https://github.com/floating-ui/floating-ui/blob/7c33a3d0198a9b523d54ae2c37cedb315a309452/packages/react/src/hooks/useTransition.ts * * States (transitionStatus): * - "entering" : just entered (initial frame of enter) OR re-entering to reach "idle". * - "idle" : stable open (only when `enableIdleState === true`). * - "exiting" : exit animation is running; element still mounted. * - undefined : closed (unmounted) OR stable open when idle state is disabled. * When `enableIdleState` is false we clear the label after * the first frame so styling can rely purely on `mounted`. * * Distinction: * - `mounted` tells you whether to render DOM. * - `transitionStatus` tells you which phase-specific classes to apply. * * Frame separation: state changes that must trigger CSS transitions are * scheduled with `requestAnimationFrame` so the browser sees distinct style * mutations across frames (avoids missed transitions due to batching). * * @param open Controls visibility lifecycle. * @param enableIdleState Insert a persistent "idle" phase after entering. * @param deferExitingState Delay entering the exit phase by one frame to allow * measurement / layout work before applying exit styles. */ function useTransitionStatus( open: boolean, enableIdleState: boolean = false, deferExitingState: boolean = false, ) { const [mounted, setMounted] = useState(open); const [transitionStatus, setTransitionStatus] = useState<TransitionStatus>( open && enableIdleState ? "idle" : undefined, ); /* Opening: mount immediately and label as "entering" for the first frame. */ if (open && !mounted) { setMounted(true); setTransitionStatus("entering"); } /* Closing (no defer): begin exit animation right away. */ if ( !open && mounted && transitionStatus !== "exiting" && !deferExitingState ) { setTransitionStatus("exiting"); } /* Cleanup: after unmount post-exit ensure status returns to undefined. */ if (!open && !mounted && transitionStatus === "exiting") { setTransitionStatus(undefined); } /* Deferred closing: provide one frame to measure / flush layout before exit styles. */ useClientLayoutEffect(() => { if ( !open && mounted && transitionStatus !== "exiting" && deferExitingState ) { const frame = requestAnimationFrame(() => { setTransitionStatus("exiting"); }); return () => { cancelAnimationFrame(frame); }; } return undefined; }, [open, mounted, transitionStatus, deferExitingState]); /* Enter (no idle): hold "entering" for one frame, then clear label (stable open). */ useClientLayoutEffect(() => { if (!open || enableIdleState) { return undefined; } const frame = requestAnimationFrame(() => { ReactDOM.flushSync(() => { setTransitionStatus(undefined); }); }); return () => { cancelAnimationFrame(frame); }; }, [enableIdleState, open]); useClientLayoutEffect(() => { if (!open || !enableIdleState) { return undefined; } /* * We can enter this state by re-opening before exiting-state was finished * (open → close → open in quick succession). */ if (open && mounted && transitionStatus !== "idle") { setTransitionStatus("entering"); } /** * Next frame, after "entering", switch to persistent "idle" state. * By delaying to the next frame we ensure any "entering" styles are * applied and the transition can run. */ const frame = requestAnimationFrame(() => { setTransitionStatus("idle"); }); return () => { cancelAnimationFrame(frame); }; }, [enableIdleState, open, mounted, setTransitionStatus, transitionStatus]); return useMemo( () => ({ mounted, setMounted, transitionStatus, }), [mounted, transitionStatus], ); } export { useTransitionStatus }; export type { TransitionStatus };