Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add queue for animation when visualEl is deferred #2293

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
76 changes: 74 additions & 2 deletions packages/framer-motion/src/animation/hooks/animation-controls.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import { invariant } from "../../utils/errors"
import { setValues } from "../../render/utils/setters"
import type { VisualElement } from "../../render/VisualElement"
import { AnimationControls } from "../types"
import type {
AnimationControls,
AnimationDefinition,
Transition,
} from "../types"
import { animateVisualElement } from "../interfaces/visual-element"

/**
* Represents an individual animation start call.
* Each item encapsulates:
* - `definition`: The details of the desired animation.
* - `transitionOverride` (optional): Overrides for default transition settings.
* - `resolve`: Promise resolution function, invoked when the animation completes successfully.
* - `reject`: Promise rejection function, invoked upon animation error or failure.
*/
type StartQueueItem = {
definition: AnimationDefinition
transitionOverride?: Transition
resolve: (value?: any) => void
reject: (reason?: any) => void
}

function stopAnimation(visualElement: VisualElement) {
visualElement.values.forEach((value) => value.stop())
}
Expand All @@ -22,9 +41,19 @@ export function animationControls(): AnimationControls {
*/
const subscribers = new Set<VisualElement>()

/**
* `startQueue` is an array that temporarily holds animation start calls, ensuring they are
* deferred until their respective visual elements have subscribed to the animation controller.
*/
const startQueue: Array<StartQueueItem> = []

const controls: AnimationControls = {
subscribe(visualElement) {
subscribers.add(visualElement)

// Upon a new subscription, handle any queued animations.
flushStartQueue()

return () => void subscribers.delete(visualElement)
},

Expand All @@ -34,7 +63,27 @@ export function animationControls(): AnimationControls {
"controls.start() should only be called after a component has mounted. Consider calling within a useEffect hook."
)

const animations: Array<Promise<any>> = []
if (subscribers.size === 0) {
/*
* Return a new promise to keep track of the animation state.
* The promise will be resolved or rejected when the visual element eventually subscribes
* and the queued animations are processed.
*/
return new Promise((resolve, reject) => {
/*
* If there are no subscribers at the moment, add the animation details to the startQueue.
* This ensures that when a visual element eventually subscribes, the queued animations can be processed and played.
*/
startQueue.push({
definition,
transitionOverride,
resolve,
reject,
})
})
}

const animations: Array<Promise<void>> = []
subscribers.forEach((visualElement) => {
animations.push(
animateVisualElement(visualElement, definition, {
Expand Down Expand Up @@ -73,5 +122,28 @@ export function animationControls(): AnimationControls {
},
}

// Helper function to process the startQueue
async function flushStartQueue() {
// Copy startQueue to prevent mutation during iteration, ensuring consistent processing.
const currentQueue = [...startQueue]

// Clear the startQueue by setting its length to 0, efficiently removing all its items.
startQueue.length = 0

for (const item of currentQueue) {
const { definition, transitionOverride, resolve, reject } = item

try {
const result = controls.start(definition, transitionOverride)
if (result instanceof Promise) {
await result // This ensures that any rejection from the promise leads to the catch block.
}
resolve() // Signal successful completion
} catch (error) {
reject(error) // Signal that an error occurred
}
}
}

return controls
}
2 changes: 1 addition & 1 deletion packages/framer-motion/src/animation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export interface AnimationControls {
start(
definition: AnimationDefinition,
transitionOverride?: Transition
): Promise<any>
): Promise<void | Array<void>>

/**
* Instantly set to a set of properties or a variant.
Expand Down