"use client"; import { jsx, Fragment } from 'react/jsx-runtime'; import { useMemo, useRef, useState, useEffect, useContext } from 'react'; import { LayoutGroupContext } from '../../context/LayoutGroupContext.mjs'; import { useConstant } from '../../utils/use-constant.mjs'; import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs'; import { PresenceChild } from './PresenceChild.mjs'; import { usePresence } from './use-presence.mjs'; import { onlyElements, getChildKey } from './utils.mjs'; /** * `AnimatePresence` enables the animation of components that have been removed from the tree. * * When adding/removing more than a single child, every child **must** be given a unique `key` prop. * * Any `motion` components that have an `exit` property defined will animate out when removed from * the tree. * * ```jsx * import { motion, AnimatePresence } from 'framer-motion' * * export const Items = ({ items }) => ( * * {items.map(item => ( * * ))} * * ) * ``` * * You can sequence exit animations throughout a tree using variants. * * If a child contains multiple `motion` components with `exit` props, it will only unmount the child * once all `motion` components have finished animating out. Likewise, any components using * `usePresence` all need to call `safeToRemove`. * * @public */ const AnimatePresence = ({ children, custom, initial = true, onExitComplete, presenceAffectsLayout = true, mode = "sync", propagate = false, anchorX = "left", anchorY = "top", root }) => { const [isParentPresent, safeToRemove] = usePresence(propagate); /** * Filter any children that aren't ReactElements. We can only track components * between renders with a props.key. */ const presentChildren = useMemo(() => onlyElements(children), [children]); /** * Track the keys of the currently rendered children. This is used to * determine which children are exiting. */ const presentKeys = propagate && !isParentPresent ? [] : presentChildren.map(getChildKey); /** * If `initial={false}` we only want to pass this to components in the first render. */ const isInitialRender = useRef(true); /** * Track which exiting children have finished animating out. */ const exitComplete = useConstant(() => new Map()); /** * Track which components are currently processing exit to prevent duplicate processing. */ const exitingComponents = useRef(new Set()); /** * Save children to render as React state. To ensure this component is concurrent-safe, * we check for exiting children via an effect. */ const [diffedChildren, setDiffedChildren] = useState(presentChildren); const [renderedChildren, setRenderedChildren] = useState(presentChildren); useIsomorphicLayoutEffect(() => { if (propagate && !isParentPresent && !renderedChildren.length) { safeToRemove?.(); } }, [isParentPresent, propagate, renderedChildren.length, safeToRemove]); /** * When every exit completes, we re-render to remove the exited children * and store onExitComplete here. It fires from the cleanup of an effect * keyed on the rendered children, so it runs in the commit that removes * them (or when we unmount), after their own effect cleanups. */ const pendingExitComplete = useRef(); useEffect(() => () => { const callback = pendingExitComplete.current; pendingExitComplete.current = undefined; callback?.(); }, [renderedChildren]); useIsomorphicLayoutEffect(() => { isInitialRender.current = false; /** * Update complete status of exiting children. */ for (let i = 0; i < renderedChildren.length; i++) { const key = getChildKey(renderedChildren[i]); if (!presentKeys.includes(key)) { if (exitComplete.get(key) !== true) { exitComplete.set(key, false); } } else { exitComplete.delete(key); exitingComponents.current.delete(key); } } }, [renderedChildren, presentKeys.length, presentKeys.join("-")]); const exitingChildren = []; if (presentChildren !== diffedChildren) { let nextChildren = [...presentChildren]; /** * Loop through all the currently rendered components and decide which * are exiting. * * Exiting children are reinserted directly after the child they * previously followed. Splicing at their index within the previously * rendered children, as we used to, indexes into the wrong list: it * can interleave them with entering children and push present children * to new positions, remounting them (#3746). */ let insertionIndex = 0; for (const child of renderedChildren) { const presentIndex = presentKeys.indexOf(getChildKey(child)); if (presentIndex === -1) { nextChildren.splice(insertionIndex++, 0, child); exitingChildren.push(child); } else { insertionIndex = presentIndex + exitingChildren.length + 1; } } /** * Once every exiting child has finished animating out, remove them * all at once. Otherwise, if we're in "wait" mode, we want to only * render the exiting children until they've all exited. */ if (exitingChildren.every((child) => exitComplete.get(getChildKey(child)))) { nextChildren = presentChildren; } else if (mode === "wait") { nextChildren = exitingChildren; } setRenderedChildren(onlyElements(nextChildren)); setDiffedChildren(presentChildren); /** * Early return to ensure once we've set state with the latest diffed * children, we can immediately re-render. */ return null; } if (process.env.NODE_ENV !== "production" && mode === "wait" && renderedChildren.length > 1) { console.warn(`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`); } /** * If we've been provided a forceRender function by the LayoutGroupContext, * we can use it to force a re-render amongst all surrounding components once * all components have finished animating out. */ const { forceRender } = useContext(LayoutGroupContext); return (jsx(Fragment, { children: renderedChildren.map((child) => { const key = getChildKey(child); const isPresent = propagate && !isParentPresent ? false : presentChildren === renderedChildren || presentKeys.includes(key); const onExit = () => { if (exitingComponents.current.has(key)) { return; } if (exitComplete.has(key)) { exitingComponents.current.add(key); exitComplete.set(key, true); } else { return; } let isEveryExitComplete = true; exitComplete.forEach((isExitComplete) => { if (!isExitComplete) isEveryExitComplete = false; }); if (isEveryExitComplete) { forceRender?.(); /** * Re-diff rather than setting the children to render * directly: this update can be applied after a * transition has committed new children, so any * children read now could be stale by then. */ setDiffedChildren([]); propagate && safeToRemove?.(); pendingExitComplete.current = onExitComplete; } }; return (jsx(PresenceChild, { isPresent: isPresent, initial: !isInitialRender.current || initial ? undefined : false, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode, root: root, onExitComplete: isPresent ? undefined : onExit, anchorX: anchorX, anchorY: anchorY, children: child }, key)); }) })); }; export { AnimatePresence }; //# sourceMappingURL=index.mjs.map