use-visual-element.mjs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import { microtask } from 'motion-dom';
  2. import { useContext, useRef, useInsertionEffect, useEffect } from 'react';
  3. import { optimizedAppearDataAttribute } from '../../animation/optimized-appear/data-id.mjs';
  4. import { LazyContext } from '../../context/LazyContext.mjs';
  5. import { MotionConfigContext } from '../../context/MotionConfigContext.mjs';
  6. import { MotionContext } from '../../context/MotionContext/index.mjs';
  7. import { PresenceContext } from '../../context/PresenceContext.mjs';
  8. import { SwitchLayoutGroupContext } from '../../context/SwitchLayoutGroupContext.mjs';
  9. import { isRefObject } from '../../utils/is-ref-object.mjs';
  10. import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
  11. function useVisualElement(Component, visualState, props, createVisualElement, ProjectionNodeConstructor) {
  12. const { visualElement: parent } = useContext(MotionContext);
  13. const lazyContext = useContext(LazyContext);
  14. const presenceContext = useContext(PresenceContext);
  15. const reducedMotionConfig = useContext(MotionConfigContext).reducedMotion;
  16. const visualElementRef = useRef(null);
  17. /**
  18. * If we haven't preloaded a renderer, check to see if we have one lazy-loaded
  19. */
  20. createVisualElement = createVisualElement || lazyContext.renderer;
  21. if (!visualElementRef.current && createVisualElement) {
  22. visualElementRef.current = createVisualElement(Component, {
  23. visualState,
  24. parent,
  25. props,
  26. presenceContext,
  27. blockInitialAnimation: presenceContext
  28. ? presenceContext.initial === false
  29. : false,
  30. reducedMotionConfig,
  31. });
  32. }
  33. const visualElement = visualElementRef.current;
  34. /**
  35. * Load Motion gesture and animation features. These are rendered as renderless
  36. * components so each feature can optionally make use of React lifecycle methods.
  37. */
  38. const initialLayoutGroupConfig = useContext(SwitchLayoutGroupContext);
  39. if (visualElement &&
  40. !visualElement.projection &&
  41. ProjectionNodeConstructor &&
  42. (visualElement.type === "html" || visualElement.type === "svg")) {
  43. createProjectionNode(visualElementRef.current, props, ProjectionNodeConstructor, initialLayoutGroupConfig);
  44. }
  45. const isMounted = useRef(false);
  46. useInsertionEffect(() => {
  47. /**
  48. * Check the component has already mounted before calling
  49. * `update` unnecessarily. This ensures we skip the initial update.
  50. */
  51. if (visualElement && isMounted.current) {
  52. visualElement.update(props, presenceContext);
  53. }
  54. });
  55. /**
  56. * Cache this value as we want to know whether HandoffAppearAnimations
  57. * was present on initial render - it will be deleted after this.
  58. */
  59. const optimisedAppearId = props[optimizedAppearDataAttribute];
  60. const wantsHandoff = useRef(Boolean(optimisedAppearId) &&
  61. !window.MotionHandoffIsComplete?.(optimisedAppearId) &&
  62. window.MotionHasOptimisedAnimation?.(optimisedAppearId));
  63. useIsomorphicLayoutEffect(() => {
  64. if (!visualElement)
  65. return;
  66. isMounted.current = true;
  67. window.MotionIsMounted = true;
  68. visualElement.updateFeatures();
  69. microtask.render(visualElement.render);
  70. /**
  71. * Ideally this function would always run in a useEffect.
  72. *
  73. * However, if we have optimised appear animations to handoff from,
  74. * it needs to happen synchronously to ensure there's no flash of
  75. * incorrect styles in the event of a hydration error.
  76. *
  77. * So if we detect a situtation where optimised appear animations
  78. * are running, we use useLayoutEffect to trigger animations.
  79. */
  80. if (wantsHandoff.current && visualElement.animationState) {
  81. visualElement.animationState.animateChanges();
  82. }
  83. });
  84. useEffect(() => {
  85. if (!visualElement)
  86. return;
  87. if (!wantsHandoff.current && visualElement.animationState) {
  88. visualElement.animationState.animateChanges();
  89. }
  90. if (wantsHandoff.current) {
  91. // This ensures all future calls to animateChanges() in this component will run in useEffect
  92. queueMicrotask(() => {
  93. window.MotionHandoffMarkAsComplete?.(optimisedAppearId);
  94. });
  95. wantsHandoff.current = false;
  96. }
  97. });
  98. return visualElement;
  99. }
  100. function createProjectionNode(visualElement, props, ProjectionNodeConstructor, initialPromotionConfig) {
  101. const { layoutId, layout, drag, dragConstraints, layoutScroll, layoutRoot, layoutCrossfade, } = props;
  102. visualElement.projection = new ProjectionNodeConstructor(visualElement.latestValues, props["data-framer-portal-id"]
  103. ? undefined
  104. : getClosestProjectingNode(visualElement.parent));
  105. visualElement.projection.setOptions({
  106. layoutId,
  107. layout,
  108. alwaysMeasureLayout: Boolean(drag) || (dragConstraints && isRefObject(dragConstraints)),
  109. visualElement,
  110. /**
  111. * TODO: Update options in an effect. This could be tricky as it'll be too late
  112. * to update by the time layout animations run.
  113. * We also need to fix this safeToRemove by linking it up to the one returned by usePresence,
  114. * ensuring it gets called if there's no potential layout animations.
  115. *
  116. */
  117. animationType: typeof layout === "string" ? layout : "both",
  118. initialPromotionConfig,
  119. crossfade: layoutCrossfade,
  120. layoutScroll,
  121. layoutRoot,
  122. });
  123. }
  124. function getClosestProjectingNode(visualElement) {
  125. if (!visualElement)
  126. return undefined;
  127. return visualElement.options.allowProjection !== false
  128. ? visualElement.projection
  129. : getClosestProjectingNode(visualElement.parent);
  130. }
  131. export { useVisualElement };