import { useEventCallback, useFirstMount, useIsomorphicLayoutEffect, useMergedRefs } from '@fluentui/react-utilities'; import * as React from 'react'; import { PresenceGroupChildContext } from '../contexts/PresenceGroupChildContext'; import { useAnimateAtoms } from '../hooks/useAnimateAtoms'; import { useMotionImperativeRef } from '../hooks/useMotionImperativeRef'; import { useMountedState } from '../hooks/useMountedState'; import { useIsReducedMotion } from '../hooks/useIsReducedMotion'; import { getChildElement } from '../utils/getChildElement'; import { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext'; /** * @internal A private symbol to store the motion definition on the component for variants. */ export const MOTION_DEFINITION = Symbol('MOTION_DEFINITION'); function shouldSkipAnimation(appear, isFirstMount, visible) { return !appear && isFirstMount && !!visible; } export function createPresenceComponent(value) { return Object.assign((props)=>{ 'use no memo'; const itemContext = React.useContext(PresenceGroupChildContext); const merged = { ...itemContext, ...props }; const skipMotions = useMotionBehaviourContext() === 'skip'; const { appear, children, imperativeRef, onExit, onMotionFinish, onMotionStart, onMotionCancel, visible, unmountOnExit, ..._rest } = merged; const params = _rest; const [mounted, setMounted] = useMountedState(visible, unmountOnExit); const child = getChildElement(children); const handleRef = useMotionImperativeRef(imperativeRef); const elementRef = React.useRef(); const ref = useMergedRefs(elementRef, child.ref); const optionsRef = React.useRef({ appear, params, skipMotions }); const animateAtoms = useAnimateAtoms(); const isFirstMount = useFirstMount(); const isReducedMotion = useIsReducedMotion(); const handleMotionStart = useEventCallback((direction)=>{ onMotionStart === null || onMotionStart === void 0 ? void 0 : onMotionStart(null, { direction }); }); const handleMotionFinish = useEventCallback((direction)=>{ onMotionFinish === null || onMotionFinish === void 0 ? void 0 : onMotionFinish(null, { direction }); if (direction === 'exit' && unmountOnExit) { setMounted(false); onExit === null || onExit === void 0 ? void 0 : onExit(); } }); const handleMotionCancel = useEventCallback((direction)=>{ onMotionCancel === null || onMotionCancel === void 0 ? void 0 : onMotionCancel(null, { direction }); }); useIsomorphicLayoutEffect(()=>{ // Heads up! // We store the params in a ref to avoid re-rendering the component when the params change. optionsRef.current = { appear, params, skipMotions }; }); useIsomorphicLayoutEffect(()=>{ const element = elementRef.current; if (!element || shouldSkipAnimation(optionsRef.current.appear, isFirstMount, visible)) { return; } const presenceMotion = typeof value === 'function' ? value({ element, ...optionsRef.current.params }) : value; const atoms = visible ? presenceMotion.enter : presenceMotion.exit; const direction = visible ? 'enter' : 'exit'; const applyInitialStyles = !visible && isFirstMount; const skipAnimation = optionsRef.current.skipMotions; if (!applyInitialStyles) { handleMotionStart(direction); } const handle = animateAtoms(element, atoms, { isReducedMotion: isReducedMotion() }); if (applyInitialStyles) { // Heads up! // .finish() is used in this case to skip animation and apply animation styles immediately handle.finish(); return; } handleRef.current = handle; handle.setMotionEndCallbacks(()=>handleMotionFinish(direction), ()=>handleMotionCancel(direction)); if (skipAnimation) { handle.finish(); } return ()=>{ handle.cancel(); }; }, // Excluding `isFirstMount` from deps to prevent re-triggering the animation on subsequent renders // eslint-disable-next-line react-hooks/exhaustive-deps [ animateAtoms, handleRef, isReducedMotion, handleMotionFinish, handleMotionStart, handleMotionCancel, visible ]); if (mounted) { return React.cloneElement(child, { ref }); } return null; }, { // Heads up! // Always normalize it to a function to simplify types [MOTION_DEFINITION]: typeof value === 'function' ? value : ()=>value }); }