angular/packages/core/src/animation/queue.ts
Jessica Janiuk df659b8d0c feat(core): re-introduce nested leave animations scoped to component boundaries
This commit re-introduces support for nested leave animations with a critical adjustment to prevent cross-component blocking. Wait for nested inner `animate.leave` transitions natively only when they exist within the same component's view or its embedded tracking structures (like `@if` and `@for`).

This resolves the issue where route navigations and parental destruction would excessively stall by traversing down into child component architectures to wait for their distinct leaf animations.

BREAKING CHANGE: Leave animations are no longer limited to the element being removed.

Fixes #67633
2026-03-13 13:01:55 -06:00

119 lines
3.9 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {afterNextRender} from '../render3/after_render/hooks';
import {InjectionToken, EnvironmentInjector, Injector, inject} from '../di';
import {AnimationLViewData, EnterNodeAnimations} from './interfaces';
export interface AnimationQueue {
queue: Set<VoidFunction>;
isScheduled: boolean;
scheduler: typeof initializeAnimationQueueScheduler | null;
injector: EnvironmentInjector;
}
/**
* A [DI token](api/core/InjectionToken) for the queue of all animations.
*/
export const ANIMATION_QUEUE = new InjectionToken<AnimationQueue>(
typeof ngDevMode !== 'undefined' && ngDevMode ? 'AnimationQueue' : '',
{
factory: () => {
const injector = inject(EnvironmentInjector);
const queue = new Set<VoidFunction>();
injector.onDestroy(() => queue.clear());
return {
queue,
isScheduled: false,
scheduler: null,
injector, // should be the root injector
};
},
},
);
export function addToAnimationQueue(
injector: Injector,
animationFns: VoidFunction | VoidFunction[],
animationData?: AnimationLViewData,
) {
const animationQueue = injector.get(ANIMATION_QUEUE);
if (Array.isArray(animationFns)) {
for (const animateFn of animationFns) {
animationQueue.queue.add(animateFn);
// If a node is detached, we need to keep track of the queued animation functions
// so we can later remove them from the global animation queue if the view
// is re-attached before the animation queue runs.
animationData?.detachedLeaveAnimationFns?.push(animateFn);
}
} else {
animationQueue.queue.add(animationFns);
// If a node is detached, we need to keep track of the queued animation functions
// so we can later remove them from the global animation queue if the view
// is re-attached before the animation queue runs.
animationData?.detachedLeaveAnimationFns?.push(animationFns);
}
animationQueue.scheduler && animationQueue.scheduler(injector);
}
export function removeAnimationsFromQueue(
injector: Injector,
animationFns: VoidFunction | VoidFunction[],
) {
const animationQueue = injector.get(ANIMATION_QUEUE);
if (Array.isArray(animationFns)) {
for (const animateFn of animationFns) {
animationQueue.queue.delete(animateFn);
}
} else {
animationQueue.queue.delete(animationFns);
}
}
export function removeFromAnimationQueue(injector: Injector, animationData: AnimationLViewData) {
const animationQueue = injector.get(ANIMATION_QUEUE);
if (animationData.detachedLeaveAnimationFns) {
for (const animationFn of animationData.detachedLeaveAnimationFns) {
animationQueue.queue.delete(animationFn);
}
animationData.detachedLeaveAnimationFns = undefined;
}
}
export function scheduleAnimationQueue(injector: Injector) {
const animationQueue = injector.get(ANIMATION_QUEUE);
// We only want to schedule the animation queue if it hasn't already been scheduled.
if (!animationQueue.isScheduled) {
afterNextRender(
() => {
animationQueue.isScheduled = false;
for (let animateFn of animationQueue.queue) {
animateFn();
}
animationQueue.queue.clear();
},
{injector: animationQueue.injector},
);
animationQueue.isScheduled = true;
}
}
export function initializeAnimationQueueScheduler(injector: Injector) {
const animationQueue = injector.get(ANIMATION_QUEUE);
animationQueue.scheduler = scheduleAnimationQueue;
animationQueue.scheduler(injector);
}
export function queueEnterAnimations(
injector: Injector,
enterAnimations: Map<number, EnterNodeAnimations>,
) {
for (const [_, nodeAnimations] of enterAnimations) {
addToAnimationQueue(injector, nodeAnimations.animateFns);
}
}