| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- import { microtask } from 'motion-dom';
- import { useContext, useRef, useInsertionEffect, useEffect } from 'react';
- import { optimizedAppearDataAttribute } from '../../animation/optimized-appear/data-id.mjs';
- import { LazyContext } from '../../context/LazyContext.mjs';
- import { MotionConfigContext } from '../../context/MotionConfigContext.mjs';
- import { MotionContext } from '../../context/MotionContext/index.mjs';
- import { PresenceContext } from '../../context/PresenceContext.mjs';
- import { SwitchLayoutGroupContext } from '../../context/SwitchLayoutGroupContext.mjs';
- import { isRefObject } from '../../utils/is-ref-object.mjs';
- import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
- function useVisualElement(Component, visualState, props, createVisualElement, ProjectionNodeConstructor) {
- const { visualElement: parent } = useContext(MotionContext);
- const lazyContext = useContext(LazyContext);
- const presenceContext = useContext(PresenceContext);
- const reducedMotionConfig = useContext(MotionConfigContext).reducedMotion;
- const visualElementRef = useRef(null);
- /**
- * If we haven't preloaded a renderer, check to see if we have one lazy-loaded
- */
- createVisualElement = createVisualElement || lazyContext.renderer;
- if (!visualElementRef.current && createVisualElement) {
- visualElementRef.current = createVisualElement(Component, {
- visualState,
- parent,
- props,
- presenceContext,
- blockInitialAnimation: presenceContext
- ? presenceContext.initial === false
- : false,
- reducedMotionConfig,
- });
- }
- const visualElement = visualElementRef.current;
- /**
- * Load Motion gesture and animation features. These are rendered as renderless
- * components so each feature can optionally make use of React lifecycle methods.
- */
- const initialLayoutGroupConfig = useContext(SwitchLayoutGroupContext);
- if (visualElement &&
- !visualElement.projection &&
- ProjectionNodeConstructor &&
- (visualElement.type === "html" || visualElement.type === "svg")) {
- createProjectionNode(visualElementRef.current, props, ProjectionNodeConstructor, initialLayoutGroupConfig);
- }
- const isMounted = useRef(false);
- useInsertionEffect(() => {
- /**
- * Check the component has already mounted before calling
- * `update` unnecessarily. This ensures we skip the initial update.
- */
- if (visualElement && isMounted.current) {
- visualElement.update(props, presenceContext);
- }
- });
- /**
- * Cache this value as we want to know whether HandoffAppearAnimations
- * was present on initial render - it will be deleted after this.
- */
- const optimisedAppearId = props[optimizedAppearDataAttribute];
- const wantsHandoff = useRef(Boolean(optimisedAppearId) &&
- !window.MotionHandoffIsComplete?.(optimisedAppearId) &&
- window.MotionHasOptimisedAnimation?.(optimisedAppearId));
- useIsomorphicLayoutEffect(() => {
- if (!visualElement)
- return;
- isMounted.current = true;
- window.MotionIsMounted = true;
- visualElement.updateFeatures();
- microtask.render(visualElement.render);
- /**
- * Ideally this function would always run in a useEffect.
- *
- * However, if we have optimised appear animations to handoff from,
- * it needs to happen synchronously to ensure there's no flash of
- * incorrect styles in the event of a hydration error.
- *
- * So if we detect a situtation where optimised appear animations
- * are running, we use useLayoutEffect to trigger animations.
- */
- if (wantsHandoff.current && visualElement.animationState) {
- visualElement.animationState.animateChanges();
- }
- });
- useEffect(() => {
- if (!visualElement)
- return;
- if (!wantsHandoff.current && visualElement.animationState) {
- visualElement.animationState.animateChanges();
- }
- if (wantsHandoff.current) {
- // This ensures all future calls to animateChanges() in this component will run in useEffect
- queueMicrotask(() => {
- window.MotionHandoffMarkAsComplete?.(optimisedAppearId);
- });
- wantsHandoff.current = false;
- }
- });
- return visualElement;
- }
- function createProjectionNode(visualElement, props, ProjectionNodeConstructor, initialPromotionConfig) {
- const { layoutId, layout, drag, dragConstraints, layoutScroll, layoutRoot, layoutCrossfade, } = props;
- visualElement.projection = new ProjectionNodeConstructor(visualElement.latestValues, props["data-framer-portal-id"]
- ? undefined
- : getClosestProjectingNode(visualElement.parent));
- visualElement.projection.setOptions({
- layoutId,
- layout,
- alwaysMeasureLayout: Boolean(drag) || (dragConstraints && isRefObject(dragConstraints)),
- visualElement,
- /**
- * TODO: Update options in an effect. This could be tricky as it'll be too late
- * to update by the time layout animations run.
- * We also need to fix this safeToRemove by linking it up to the one returned by usePresence,
- * ensuring it gets called if there's no potential layout animations.
- *
- */
- animationType: typeof layout === "string" ? layout : "both",
- initialPromotionConfig,
- crossfade: layoutCrossfade,
- layoutScroll,
- layoutRoot,
- });
- }
- function getClosestProjectingNode(visualElement) {
- if (!visualElement)
- return undefined;
- return visualElement.options.allowProjection !== false
- ? visualElement.projection
- : getClosestProjectingNode(visualElement.parent);
- }
- export { useVisualElement };
|