@kcirtaptrick/framer-motion
Version:
A simple and powerful React animation library
164 lines (161 loc) • 7.87 kB
JavaScript
import { __read, __spreadArray } from 'tslib';
import * as React from 'react';
import { useContext, useRef, cloneElement, Children, isValidElement } from 'react';
import process from '../../utils/process.mjs';
import { useForceUpdate } from '../../utils/use-force-update.mjs';
import { useIsMounted } from '../../utils/use-is-mounted.mjs';
import { PresenceChild } from './PresenceChild.mjs';
import { LayoutGroupContext } from '../../context/LayoutGroupContext.mjs';
import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
import { useUnmountEffect } from '../../utils/use-unmount-effect.mjs';
var getChildKey = function (child) { return child.key || ""; };
var isDev = process.env.NODE_ENV !== "production";
function updateChildLookup(children, allChildren) {
var seenChildren = isDev ? new Set() : null;
children.forEach(function (child) {
var key = getChildKey(child);
if (isDev && seenChildren && seenChildren.has(key)) {
console.warn("Children of AnimatePresence require unique keys. \"".concat(key, "\" is a duplicate."));
seenChildren.add(key);
}
allChildren.set(key, child);
});
}
function onlyElements(children) {
var filtered = [];
// We use forEach here instead of map as map mutates the component key by preprending `.$`
Children.forEach(children, function (child) {
if (isValidElement(child))
filtered.push(child);
});
return filtered;
}
/**
* `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
*/
var AnimatePresence = function (_a) {
var children = _a.children, custom = _a.custom, _b = _a.initial, initial = _b === void 0 ? true : _b, onExitComplete = _a.onExitComplete, exitBeforeEnter = _a.exitBeforeEnter, _c = _a.presenceAffectsLayout, presenceAffectsLayout = _c === void 0 ? true : _c;
// We want to force a re-render once all exiting animations have finished. We
// either use a local forceRender function, or one from a parent context if it exists.
var _d = __read(useForceUpdate(), 1), forceRender = _d[0];
var forceRenderLayoutGroup = useContext(LayoutGroupContext).forceRender;
if (forceRenderLayoutGroup)
forceRender = forceRenderLayoutGroup;
var isMounted = useIsMounted();
// Filter out any children that aren't ReactElements. We can only track ReactElements with a props.key
var filteredChildren = onlyElements(children);
var childrenToRender = filteredChildren;
var exiting = new Set();
// Keep a living record of the children we're actually rendering so we
// can diff to figure out which are entering and exiting
var presentChildren = useRef(childrenToRender);
// A lookup table to quickly reference components by key
var allChildren = useRef(new Map()).current;
// If this is the initial component render, just deal with logic surrounding whether
// we play onMount animations or not.
var isInitialRender = useRef(true);
useIsomorphicLayoutEffect(function () {
isInitialRender.current = false;
updateChildLookup(filteredChildren, allChildren);
presentChildren.current = childrenToRender;
});
useUnmountEffect(function () {
isInitialRender.current = true;
allChildren.clear();
exiting.clear();
});
if (isInitialRender.current) {
return (React.createElement(React.Fragment, null, childrenToRender.map(function (child) { return (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, initial: initial ? undefined : false, presenceAffectsLayout: presenceAffectsLayout }, child)); })));
}
// If this is a subsequent render, deal with entering and exiting children
childrenToRender = __spreadArray([], __read(childrenToRender), false);
// Diff the keys of the currently-present and target children to update our
// exiting list.
var presentKeys = presentChildren.current.map(getChildKey);
var targetKeys = filteredChildren.map(getChildKey);
// Diff the present children with our target children and mark those that are exiting
var numPresent = presentKeys.length;
for (var i = 0; i < numPresent; i++) {
var key = presentKeys[i];
if (targetKeys.indexOf(key) === -1) {
exiting.add(key);
}
}
// If we currently have exiting children, and we're deferring rendering incoming children
// until after all current children have exiting, empty the childrenToRender array
if (exitBeforeEnter && exiting.size) {
childrenToRender = [];
}
// Loop through all currently exiting components and clone them to overwrite `animate`
// with any `exit` prop they might have defined.
exiting.forEach(function (key) {
// If this component is actually entering again, early return
if (targetKeys.indexOf(key) !== -1)
return;
var child = allChildren.get(key);
if (!child)
return;
var insertionIndex = presentKeys.indexOf(key);
var onExit = function () {
allChildren.delete(key);
exiting.delete(key);
// Remove this child from the present children
var removeIndex = presentChildren.current.findIndex(function (presentChild) { return presentChild.key === key; });
presentChildren.current.splice(removeIndex, 1);
// Defer re-rendering until all exiting children have indeed left
if (!exiting.size) {
presentChildren.current = filteredChildren;
if (isMounted.current === false)
return;
forceRender();
onExitComplete && onExitComplete();
}
};
childrenToRender.splice(insertionIndex, 0, React.createElement(PresenceChild, { key: getChildKey(child), isPresent: false, onExitComplete: onExit, custom: custom, presenceAffectsLayout: presenceAffectsLayout }, child));
});
// Add `MotionContext` even to children that don't need it to ensure we're rendering
// the same tree between renders
childrenToRender = childrenToRender.map(function (child) {
var key = child.key;
return exiting.has(key) ? (child) : (React.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, presenceAffectsLayout: presenceAffectsLayout }, child));
});
if (process.env.NODE_ENV !== "production" &&
exitBeforeEnter &&
childrenToRender.length > 1) {
console.warn("You're attempting to animate multiple children within AnimatePresence, but its exitBeforeEnter prop is set to true. This will lead to odd visual behaviour.");
}
return (React.createElement(React.Fragment, null, exiting.size
? childrenToRender
: childrenToRender.map(function (child) { return cloneElement(child); })));
};
export { AnimatePresence };