Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/animation/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type RunEnterAnimationFn = VoidFunction;
export type RunLeaveAnimationFn = () => {promise: Promise<void>; resolve: VoidFunction};

export interface LongestAnimation {
animation?: Animation;
animationName: string | undefined;
propertyName: string | undefined;
duration: number;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/animation/longest_animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ function determineLongestAnimationFromElementAnimations(
}

if (duration >= longest.duration) {
longest = {animationName, propertyName, duration};
longest = {animation, animationName, propertyName, duration};
}
}
if (isShorterThanExistingAnimation(animationsMap.get(el), longest)) return;
Expand Down
34 changes: 26 additions & 8 deletions packages/core/src/animation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,32 @@ export function isLongestAnimation(
// If we don't have any record of a longest animation, then we shouldn't
// block the animationend/transitionend event from doing its work.
if (longestAnimation === undefined) return true;
return (
nativeElement === getEventTarget(event) &&
((longestAnimation.animationName !== undefined &&
(event as AnimationEvent).animationName === longestAnimation.animationName) ||
(longestAnimation.propertyName !== undefined &&
(longestAnimation.propertyName === 'all' ||
(event as TransitionEvent).propertyName === longestAnimation.propertyName)))
);

if (nativeElement !== getEventTarget(event)) return false;

// Distinct CSS animations can share a name. Chrome 151 stable exposes their exact instance:
// https://developer.chrome.com/release-notes/151#animation_accessor_on_animation_and_transition_events
const eventAnimation = (
event as (AnimationEvent | TransitionEvent) & {readonly animation?: Animation | null}
).animation;

if (eventAnimation && longestAnimation.animation) {
return eventAnimation === longestAnimation.animation;
}

// Fall back to strings for older browsers and animations determined from computed styles.
if (longestAnimation.animationName !== undefined) {
return (event as AnimationEvent).animationName === longestAnimation.animationName;
}

if (longestAnimation.propertyName !== undefined) {
return (
longestAnimation.propertyName === 'all' ||
(event as TransitionEvent).propertyName === longestAnimation.propertyName
);
}

return false;
}

/**
Expand Down
64 changes: 62 additions & 2 deletions packages/core/test/acceptance/animation_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,67 @@ describe('Animation', () => {
expect(fixture.debugElement.query(By.css('div'))).toBeNull();
}));

it('should wait for the exact longest animation when animation names are duplicated', fakeAsync(() => {
const multiple = `
.duplicate-animation-name {
animation:
duplicate-name 10s linear,
duplicate-name 20s linear;
}
@keyframes duplicate-name {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
`;
@Component({
changeDetection: ChangeDetectionStrategy.Eager,
selector: 'test-cmp',
styles: multiple,
template:
'@if (show()) { <p animate.leave="duplicate-animation-name">Element with text</p> }',
encapsulation: ViewEncapsulation.None,
})
class TestComponent {
show = signal(true);
}

TestBed.configureTestingModule({animationsEnabled: true});

const fixture = TestBed.createComponent(TestComponent);
const cmp = fixture.componentInstance;
fixture.detectChanges();
const paragraph = fixture.debugElement.query(By.css('p'));

expect(paragraph.nativeElement.className).not.toContain('duplicate-animation-name');
cmp.show.set(false);
fixture.detectChanges();
tickAnimationFrames(1);
expect(cmp.show()).toBeFalse();
fixture.detectChanges();
expect(paragraph.nativeElement.className).toContain('duplicate-animation-name');

const [shortAnimation, longAnimation] = paragraph.nativeElement.getAnimations();
const dispatchAnimationEnd = (animation: Animation) => {
const event = new AnimationEvent('animationend', {animationName: 'duplicate-name'});
Object.defineProperty(event, 'animation', {value: animation});
paragraph.nativeElement.dispatchEvent(event);
};

dispatchAnimationEnd(shortAnimation);
// Flush leave completion so this fails if the shorter event removes the element.
tick();
expect(fixture.nativeElement.outerHTML).toContain('duplicate-animation-name');

dispatchAnimationEnd(longAnimation);
tick();
expect(fixture.nativeElement.outerHTML).not.toContain('duplicate-animation-name');
expect(fixture.debugElement.query(By.css('p'))).toBeNull();
}));

describe('legacy animations compatibility', () => {
beforeAll(() => {
TestBed.resetTestEnvironment();
Expand Down Expand Up @@ -2678,8 +2739,7 @@ describe('Animation', () => {
const panels = () => Array.from(fixture.nativeElement.querySelectorAll('.panel'));
const panelByText = (text: string) =>
panels().find((el) => (el as HTMLElement).textContent?.includes(text)) as
| HTMLElement
| undefined;
HTMLElement | undefined;

expect(panels().length).toBe(1);
expect(panelByText('Panel A')).toBeTruthy();
Expand Down
24 changes: 21 additions & 3 deletions packages/core/test/animation/longest_animation_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ describe('determineLongestAnimation', () => {

determineLongestAnimation(el, animationsMap, true);
const longest = animationsMap.get(el);
expect(longest).toEqual({animationName: 'anim-2', propertyName: undefined, duration: 1000});
expect(longest).toEqual({
animation: el.getAnimations()[1],
animationName: 'anim-2',
propertyName: undefined,
duration: 1000,
});
});

it('should correctly identify CSSTransitions vs CSSAnimations', () => {
Expand All @@ -77,7 +82,12 @@ describe('determineLongestAnimation', () => {

determineLongestAnimation(el, animationsMap, true);
const longest = animationsMap.get(el);
expect(longest).toEqual({animationName: undefined, propertyName: 'opacity', duration: 800});
expect(longest).toEqual({
animation: el.getAnimations()[0],
animationName: undefined,
propertyName: 'opacity',
duration: 800,
});
});

it('should handle "auto" or undefined duration gracefully', () => {
Expand All @@ -97,6 +107,7 @@ describe('determineLongestAnimation', () => {
determineLongestAnimation(el, animationsMap, true);
const longest = animationsMap.get(el);
expect(longest).toEqual({
animation: el.getAnimations()[0],
animationName: 'bad-duration',
propertyName: undefined,
duration: 200,
Expand Down Expand Up @@ -127,6 +138,7 @@ describe('determineLongestAnimation', () => {
determineLongestAnimation(el, animationsMap, true);
const longest = animationsMap.get(el);
expect(longest).toEqual({
animation: el.getAnimations()[1],
animationName: 'finite-anim',
propertyName: undefined,
duration: 500,
Expand Down Expand Up @@ -197,7 +209,12 @@ describe('determineLongestAnimation', () => {
determineLongestAnimation(el, animationsMap, true);

const longest = animationsMap.get(el);
expect(longest).toEqual({animationName: 'mock-anim', propertyName: undefined, duration: 500});
expect(longest).toEqual({
animation: el.getAnimations()[0],
animationName: 'mock-anim',
propertyName: undefined,
duration: 500,
});
});

it('should handle negative playback rates by taking the absolute value', () => {
Expand All @@ -218,6 +235,7 @@ describe('determineLongestAnimation', () => {

const longest = animationsMap.get(el);
expect(longest).toEqual({
animation: el.getAnimations()[0],
animationName: 'mock-anim',
propertyName: undefined,
duration: 1200,
Expand Down