import { animateVisualElement } from '../../animation/interfaces/visual-element.mjs'; import { calcChildStagger } from '../../animation/utils/calc-child-stagger.mjs'; import { isMotionValue } from '../../value/utils/is-motion-value.mjs'; import { isAnimationControls } from './is-animation-controls.mjs'; import { isVariantLabel } from './is-variant-label.mjs'; import { resolveVariant, getTypeCustom } from './resolve-dynamic-variants.mjs'; import { shallowCompare } from './shallow-compare.mjs'; import { variantPriorityOrder } from './variant-props.mjs'; const reversePriorityOrder = [...variantPriorityOrder].reverse(); /** * Diffs the element's animation props (one layer per AnimationType, highest * priority wins each value) against the previous call and starts animations * for what changed. * * Uses `any` type for visualElement to avoid circular dependencies. It reads * props, parent, presenceContext, variantChildren, enteringChildren, * manuallyAnimateOnMount, blockInitialAnimation, getValue(), * getBaseTargetFromProps(), baseTarget and initialValues. */ function createAnimationState(visualElement) { let animate = (animations) => Promise.all(animations.map(({ animation, options }) => animateVisualElement(visualElement, animation, options))); let state = createState(); let isInitialRender = true; /** * After a reset (StrictMode, Suspense, AnimatePresence re-entry) the next * animateChanges() mounts again, but initial={false} no longer applies. */ let wasReset = false; function animateChanges(changedActiveType) { const { props, parent, manuallyAnimateOnMount } = visualElement; const isMounting = isInitialRender || wasReset; const animations = []; /** * Values removed from a type. A lower-priority type that defines * them animates them back, otherwise they fall back to a base target. */ const removedKeys = new Set(); /** * Values claimed by active, higher-priority types. */ let encounteredKeys = {}; /** * Once a type has been deactivated, lower-priority variant labels * re-animate so variant children animate back too. */ let removedVariantIndex = Infinity; /** * Variant labels are inherited from the closest variant-controlling * ancestor, unless an element on the way has inherit={false}. */ let source = visualElement; do source = source.props.inherit !== false && source.parent; while (source && !source.isControllingVariants); reversePriorityOrder.forEach((type, i) => { const typeState = state[type]; const ownProp = props[type]; const inheritedProp = source && source.props[type]; const prop = ownProp !== undefined ? ownProp : isVariantLabel(inheritedProp) || inheritedProp === false ? inheritedProp : undefined; const propIsVariant = isVariantLabel(prop); const activeDelta = type === changedActiveType ? typeState.isActive : null; if (activeDelta === false) removedVariantIndex = i; /** * Inherited labels are animated by the parent through * variantChildren, unless this element mounted after its parent. */ const isInherited = propIsVariant && ownProp === undefined && !(isMounting && manuallyAnimateOnMount); typeState.protectedKeys = { ...encounteredKeys }; if ((!typeState.isActive && activeDelta === null) || (!prop && !typeState.prevProp) || isAnimationControls(prop) || typeof prop === "boolean") { return; } /** * Don't re-resolve a running exit: a changed custom would start * new value animations that stop the originals, leaving the exit * promise unresolved and the element stuck in the DOM. */ if (type === "exit" && typeState.isActive && activeDelta !== true) { encounteredKeys = { ...encounteredKeys, ...typeState.prevResolvedValues, }; return; } const variantDidChange = checkVariantsDidChange(typeState.prevProp, prop); let shouldAnimateType = variantDidChange || (propIsVariant && ((activeDelta && !isInherited) || i > removedVariantIndex)); let handledRemovedValues = false; const definitionList = Array.isArray(prop) ? prop : [prop]; let resolvedValues = {}; if (activeDelta !== false) { for (const definition of definitionList) { const { transition, transitionEnd, ...target } = resolveVariant(visualElement, definition, getTypeCustom(visualElement, type)) || {}; resolvedValues = { ...resolvedValues, ...target, ...transitionEnd, }; } } const { prevResolvedValues } = typeState; for (const key in { ...prevResolvedValues, ...resolvedValues }) { if (key in encounteredKeys) continue; const next = resolvedValues[key]; const prev = prevResolvedValues[key]; /** * Keyframes compare by value, but replay whenever the * variant label changed. */ const valueHasChanged = Array.isArray(next) && Array.isArray(prev) ? !shallowCompare(next, prev) || variantDidChange : next !== prev; if (valueHasChanged && (next === undefined || next === null)) { removedKeys.add(key); } else if (valueHasChanged || (next !== undefined && removedKeys.has(key))) { shouldAnimateType = true; if (removedKeys.delete(key)) handledRemovedValues = true; typeState.needsAnimating[key] = true; const motionValue = visualElement.getValue(key); if (motionValue) motionValue.liveStyle = false; } else { typeState.protectedKeys[key] = true; } } typeState.prevProp = prop; typeState.prevResolvedValues = resolvedValues; if (typeState.isActive) { encounteredKeys = { ...encounteredKeys, ...resolvedValues }; } if (shouldAnimateType && (!(isInherited && variantDidChange) || handledRemovedValues)) { for (const animation of definitionList) { const options = { type }; /** * Elements that mount into an already-mounted parent * animate themselves, staggered by the parent variant's * delayChildren. */ if (typeof animation === "string" && isMounting && manuallyAnimateOnMount && parent?.enteringChildren) { const delayChildren = resolveVariant(parent, animation) ?.transition?.delayChildren; options.delay = calcChildStagger(parent.enteringChildren, visualElement, delayChildren); } animations.push({ animation, options }); } } }); /** * Removed values that no lower-priority type defines animate back to * initial, then style, then the value as first read. If initial no * longer defines a value it once did, it stays where it is. */ if (removedKeys.size) { const { initial } = props; const fallbackAnimation = {}; const resolvedInitial = typeof initial !== "boolean" && resolveVariant(visualElement, Array.isArray(initial) ? initial[0] : initial, visualElement.presenceContext?.custom); if (resolvedInitial && resolvedInitial.transition) { fallbackAnimation.transition = resolvedInitial.transition; } removedKeys.forEach((key) => { const motionValue = visualElement.getValue(key); if (motionValue) motionValue.liveStyle = true; const fromInitial = resolvedInitial && !Array.isArray(initial) ? resolvedInitial[key] : undefined; const fromProps = visualElement.getBaseTargetFromProps(props, key); fallbackAnimation[key] = (fromInitial !== undefined ? fromInitial : fromProps !== undefined && !isMotionValue(fromProps) ? fromProps : visualElement.initialValues[key] === undefined ? visualElement.baseTarget[key] : undefined) ?? null; /** * If it stays where it is, that's now its base. Otherwise a * later removal (like a gesture ending) would also stay, * leaving the value stuck. Stop first so accelerated * animations sync the value they reached. In practice initial * only changes like this with Fast Refresh, so this is * dev-only (#3787). */ if (process.env.NODE_ENV !== "production" && fallbackAnimation[key] === null && visualElement.initialValues[key] !== undefined) { delete visualElement.initialValues[key]; motionValue?.stop(); visualElement.baseTarget[key] = motionValue?.get(); } }); animations.push({ animation: fallbackAnimation }); } const blockAnimation = (isMounting && visualElement.blockInitialAnimation) || (isInitialRender && !manuallyAnimateOnMount && (props.initial === false || props.initial === props.animate)); isInitialRender = wasReset = false; return !blockAnimation && animations.length ? animate(animations) : Promise.resolve(); } /** * Change whether a certain animation type is active. */ function setActive(type, isActive) { if (state[type].isActive === isActive) return Promise.resolve(); visualElement.variantChildren?.forEach((child) => child.animationState?.setActive(type, isActive)); state[type].isActive = isActive; const animations = animateChanges(type); for (const key in state) { state[key].protectedKeys = {}; } return animations; } return { animateChanges, setActive, /** * Allows tests to inject a mocked animate function. * @internal */ setAnimateFunction: (makeAnimator) => { animate = makeAnimator(visualElement); }, getState: () => state, reset: () => { state = createState(); wasReset = true; }, }; } function checkVariantsDidChange(prev, next) { if (typeof next === "string") { return next !== prev; } else if (Array.isArray(next)) { return !shallowCompare(next, prev); } return false; } function createState() { const state = {}; for (const type of variantPriorityOrder) { state[type] = { isActive: type === "animate", protectedKeys: {}, needsAnimating: {}, prevResolvedValues: {}, }; } return state; } export { checkVariantsDidChange, createAnimationState }; //# sourceMappingURL=animation-state.mjs.map