framer-motion
Version:
A simple and powerful JavaScript animation library
206 lines (203 loc) • 8.79 kB
JavaScript
"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 }) => (
* <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", 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