Skip to content

Commit f9d73cc

Browse files
fix(core): handle cases where classes added have no animations (#63242)
In the case that someone wants to disable animations via selector specificity, for example by adding an `.animate-disabled` class to a parent node, we need to make sure the animate instructions don't misbehave. Now we detect if animations exist in the provided classes and react accordingly. fixes: #63161 PR Close #63242
1 parent 5ceb116 commit f9d73cc

3 files changed

Lines changed: 192 additions & 87 deletions

File tree

packages/core/src/animation/longest_animation.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ function getLongestComputedAnimation(computedStyle: CSSStyleDeclaration): Longes
5656
return longest;
5757
}
5858

59+
function isShorterThanExistingAnimation(
60+
existing: LongestAnimation | undefined,
61+
longest: LongestAnimation,
62+
): boolean {
63+
return existing !== undefined && existing.duration > longest.duration;
64+
}
65+
66+
function longestExists(longest: LongestAnimation): boolean {
67+
return (
68+
(longest.animationName != undefined || longest.propertyName != undefined) &&
69+
longest.duration > 0
70+
);
71+
}
72+
5973
/**
6074
* Determines the longest animation, but with `getComputedStyles` instead of `getAnimations`. This
6175
* is ultimately safer than getAnimations because it can be used when recalculations are in
@@ -72,10 +86,10 @@ function determineLongestAnimationFromComputedStyles(
7286

7387
const longest =
7488
longestAnimation.duration > longestTransition.duration ? longestAnimation : longestTransition;
75-
if (animationsMap.has(el) && animationsMap.get(el)!.duration > longest.duration) {
76-
return;
89+
if (isShorterThanExistingAnimation(animationsMap.get(el), longest)) return;
90+
if (longestExists(longest)) {
91+
animationsMap.set(el, longest);
7792
}
78-
animationsMap.set(el, longest);
7993
}
8094

8195
/**
@@ -86,12 +100,11 @@ function determineLongestAnimationFromComputedStyles(
86100
* that animation completes.
87101
*/
88102
export function determineLongestAnimation(
89-
event: AnimationEvent | TransitionEvent,
90103
el: HTMLElement,
91104
animationsMap: WeakMap<HTMLElement, LongestAnimation>,
92105
areAnimationSupported: boolean,
93106
): void {
94-
if (!areAnimationSupported || !(event.target instanceof Element) || event.target !== el) return;
107+
if (!areAnimationSupported) return;
95108
const animations = el.getAnimations();
96109
return animations.length === 0
97110
? // fallback to computed styles if getAnimations is empty. This would happen if styles are
@@ -105,7 +118,7 @@ function determineLongestAnimationFromElementAnimations(
105118
animationsMap: WeakMap<HTMLElement, LongestAnimation>,
106119
animations: Animation[],
107120
): void {
108-
let currentLongest: LongestAnimation = {
121+
let longest: LongestAnimation = {
109122
animationName: undefined,
110123
propertyName: undefined,
111124
duration: 0,
@@ -126,12 +139,12 @@ function determineLongestAnimationFromElementAnimations(
126139
propertyName = (animation as CSSTransition).transitionProperty;
127140
}
128141

129-
if (duration >= currentLongest.duration) {
130-
currentLongest = {animationName, propertyName, duration};
142+
if (duration >= longest.duration) {
143+
longest = {animationName, propertyName, duration};
131144
}
132145
}
133-
if (animationsMap.has(el) && animationsMap.get(el)!.duration > currentLongest.duration) {
134-
return;
146+
if (isShorterThanExistingAnimation(animationsMap.get(el), longest)) return;
147+
if (longestExists(longest)) {
148+
animationsMap.set(el, longest);
135149
}
136-
animationsMap.set(el, currentLongest);
137150
}

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

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
141141
// This also allows us to setup cancellation of animations in progress if the
142142
// gets removed early.
143143
const handleAnimationStart = (event: AnimationEvent | TransitionEvent) => {
144-
determineLongestAnimation(event, nativeElement, longestAnimations, areAnimationSupported);
145144
setupAnimationCancel(event, renderer);
146145
const eventName = event instanceof AnimationEvent ? 'animationend' : 'transitionend';
147146
ngZone.runOutsideAngular(() => {
@@ -175,6 +174,20 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
175174
for (const klass of activeClasses) {
176175
renderer.addClass(nativeElement, klass);
177176
}
177+
// In the case that the classes added have no animations, we need to remove
178+
// the classes right away. This could happen because someone is intentionally
179+
// preventing an animation via selector specificity.
180+
ngZone.runOutsideAngular(() => {
181+
requestAnimationFrame(() => {
182+
determineLongestAnimation(nativeElement, longestAnimations, areAnimationSupported);
183+
if (!longestAnimations.has(nativeElement)) {
184+
for (const klass of activeClasses) {
185+
renderer.removeClass(nativeElement, klass);
186+
}
187+
cleanupEnterClassData(nativeElement);
188+
}
189+
});
190+
});
178191
}
179192

180193
return ɵɵanimateEnter; // For chaining
@@ -504,14 +517,11 @@ function animateLeaveClassRunner(
504517
if (animationsDisabled) {
505518
longestAnimations.delete(el);
506519
finalRemoveFn();
520+
return;
507521
}
508522

509523
cancelAnimationsIfRunning(el, renderer);
510524

511-
const handleAnimationStart = (event: AnimationEvent | TransitionEvent) => {
512-
determineLongestAnimation(event, el, longestAnimations, areAnimationSupported);
513-
};
514-
515525
const handleOutAnimationEnd = (event: AnimationEvent | TransitionEvent | CustomEvent) => {
516526
if (event instanceof CustomEvent || isLongestAnimation(event, el)) {
517527
// Now that we've found the longest animation, there's no need
@@ -525,16 +535,24 @@ function animateLeaveClassRunner(
525535
}
526536
};
527537

528-
if (!animationsDisabled) {
529-
ngZone.runOutsideAngular(() => {
530-
renderer.listen(el, 'animationstart', handleAnimationStart, {once: true});
531-
renderer.listen(el, 'transitionstart', handleAnimationStart, {once: true});
532-
renderer.listen(el, 'animationend', handleOutAnimationEnd);
533-
renderer.listen(el, 'transitionend', handleOutAnimationEnd);
534-
});
535-
trackLeavingNodes(tNode, el);
536-
for (const item of classList) {
537-
renderer.addClass(el, item);
538-
}
538+
ngZone.runOutsideAngular(() => {
539+
renderer.listen(el, 'animationend', handleOutAnimationEnd);
540+
renderer.listen(el, 'transitionend', handleOutAnimationEnd);
541+
});
542+
trackLeavingNodes(tNode, el);
543+
for (const item of classList) {
544+
renderer.addClass(el, item);
539545
}
546+
// In the case that the classes added have no animations, we need to remove
547+
// the element right away. This could happen because someone is intentionally
548+
// preventing an animation via selector specificity.
549+
ngZone.runOutsideAngular(() => {
550+
requestAnimationFrame(() => {
551+
determineLongestAnimation(el, longestAnimations, areAnimationSupported);
552+
if (!longestAnimations.has(el)) {
553+
clearLeavingNodes(tNode);
554+
finalRemoveFn();
555+
}
556+
});
557+
});
540558
}

0 commit comments

Comments
 (0)