mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): prevents unintended early termination of leave animations and hoisting (#64088)
The event listeners for animationstart and animationend weren't properly checking whether the animation event fired matched the node we're bound to, since animation events bubble. This resulted in child node animation events bubbling up and causing elements to get prematurely removed. fixes: #64084 PR Close #64088
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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: `
|
||||
<div>
|
||||
@if (show()) {
|
||||
<p animate.leave="fade" #el>
|
||||
I should slide in.
|
||||
<button [class]="buttonClass()" (click)="flash()">click me</button>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user