| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- "use client";
- import { jsx, Fragment } from 'react/jsx-runtime';
- import { useMemo, useRef, useState, 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 }) => (
- * <AnimatePresence>
- * {items.map(item => (
- * <motion.div
- * key={item.id}
- * initial={{ opacity: 0 }}
- * animate={{ opacity: 1 }}
- * exit={{ opacity: 0 }}
- * />
- * ))}
- * </AnimatePresence>
- * )
- * ```
- *
- * 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", }) => {
- 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);
- /**
- * A ref containing the currently present children. When all exit animations
- * are complete, we use this to re-render the component with the latest children
- * *committed* rather than the latest children *rendered*.
- */
- const pendingPresentChildren = useRef(presentChildren);
- /**
- * Track which exiting children have finished animating out.
- */
- const exitComplete = useConstant(() => new Map());
- /**
- * 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(() => {
- isInitialRender.current = false;
- pendingPresentChildren.current = presentChildren;
- /**
- * 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);
- }
- }
- }, [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.
- */
- for (let i = 0; i < renderedChildren.length; i++) {
- const child = renderedChildren[i];
- const key = getChildKey(child);
- if (!presentKeys.includes(key)) {
- nextChildren.splice(i, 0, child);
- exitingChildren.push(child);
- }
- }
- /**
- * If we're in "wait" mode, and we have exiting children, we want to
- * only render these until they've all exited.
- */
- if (mode === "wait" && exitingChildren.length) {
- 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 (exitComplete.has(key)) {
- exitComplete.set(key, true);
- }
- else {
- return;
- }
- let isEveryExitComplete = true;
- exitComplete.forEach((isExitComplete) => {
- if (!isExitComplete)
- isEveryExitComplete = false;
- });
- if (isEveryExitComplete) {
- forceRender?.();
- setRenderedChildren(pendingPresentChildren.current);
- propagate && safeToRemove?.();
- onExitComplete && onExitComplete();
- }
- };
- return (jsx(PresenceChild, { isPresent: isPresent, initial: !isInitialRender.current || initial
- ? undefined
- : false, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode, onExitComplete: isPresent ? undefined : onExit, anchorX: anchorX, children: child }, key));
- }) }));
- };
- export { AnimatePresence };
|