diff --git a/packages/core/src/render3/instructions/animation.ts b/packages/core/src/render3/instructions/animation.ts index 6e8597df319..54c4ebebec9 100644 --- a/packages/core/src/render3/instructions/animation.ts +++ b/packages/core/src/render3/instructions/animation.ts @@ -136,6 +136,11 @@ export function runEnterAnimation( // We only need to add these event listeners if there are actual classes to apply if (activeClasses && activeClasses.length > 0) { + let isCleanedUp = false; + cleanupFns.push(() => { + isCleanedUp = true; + }); + ngZone.runOutsideAngular(() => { cleanupFns.push(renderer.listen(nativeElement, 'animationstart', handleEnterAnimationStart)); cleanupFns.push(renderer.listen(nativeElement, 'transitionstart', handleEnterAnimationStart)); @@ -152,14 +157,16 @@ export function runEnterAnimation( // preventing an animation via selector specificity. ngZone.runOutsideAngular(() => { requestAnimationFrame(() => { - if (hasCompleted) return; - determineLongestAnimation(nativeElement, longestAnimations, areAnimationSupported); - if (!longestAnimations.has(nativeElement)) { - for (const klass of activeClasses) { - renderer.removeClass(nativeElement, klass); + setTimeout(() => { + if (hasCompleted || isCleanedUp) return; + determineLongestAnimation(nativeElement, longestAnimations, areAnimationSupported); + if (!longestAnimations.has(nativeElement)) { + for (const klass of activeClasses) { + renderer.removeClass(nativeElement, klass); + } + cleanupEnterClassData(nativeElement); } - cleanupEnterClassData(nativeElement); - } + }); }); }); } @@ -326,6 +333,14 @@ function animateLeaveClassRunner( const componentResolvers = getLViewLeaveAnimations(lView).get(tNode.index)?.resolvers; let fallbackTimeoutId: number | undefined; let hasCompleted = false; + let isCleanedUp = false; + + cleanupFns.push(() => { + isCleanedUp = true; + if (fallbackTimeoutId !== undefined) { + clearTimeout(fallbackTimeoutId); + } + }); const handleOutAnimationEnd = (event: AnimationEvent | TransitionEvent | CustomEvent) => { const target = getEventTarget(event as Event); @@ -378,21 +393,22 @@ function animateLeaveClassRunner( // preventing an animation via selector specificity. ngZone.runOutsideAngular(() => { requestAnimationFrame(() => { - if (hasCompleted) return; - determineLongestAnimation(el, longestAnimations, areAnimationSupported); - const longest = longestAnimations.get(el); - if (!longest) { - clearLeavingNodes(tNode, el); - cleanupAfterLeaveAnimations(componentResolvers, cleanupFns); - clearLViewNodeAnimationResolvers(lView, tNode); - } else { - // Fallback cleanup if the browser drops the transitionend/animationend event - // entirely due to off-screen optimizations or rapid DOM teardown. - fallbackTimeoutId = setTimeout(() => { - handleOutAnimationEnd(new CustomEvent('animation-fallback')); - }, longest.duration + 50) as unknown as number; - cleanupFns.push(() => clearTimeout(fallbackTimeoutId)); - } + setTimeout(() => { + if (hasCompleted || isCleanedUp) return; + determineLongestAnimation(el, longestAnimations, areAnimationSupported); + const longest = longestAnimations.get(el); + if (!longest) { + clearLeavingNodes(tNode, el); + cleanupAfterLeaveAnimations(componentResolvers, cleanupFns); + clearLViewNodeAnimationResolvers(lView, tNode); + } else { + // Fallback cleanup if the browser drops the transitionend/animationend event + // entirely due to off-screen optimizations or rapid DOM teardown. + fallbackTimeoutId = setTimeout(() => { + handleOutAnimationEnd(new CustomEvent('animation-fallback')); + }, longest.duration + 50) as unknown as number; + } + }); }); }); } diff --git a/packages/core/test/acceptance/animation_spec.ts b/packages/core/test/acceptance/animation_spec.ts index 7279a98ab80..cb3611738e3 100644 --- a/packages/core/test/acceptance/animation_spec.ts +++ b/packages/core/test/acceptance/animation_spec.ts @@ -31,7 +31,7 @@ import { ViewChild, ViewContainerRef, } from '@angular/core'; -import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {fakeAsync, flush, TestBed, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {isNode} from '@angular/private/testing'; import {tickAnimationFrames} from '../animation_utils/tick_animation_frames'; @@ -171,6 +171,68 @@ describe('Animation', () => { expect(cmp.el).toBeUndefined(); })); + it('should fire the fallback timer and clean up if the animationend event is not dispatched', fakeAsync(() => { + @Component({ + selector: 'test-cmp', + styles: styles, + template: '
I should fade
}I should fade
}