diff --git a/packages/core/src/render3/instructions/animation.ts b/packages/core/src/render3/instructions/animation.ts index d94f8ceea03..a409ad08e43 100644 --- a/packages/core/src/render3/instructions/animation.ts +++ b/packages/core/src/render3/instructions/animation.ts @@ -89,6 +89,9 @@ export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Fu // This also allows us to setup cancellation of animations in progress if the // gets removed early. const handleEnterAnimationStart = (event: AnimationEvent | TransitionEvent) => { + // this early exit case is to prevent issues with bubbling events that are from child element animations + if (event.target !== nativeElement) return; + const eventName = event instanceof AnimationEvent ? 'animationend' : 'transitionend'; ngZone.runOutsideAngular(() => { cleanupFns.push(renderer.listen(nativeElement, eventName, handleEnterAnimationEnd)); @@ -97,6 +100,9 @@ export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Fu // When the longest animation ends, we can remove all the classes const handleEnterAnimationEnd = (event: AnimationEvent | TransitionEvent) => { + // this early exit case is to prevent issues with bubbling events that are from child element animations + if (event.target !== nativeElement) return; + enterAnimationEnd(event, nativeElement, renderer); }; @@ -135,7 +141,8 @@ function enterAnimationEnd( renderer: Renderer, ) { const elementData = enterClassMap.get(nativeElement); - if (!elementData) return; + // this event.target check is to prevent issues with bubbling events that are from child element animations + if (event.target !== nativeElement || !elementData) return; if (isLongestAnimation(event, nativeElement)) { // Now that we've found the longest animation, there's no need // to keep bubbling up this event as it's not going to apply to @@ -279,6 +286,8 @@ function animateLeaveClassRunner( cancelAnimationsIfRunning(el, renderer); const handleOutAnimationEnd = (event: AnimationEvent | TransitionEvent | CustomEvent) => { + // this early exit case is to prevent issues with bubbling events that are from child element animations + if (event.target !== el) return; if (event instanceof CustomEvent || isLongestAnimation(event, el)) { // Now that we've found the longest animation, there's no need // to keep bubbling up this event as it's not going to apply to diff --git a/packages/core/test/acceptance/animation_spec.ts b/packages/core/test/acceptance/animation_spec.ts index f87329d0f04..333fe6ee200 100644 --- a/packages/core/test/acceptance/animation_spec.ts +++ b/packages/core/test/acceptance/animation_spec.ts @@ -1673,5 +1673,79 @@ describe('Animation', () => { tick(); expect(fixture.debugElement.queryAll(By.css('p')).length).toBe(3); })); + + it('should not remove elements when child element animations finish', fakeAsync(() => { + const animateStyles = ` + .fade { + animation: fade-out 500ms; + } + .flash { + animation: flash 200ms; + } + @keyframes flash { + from { + background-color: rgba(255, 255, 255, 1); + } + to { + background-color: rgba(255, 255, 255, 0);; + } + } + @keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + `; + + @Component({ + selector: 'test-cmp', + styles: animateStyles, + template: ` +
+ @if (show()) { +

+ I should slide in. + +

+ } +
+ `, + encapsulation: ViewEncapsulation.None, + }) + class TestComponent { + show = signal(true); + buttonClass = signal(''); + flash() { + this.show.update((val) => !val); + this.buttonClass.set('flash'); + } + } + TestBed.configureTestingModule({animationsEnabled: true}); + + const fixture = TestBed.createComponent(TestComponent); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + cmp.flash(); + fixture.detectChanges(); + tickAnimationFrames(1); + + fixture.debugElement + .query(By.css('button')) + .nativeElement.dispatchEvent( + new AnimationEvent('animationend', {animationName: 'flash', bubbles: true}), + ); + tick(); + expect(fixture.debugElement.queryAll(By.css('p')).length).toBe(1); + fixture.debugElement + .query(By.css('p')) + .nativeElement.dispatchEvent( + new AnimationEvent('animationend', {animationName: 'fade-out', bubbles: true}), + ); + tick(); + expect(fixture.debugElement.queryAll(By.css('p')).length).toBe(0); + })); }); });