mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): ensure animate events do not have duplicate elements (#63216)
This applies the same fix that fixed the class version to the event binding version. It prevents duplicate elements from being on screen when animations have been toggled too fast. fixes: #63127 PR Close #63216
This commit is contained in:
@@ -88,6 +88,23 @@ const longestAnimations = new WeakMap<HTMLElement, LongestAnimation>();
|
||||
// from an `@if` or `@for`.
|
||||
const leavingNodes = new WeakMap<TNode, HTMLElement[]>();
|
||||
|
||||
function clearLeavingNodes(tNode: TNode): void {
|
||||
if (leavingNodes.get(tNode)?.length === 0) {
|
||||
leavingNodes.delete(tNode);
|
||||
}
|
||||
}
|
||||
|
||||
function trackLeavingNodes(tNode: TNode, el: HTMLElement): void {
|
||||
// We need to track this tNode's element just to be sure we don't add
|
||||
// a new RNode for this TNode while this one is still animating away.
|
||||
// once the animation is complete, we remove this reference.
|
||||
if (leavingNodes.has(tNode)) {
|
||||
leavingNodes.get(tNode)?.push(el);
|
||||
} else {
|
||||
leavingNodes.set(tNode, [el]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruction to handle the `animate.enter` behavior for class bindings.
|
||||
*
|
||||
@@ -208,6 +225,15 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
|
||||
|
||||
const tNode = getCurrentTNode()!;
|
||||
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
|
||||
// In the case that we have an existing node that's animating away, like when
|
||||
// an `@if` toggles quickly or `@for` adds and removes elements quickly, we
|
||||
// need to end the animation for the former node and remove it right away to
|
||||
// prevent duplicate nodes showing up.
|
||||
leavingNodes
|
||||
.get(tNode)
|
||||
?.pop()
|
||||
?.dispatchEvent(new CustomEvent('animationend', {detail: {cancel: true}}));
|
||||
|
||||
value.call(lView[CONTEXT], {target: nativeElement, animationComplete: noOpAnimationComplete});
|
||||
|
||||
return ɵɵanimateEnterListener;
|
||||
@@ -320,7 +346,10 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
|
||||
elementRegistry.elements,
|
||||
'Expected `ElementRegistry` to be present in animations subsystem',
|
||||
);
|
||||
|
||||
const renderer = lView[RENDERER];
|
||||
const animationsDisabled = areAnimationsDisabled(lView);
|
||||
const ngZone = lView[INJECTOR]!.get(NgZone);
|
||||
|
||||
const animate: AnimationEventFunction = (
|
||||
_el: Element,
|
||||
@@ -333,9 +362,15 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
|
||||
const event: AnimationCallbackEvent = {
|
||||
target: nativeElement,
|
||||
animationComplete: () => {
|
||||
clearLeavingNodes(tNode);
|
||||
removeFn();
|
||||
},
|
||||
};
|
||||
trackLeavingNodes(tNode, _el as HTMLElement);
|
||||
|
||||
ngZone.runOutsideAngular(() => {
|
||||
renderer.listen(_el, 'animationend', () => removeFn(), {once: true});
|
||||
});
|
||||
value.call(lView[CONTEXT], event);
|
||||
}
|
||||
};
|
||||
@@ -485,9 +520,7 @@ function animateLeaveClassRunner(
|
||||
// affect any other animations on the page.
|
||||
event.stopImmediatePropagation();
|
||||
longestAnimations.delete(el);
|
||||
if (leavingNodes.get(tNode)?.length === 0) {
|
||||
leavingNodes.delete(tNode);
|
||||
}
|
||||
clearLeavingNodes(tNode);
|
||||
finalRemoveFn();
|
||||
}
|
||||
};
|
||||
@@ -499,14 +532,7 @@ function animateLeaveClassRunner(
|
||||
renderer.listen(el, 'animationend', handleOutAnimationEnd);
|
||||
renderer.listen(el, 'transitionend', handleOutAnimationEnd);
|
||||
});
|
||||
// We need to track this tNode's element just to be sure we don't add
|
||||
// a new RNode for this TNode while this one is still animating away.
|
||||
// once the animation is complete, we remove this reference.
|
||||
if (leavingNodes.has(tNode)) {
|
||||
leavingNodes.get(tNode)?.push(el);
|
||||
} else {
|
||||
leavingNodes.set(tNode, [el]);
|
||||
}
|
||||
trackLeavingNodes(tNode, el);
|
||||
for (const item of classList) {
|
||||
renderer.addClass(el, item);
|
||||
}
|
||||
|
||||
@@ -978,5 +978,70 @@ describe('Animation', () => {
|
||||
const paragraphs = fixture.debugElement.queryAll(By.css('p'));
|
||||
expect(paragraphs.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should reset leave animation and not duplicate node when toggled quickly using event bindings', () => {
|
||||
const animateStyles = `
|
||||
.slide-in {
|
||||
animation: slide-in 500ms;
|
||||
}
|
||||
.fade {
|
||||
animation: fade-out 500ms;
|
||||
}
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
styles: animateStyles,
|
||||
template:
|
||||
'<div>@if (show()) {<p (animate.enter)="slideIn($event)" (animate.leave)="fade($event)" #el>I should slide in</p>}</div>',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
class TestComponent {
|
||||
show = signal(false);
|
||||
@ViewChild('el', {read: ElementRef}) el!: ElementRef<HTMLParagraphElement>;
|
||||
|
||||
slideIn(event: AnimationCallbackEvent) {
|
||||
event.target.classList.add('slide-in');
|
||||
setTimeout(() => event.animationComplete(), 500);
|
||||
}
|
||||
|
||||
fade(event: AnimationCallbackEvent) {
|
||||
event.target.classList.add('fade');
|
||||
setTimeout(() => event.animationComplete(), 500);
|
||||
}
|
||||
}
|
||||
TestBed.configureTestingModule({animationsEnabled: true});
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
const cmp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
cmp.show.set(true);
|
||||
fixture.detectChanges();
|
||||
expect(cmp.show()).toBeTruthy();
|
||||
cmp.show.set(false);
|
||||
fixture.detectChanges();
|
||||
expect(cmp.show()).toBeFalsy();
|
||||
cmp.show.set(true);
|
||||
fixture.detectChanges();
|
||||
expect(cmp.show()).toBeTruthy();
|
||||
const paragraphs = fixture.debugElement.queryAll(By.css('p'));
|
||||
expect(paragraphs.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user