All files / src/animation animation-controls.ts

100% Statements 32/32
100% Branches 2/2
100% Functions 13/13
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 9332x   32x 32x           32x       37x           37x         37x   37x   19x 19x                   25x 19x 19x 21x             19x   6x 6x                 6x         6x 10x         16x 18x         17x 17x 3x     17x 16x 16x         37x    
import { invariant } from "hey-listen"
import { VisualElement } from "../render/types"
import { animateVisualElement, stopAnimation } from "../render/utils/animation"
import { setValues } from "../render/utils/setters"
import { AnimationControls, PendingAnimations } from "./types"
 
/**
 * @public
 */
export function animationControls(): AnimationControls {
    /**
     * Track whether the host component has mounted.
     */
    let hasMounted = false
 
    /**
     * Pending animations that are started before a component is mounted.
     * TODO: Remove this as animations should only run in effects
     */
    const pendingAnimations: PendingAnimations[] = []
 
    /**
     * A collection of linked component animation controls.
     */
    const subscribers = new Set<VisualElement>()
 
    const controls: AnimationControls = {
        subscribe(visualElement) {
            subscribers.add(visualElement)
            return () => void subscribers.delete(visualElement)
        },
 
        start(definition, transitionOverride) {
            /**
             * TODO: We only perform this hasMounted check because in Framer we used to
             * encourage the ability to start an animation within the render phase. This
             * isn't behaviour concurrent-safe so when we make Framer concurrent-safe
             * we can ditch this.
             */
            if (hasMounted) {
                const animations: Array<Promise<any>> = []
                subscribers.forEach((visualElement) => {
                    animations.push(
                        animateVisualElement(visualElement, definition, {
                            transitionOverride,
                        })
                    )
                })
 
                return Promise.all(animations)
            } else {
                return new Promise<void>((resolve) => {
                    pendingAnimations.push({
                        animation: [definition, transitionOverride],
                        resolve,
                    })
                })
            }
        },
 
        set(definition) {
            invariant(
                hasMounted,
                "controls.set() should only be called after a component has mounted. Consider calling within a useEffect hook."
            )
 
            return subscribers.forEach((visualElement) => {
                setValues(visualElement, definition)
            })
        },
 
        stop() {
            subscribers.forEach((visualElement) => {
                stopAnimation(visualElement)
            })
        },
 
        mount() {
            hasMounted = true
            pendingAnimations.forEach(({ animation, resolve }) => {
                controls.start(...animation).then(resolve)
            })
 
            return () => {
                hasMounted = false
                controls.stop()
            }
        },
    }
 
    return controls
}