index.mjs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. "use client";
  2. import { jsx, Fragment } from 'react/jsx-runtime';
  3. import { useMemo, useRef, useState, useContext } from 'react';
  4. import { LayoutGroupContext } from '../../context/LayoutGroupContext.mjs';
  5. import { useConstant } from '../../utils/use-constant.mjs';
  6. import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
  7. import { PresenceChild } from './PresenceChild.mjs';
  8. import { usePresence } from './use-presence.mjs';
  9. import { onlyElements, getChildKey } from './utils.mjs';
  10. /**
  11. * `AnimatePresence` enables the animation of components that have been removed from the tree.
  12. *
  13. * When adding/removing more than a single child, every child **must** be given a unique `key` prop.
  14. *
  15. * Any `motion` components that have an `exit` property defined will animate out when removed from
  16. * the tree.
  17. *
  18. * ```jsx
  19. * import { motion, AnimatePresence } from 'framer-motion'
  20. *
  21. * export const Items = ({ items }) => (
  22. * <AnimatePresence>
  23. * {items.map(item => (
  24. * <motion.div
  25. * key={item.id}
  26. * initial={{ opacity: 0 }}
  27. * animate={{ opacity: 1 }}
  28. * exit={{ opacity: 0 }}
  29. * />
  30. * ))}
  31. * </AnimatePresence>
  32. * )
  33. * ```
  34. *
  35. * You can sequence exit animations throughout a tree using variants.
  36. *
  37. * If a child contains multiple `motion` components with `exit` props, it will only unmount the child
  38. * once all `motion` components have finished animating out. Likewise, any components using
  39. * `usePresence` all need to call `safeToRemove`.
  40. *
  41. * @public
  42. */
  43. const AnimatePresence = ({ children, custom, initial = true, onExitComplete, presenceAffectsLayout = true, mode = "sync", propagate = false, anchorX = "left", }) => {
  44. const [isParentPresent, safeToRemove] = usePresence(propagate);
  45. /**
  46. * Filter any children that aren't ReactElements. We can only track components
  47. * between renders with a props.key.
  48. */
  49. const presentChildren = useMemo(() => onlyElements(children), [children]);
  50. /**
  51. * Track the keys of the currently rendered children. This is used to
  52. * determine which children are exiting.
  53. */
  54. const presentKeys = propagate && !isParentPresent ? [] : presentChildren.map(getChildKey);
  55. /**
  56. * If `initial={false}` we only want to pass this to components in the first render.
  57. */
  58. const isInitialRender = useRef(true);
  59. /**
  60. * A ref containing the currently present children. When all exit animations
  61. * are complete, we use this to re-render the component with the latest children
  62. * *committed* rather than the latest children *rendered*.
  63. */
  64. const pendingPresentChildren = useRef(presentChildren);
  65. /**
  66. * Track which exiting children have finished animating out.
  67. */
  68. const exitComplete = useConstant(() => new Map());
  69. /**
  70. * Save children to render as React state. To ensure this component is concurrent-safe,
  71. * we check for exiting children via an effect.
  72. */
  73. const [diffedChildren, setDiffedChildren] = useState(presentChildren);
  74. const [renderedChildren, setRenderedChildren] = useState(presentChildren);
  75. useIsomorphicLayoutEffect(() => {
  76. isInitialRender.current = false;
  77. pendingPresentChildren.current = presentChildren;
  78. /**
  79. * Update complete status of exiting children.
  80. */
  81. for (let i = 0; i < renderedChildren.length; i++) {
  82. const key = getChildKey(renderedChildren[i]);
  83. if (!presentKeys.includes(key)) {
  84. if (exitComplete.get(key) !== true) {
  85. exitComplete.set(key, false);
  86. }
  87. }
  88. else {
  89. exitComplete.delete(key);
  90. }
  91. }
  92. }, [renderedChildren, presentKeys.length, presentKeys.join("-")]);
  93. const exitingChildren = [];
  94. if (presentChildren !== diffedChildren) {
  95. let nextChildren = [...presentChildren];
  96. /**
  97. * Loop through all the currently rendered components and decide which
  98. * are exiting.
  99. */
  100. for (let i = 0; i < renderedChildren.length; i++) {
  101. const child = renderedChildren[i];
  102. const key = getChildKey(child);
  103. if (!presentKeys.includes(key)) {
  104. nextChildren.splice(i, 0, child);
  105. exitingChildren.push(child);
  106. }
  107. }
  108. /**
  109. * If we're in "wait" mode, and we have exiting children, we want to
  110. * only render these until they've all exited.
  111. */
  112. if (mode === "wait" && exitingChildren.length) {
  113. nextChildren = exitingChildren;
  114. }
  115. setRenderedChildren(onlyElements(nextChildren));
  116. setDiffedChildren(presentChildren);
  117. /**
  118. * Early return to ensure once we've set state with the latest diffed
  119. * children, we can immediately re-render.
  120. */
  121. return null;
  122. }
  123. if (process.env.NODE_ENV !== "production" &&
  124. mode === "wait" &&
  125. renderedChildren.length > 1) {
  126. 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.`);
  127. }
  128. /**
  129. * If we've been provided a forceRender function by the LayoutGroupContext,
  130. * we can use it to force a re-render amongst all surrounding components once
  131. * all components have finished animating out.
  132. */
  133. const { forceRender } = useContext(LayoutGroupContext);
  134. return (jsx(Fragment, { children: renderedChildren.map((child) => {
  135. const key = getChildKey(child);
  136. const isPresent = propagate && !isParentPresent
  137. ? false
  138. : presentChildren === renderedChildren ||
  139. presentKeys.includes(key);
  140. const onExit = () => {
  141. if (exitComplete.has(key)) {
  142. exitComplete.set(key, true);
  143. }
  144. else {
  145. return;
  146. }
  147. let isEveryExitComplete = true;
  148. exitComplete.forEach((isExitComplete) => {
  149. if (!isExitComplete)
  150. isEveryExitComplete = false;
  151. });
  152. if (isEveryExitComplete) {
  153. forceRender?.();
  154. setRenderedChildren(pendingPresentChildren.current);
  155. propagate && safeToRemove?.();
  156. onExitComplete && onExitComplete();
  157. }
  158. };
  159. return (jsx(PresenceChild, { isPresent: isPresent, initial: !isInitialRender.current || initial
  160. ? undefined
  161. : false, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode, onExitComplete: isPresent ? undefined : onExit, anchorX: anchorX, children: child }, key));
  162. }) }));
  163. };
  164. export { AnimatePresence };