'use client'; import { useEventCallback, useFirstMount, useIsomorphicLayoutEffect } 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 { useChildElement } from '../utils/useChildElement'; import { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext'; import { createMotionComponent } from './createMotionComponent'; /** * A private symbol to store the motion definition on the component for variants. * * @internal */ export const PRESENCE_MOTION_DEFINITION = Symbol('PRESENCE_MOTION_DEFINITION'); const INTERRUPTABLE_MOTION_SYMBOL = Symbol.for('interruptablePresence'); 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, childRef] = useChildElement(children, mounted); const handleRef = useMotionImperativeRef(imperativeRef); 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 = childRef.current; if (!element) { return; } let handle; function cleanup() { if (!handle) { return; } // Heads up! // // If the animation is interruptible & is running, we don't want to cancel it as it will be reversed in // the next effect. if (IS_EXPERIMENTAL_INTERRUPTIBLE_MOTION && handle.isRunning()) { return; } handle.cancel(); handleRef.current = undefined; } const presenceMotion = typeof value === 'function' ? value({ element, ...optionsRef.current.params }) : value; const IS_EXPERIMENTAL_INTERRUPTIBLE_MOTION = presenceMotion[INTERRUPTABLE_MOTION_SYMBOL]; if (IS_EXPERIMENTAL_INTERRUPTIBLE_MOTION) { handle = handleRef.current; if (handle && handle.isRunning()) { handle.reverse(); return cleanup; } } const atoms = visible ? presenceMotion.enter : presenceMotion.exit; const direction = visible ? 'enter' : 'exit'; // Heads up! // Initial styles are applied when the component is mounted for the first time and "appear" is set to "false" (otherwise animations are triggered) const applyInitialStyles = !optionsRef.current.appear && isFirstMount; const skipAnimationByConfig = optionsRef.current.skipMotions; if (!applyInitialStyles) { handleMotionStart(direction); } 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 cleanup; } handleRef.current = handle; handle.setMotionEndCallbacks(()=>handleMotionFinish(direction), ()=>handleMotionCancel(direction)); if (skipAnimationByConfig) { handle.finish(); } return cleanup; }, // Excluding `isFirstMount` from deps to prevent re-triggering the animation on subsequent renders // eslint-disable-next-line react-hooks/exhaustive-deps [ animateAtoms, childRef, handleRef, isReducedMotion, handleMotionFinish, handleMotionStart, handleMotionCancel, visible ]); React.useEffect(()=>{ // Heads up! // // Dispose the handle when unmounting the component to clean up retained references. Doing it in a separate // effect to ensure that the component is unmounted. if (unmountOnExit && !mounted) { var _handleRef_current; (_handleRef_current = handleRef.current) === null || _handleRef_current === void 0 ? void 0 : _handleRef_current.dispose(); } }, [ handleRef, unmountOnExit, mounted ]); if (mounted) { return child; } return null; }, { // Heads up! // Always normalize it to a function to simplify types [PRESENCE_MOTION_DEFINITION]: typeof value === 'function' ? value : ()=>value }, { // Wrap `enter` in its own motion component as a static method, e.g. In: createMotionComponent(// If we have a motion function, wrap it to forward the runtime params and pick `enter`. // Otherwise, pass the `enter` motion object directly. typeof value === 'function' ? (...args)=>value(...args).enter : value.enter), // Wrap `exit` in its own motion component as a static method, e.g. Out: createMotionComponent(// If we have a motion function, wrap it to forward the runtime params and pick `exit`. // Otherwise, pass the `exit` motion object directly. typeof value === 'function' ? (...args)=>value(...args).exit : value.exit) }); }