Skip to content

Commit 851ef77

Browse files
thePunderWomanmattrbeck
authored andcommitted
Revert "refactor(core): Ensure determineLongestAnimation is run synchronously after style applies"
This reverts commit 318ade0. (cherry picked from commit 890c973)
1 parent 5800f17 commit 851ef77

2 files changed

Lines changed: 23 additions & 101 deletions

File tree

packages/core/src/render3/instructions/animation.ts

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,6 @@ export function runEnterAnimation(
136136

137137
// We only need to add these event listeners if there are actual classes to apply
138138
if (activeClasses && activeClasses.length > 0) {
139-
let isCleanedUp = false;
140-
cleanupFns.push(() => {
141-
isCleanedUp = true;
142-
});
143-
144139
ngZone.runOutsideAngular(() => {
145140
cleanupFns.push(renderer.listen(nativeElement, 'animationstart', handleEnterAnimationStart));
146141
cleanupFns.push(renderer.listen(nativeElement, 'transitionstart', handleEnterAnimationStart));
@@ -157,16 +152,14 @@ export function runEnterAnimation(
157152
// preventing an animation via selector specificity.
158153
ngZone.runOutsideAngular(() => {
159154
requestAnimationFrame(() => {
160-
setTimeout(() => {
161-
if (hasCompleted || isCleanedUp) return;
162-
determineLongestAnimation(nativeElement, longestAnimations, areAnimationSupported);
163-
if (!longestAnimations.has(nativeElement)) {
164-
for (const klass of activeClasses) {
165-
renderer.removeClass(nativeElement, klass);
166-
}
167-
cleanupEnterClassData(nativeElement);
155+
if (hasCompleted) return;
156+
determineLongestAnimation(nativeElement, longestAnimations, areAnimationSupported);
157+
if (!longestAnimations.has(nativeElement)) {
158+
for (const klass of activeClasses) {
159+
renderer.removeClass(nativeElement, klass);
168160
}
169-
});
161+
cleanupEnterClassData(nativeElement);
162+
}
170163
});
171164
});
172165
}
@@ -333,14 +326,6 @@ function animateLeaveClassRunner(
333326
const componentResolvers = getLViewLeaveAnimations(lView).get(tNode.index)?.resolvers;
334327
let fallbackTimeoutId: number | undefined;
335328
let hasCompleted = false;
336-
let isCleanedUp = false;
337-
338-
cleanupFns.push(() => {
339-
isCleanedUp = true;
340-
if (fallbackTimeoutId !== undefined) {
341-
clearTimeout(fallbackTimeoutId);
342-
}
343-
});
344329

345330
const handleOutAnimationEnd = (event: AnimationEvent | TransitionEvent | CustomEvent) => {
346331
const target = getEventTarget(event as Event);
@@ -393,22 +378,21 @@ function animateLeaveClassRunner(
393378
// preventing an animation via selector specificity.
394379
ngZone.runOutsideAngular(() => {
395380
requestAnimationFrame(() => {
396-
setTimeout(() => {
397-
if (hasCompleted || isCleanedUp) return;
398-
determineLongestAnimation(el, longestAnimations, areAnimationSupported);
399-
const longest = longestAnimations.get(el);
400-
if (!longest) {
401-
clearLeavingNodes(tNode, el);
402-
cleanupAfterLeaveAnimations(componentResolvers, cleanupFns);
403-
clearLViewNodeAnimationResolvers(lView, tNode);
404-
} else {
405-
// Fallback cleanup if the browser drops the transitionend/animationend event
406-
// entirely due to off-screen optimizations or rapid DOM teardown.
407-
fallbackTimeoutId = setTimeout(() => {
408-
handleOutAnimationEnd(new CustomEvent('animation-fallback'));
409-
}, longest.duration + 50) as unknown as number;
410-
}
411-
});
381+
if (hasCompleted) return;
382+
determineLongestAnimation(el, longestAnimations, areAnimationSupported);
383+
const longest = longestAnimations.get(el);
384+
if (!longest) {
385+
clearLeavingNodes(tNode, el);
386+
cleanupAfterLeaveAnimations(componentResolvers, cleanupFns);
387+
clearLViewNodeAnimationResolvers(lView, tNode);
388+
} else {
389+
// Fallback cleanup if the browser drops the transitionend/animationend event
390+
// entirely due to off-screen optimizations or rapid DOM teardown.
391+
fallbackTimeoutId = setTimeout(() => {
392+
handleOutAnimationEnd(new CustomEvent('animation-fallback'));
393+
}, longest.duration + 50) as unknown as number;
394+
cleanupFns.push(() => clearTimeout(fallbackTimeoutId));
395+
}
412396
});
413397
});
414398
}

packages/core/test/acceptance/animation_spec.ts

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
ViewChild,
3232
ViewContainerRef,
3333
} from '@angular/core';
34-
import {fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
34+
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
3535
import {By} from '@angular/platform-browser';
3636
import {isNode} from '@angular/private/testing';
3737
import {tickAnimationFrames} from '../animation_utils/tick_animation_frames';
@@ -171,68 +171,6 @@ describe('Animation', () => {
171171
expect(cmp.el).toBeUndefined();
172172
}));
173173

174-
it('should fire the fallback timer and clean up if the animationend event is not dispatched', fakeAsync(() => {
175-
@Component({
176-
selector: 'test-cmp',
177-
styles: styles,
178-
template: '<div>@if (show()) {<p animate.leave="fade" #el>I should fade</p>}</div>',
179-
encapsulation: ViewEncapsulation.None,
180-
})
181-
class TestComponent {
182-
show = signal(true);
183-
@ViewChild('el', {read: ElementRef}) el!: ElementRef<HTMLParagraphElement>;
184-
}
185-
TestBed.configureTestingModule({animationsEnabled: true});
186-
187-
const fixture = TestBed.createComponent(TestComponent);
188-
const cmp = fixture.componentInstance;
189-
fixture.detectChanges();
190-
191-
expect(fixture.nativeElement.innerHTML).toContain('I should fade');
192-
cmp.show.set(false);
193-
fixture.detectChanges();
194-
195-
tickAnimationFrames(1);
196-
197-
// Now we step the tick so the macro-task setup timeout executes and queues our fallback.
198-
tick(1);
199-
200-
// Wait for duration + 50ms buffer time for fallback
201-
tick(10 + 50);
202-
203-
// Assert that cleanup occurred successfully without needing to artificially dispatch the event
204-
expect(fixture.nativeElement.innerHTML).not.toContain('I should fade');
205-
expect(cmp.el).toBeUndefined();
206-
}));
207-
208-
it('should prevent leaking fallback timeout if view is destroyed early', fakeAsync(() => {
209-
@Component({
210-
selector: 'test-cmp',
211-
styles: styles,
212-
template: '<div>@if (show()) {<p animate.leave="fade" #el>I should fade</p>}</div>',
213-
encapsulation: ViewEncapsulation.None,
214-
})
215-
class TestComponent {
216-
show = signal(true);
217-
@ViewChild('el', {read: ElementRef}) el!: ElementRef<HTMLParagraphElement>;
218-
}
219-
TestBed.configureTestingModule({animationsEnabled: true});
220-
221-
const fixture = TestBed.createComponent(TestComponent);
222-
const cmp = fixture.componentInstance;
223-
fixture.detectChanges();
224-
225-
cmp.show.set(false);
226-
fixture.detectChanges();
227-
228-
// We explicitly destroy the fixture right after triggering the leave,
229-
// imitating the situation where the view goes away before the next macro-task.
230-
fixture.destroy();
231-
232-
// Flush all tasks, expecting no errors with our cleanup flag guarding the timeout.
233-
flush();
234-
}));
235-
236174
it('should support string arrays', fakeAsync(() => {
237175
const multiple = `
238176
.slide-out {

0 commit comments

Comments
 (0)