UNPKG

react-css-transition-hook

Version:

Minimal, zero-dependency React hook to CSS class name based transitions.

117 lines (104 loc) 3.74 kB
import { TransitionEventHandler, useCallback, useEffect, useState } from "react"; /** * Options of the {@link useTransition} hook. */ interface UseTransitionOpts { /** * Whether the initial enter transition, when the state starts with `true` (component is visible) * should be disabled/skipped. */ disableInitialEnterTransition?: boolean; /** * The CSS classnames that should be returned when the enter transition is about to start. During * the enter animation the classnames will be changed from `entering` to `entered`. */ entering?: string; /** * The CSS classnames that should be returned when the enter transition is in progress. During * the enter animation the classnames will be changed from `entering` to `entered`. */ entered?: string; /** * The CSS classnames that should be returned when the exit transition is about to start. During * the exit animation the classnames will be changed from `exiting` to `exited`. */ exiting?: string; /** * The CSS classnames that should be returned when the exit transition is in progress. During * the exit animation the classnames will be changed from `exiting` to `exited`. */ exited?: string; } /** * The step the transition is in. */ export type TransitionStep = "entering" | "entered" | "exiting" | "exited" | null; /** * Transition between states using CSS classnames. Changing a state to `actualState` is delayed * until the transition has been completed. * * @param desiredState - the desired state the hook should transition to * @param opts - a set of options controlling the transition * * @returns a pair of `[currentState, className, transition]`, where `currentState` is the current * state that is changed to `desiredState` once the transition completed, `className` is the current * set of CSS class names used for the transition, and `transition` is the name of the transition * the hook is currently in. The class names are set according to the `opts`. */ export function useTransition( desiredState: boolean, opts: UseTransitionOpts ): [boolean, TransitionProps, TransitionStep] { const [currentState, setCurrentState] = useState( Boolean(desiredState && opts.disableInitialEnterTransition) ); const [transition, setTransition] = useState<TransitionStep>(() => desiredState ? "entered" : null ); useEffect(() => { // exited -> entering if (!currentState && desiredState) { setCurrentState(true); setTransition("entering"); runAfterFramePaint(() => setTransition("entered")); } // entered -> exited else if (currentState && !desiredState) { setTransition("exiting"); runAfterFramePaint(() => setTransition("exited")); } }, [currentState, desiredState]); const onTransitionEnd = useCallback(() => { if (!desiredState) { setCurrentState(false); setTransition(null); } }, [desiredState]); return [ currentState, { className: transition ? opts[transition] ?? "" : "", onTransitionEnd }, transition, ]; } /** * Properties of the element that should have the transition. It is recommended to spread these * properties into the element. */ export interface TransitionProps { /** * The classnames that control the transition. */ className: string; /** * An event handler for the `transitionEnd` event that is used to detect once a certain transition * is finished. */ onTransitionEnd: TransitionEventHandler; } function runAfterFramePaint(callback: () => void) { requestAnimationFrame(() => { const messageChannel = new MessageChannel(); messageChannel.port1.onmessage = callback; messageChannel.port2.postMessage(undefined); }); }