mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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
119 lines
3.9 KiB
TypeScript
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);
|
|
}
|
|
}
|