Skip to content

Commit 3d049af

Browse files
authored
refactor(module:alert): use native animation API (#9590)
* refactor(module:alert): new way to create animation * test(module:alert): cover animation leave logic
1 parent d2e8073 commit 3d049af

File tree

2 files changed

+88
-13
lines changed

2 files changed

+88
-13
lines changed

components/alert/alert.component.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,26 @@
55

66
import { Direction, Directionality } from '@angular/cdk/bidi';
77
import {
8+
ANIMATION_MODULE_TYPE,
9+
AnimationCallbackEvent,
10+
booleanAttribute,
811
ChangeDetectionStrategy,
912
ChangeDetectorRef,
1013
Component,
14+
DestroyRef,
1115
EventEmitter,
16+
inject,
1217
Input,
1318
OnChanges,
1419
OnInit,
1520
Output,
1621
SimpleChanges,
1722
TemplateRef,
18-
ViewEncapsulation,
19-
booleanAttribute,
20-
inject,
21-
DestroyRef
23+
ViewEncapsulation
2224
} from '@angular/core';
2325
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2426

25-
import { slideAlertMotion } from 'ng-zorro-antd/core/animation';
27+
import { NzNoAnimationDirective } from 'ng-zorro-antd/core/animation';
2628
import { NzConfigKey, onConfigChangeEventForComponent, WithConfig } from 'ng-zorro-antd/core/config';
2729
import { NzOutletModule } from 'ng-zorro-antd/core/outlet';
2830
import { NzIconModule } from 'ng-zorro-antd/icon';
@@ -33,12 +35,12 @@ export type NzAlertType = 'success' | 'info' | 'warning' | 'error';
3335
@Component({
3436
selector: 'nz-alert',
3537
exportAs: 'nzAlert',
36-
animations: [slideAlertMotion],
37-
imports: [NzIconModule, NzOutletModule],
38+
imports: [NzIconModule, NzOutletModule, NzNoAnimationDirective],
3839
template: `
3940
@if (!closed) {
4041
<div
4142
class="ant-alert"
43+
[nzNoAnimation]="nzNoAnimation"
4244
[class.ant-alert-rtl]="dir === 'rtl'"
4345
[class.ant-alert-success]="nzType === 'success'"
4446
[class.ant-alert-info]="nzType === 'info'"
@@ -48,9 +50,7 @@ export type NzAlertType = 'success' | 'info' | 'warning' | 'error';
4850
[class.ant-alert-banner]="nzBanner"
4951
[class.ant-alert-closable]="nzCloseable"
5052
[class.ant-alert-with-description]="!!nzDescription"
51-
[@.disabled]="nzNoAnimation"
52-
[@slideAlertMotion]
53-
(@slideAlertMotion.done)="onFadeAnimationDone()"
53+
(animate.leave)="onLeaveAnimationDone($event)"
5454
>
5555
@if (nzShowIcon) {
5656
<div class="ant-alert-icon">
@@ -104,6 +104,7 @@ export class NzAlertComponent implements OnChanges, OnInit {
104104
private cdr = inject(ChangeDetectorRef);
105105
private directionality = inject(Directionality);
106106
private readonly destroyRef = inject(DestroyRef);
107+
private readonly animationType = inject(ANIMATION_MODULE_TYPE, { optional: true });
107108
readonly _nzModuleName: NzConfigKey = NZ_CONFIG_MODULE_NAME;
108109

109110
@Input() nzAction: string | TemplateRef<void> | null = null;
@@ -140,12 +141,32 @@ export class NzAlertComponent implements OnChanges, OnInit {
140141

141142
closeAlert(): void {
142143
this.closed = true;
144+
// When animations are disabled, emit immediately since animate.leave won't trigger
145+
if (this.nzNoAnimation || this.animationType === 'NoopAnimations') {
146+
this.nzOnClose.emit(true);
147+
}
143148
}
144149

145-
onFadeAnimationDone(): void {
146-
if (this.closed) {
147-
this.nzOnClose.emit(true);
150+
onLeaveAnimationDone(event: AnimationCallbackEvent): void {
151+
const element = event.target as HTMLElement;
152+
153+
// If animations are disabled, complete immediately (nzOnClose already emitted in closeAlert)
154+
if (this.nzNoAnimation || this.animationType === 'NoopAnimations') {
155+
event.animationComplete();
156+
return;
148157
}
158+
159+
// Apply animation classes
160+
element.classList.add('ant-alert-motion-leave', 'ant-alert-motion-leave-active');
161+
162+
// Listen for transition end to complete the animation
163+
const onTransitionEnd = (): void => {
164+
element.removeEventListener('transitionend', onTransitionEnd);
165+
this.nzOnClose.emit(true);
166+
event.animationComplete();
167+
};
168+
169+
element.addEventListener('transitionend', onTransitionEnd);
149170
}
150171

151172
ngOnChanges(changes: SimpleChanges): void {

components/alert/alert.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Subject } from 'rxjs';
1212

1313
import { NzConfigService } from 'ng-zorro-antd/core/config';
1414
import { updateNonSignalsInput } from 'ng-zorro-antd/core/testing';
15+
import type { NzSafeAny } from 'ng-zorro-antd/core/types';
1516
import { provideNzIconsTesting } from 'ng-zorro-antd/icon/testing';
1617

1718
import { NzAlertComponent, NzAlertType } from './alert.component';
@@ -299,3 +300,56 @@ describe('NzAlertComponent', () => {
299300
expect(cdr.markForCheck).toHaveBeenCalled();
300301
});
301302
});
303+
304+
describe('NzAlertComponent Animation', () => {
305+
let component: NzAlertComponent;
306+
let fixture: ComponentFixture<NzAlertComponent>;
307+
308+
beforeEach(() => {
309+
TestBed.configureTestingModule({
310+
providers: [provideNzIconsTesting()],
311+
animationsEnabled: true
312+
});
313+
314+
fixture = TestBed.createComponent(NzAlertComponent);
315+
component = fixture.componentInstance;
316+
fixture.detectChanges();
317+
});
318+
319+
it('should add animation classes and emit onClose when animation ends', () => {
320+
spyOn(component.nzOnClose, 'emit');
321+
const element = fixture.nativeElement.querySelector('.ant-alert');
322+
const mockEvent = {
323+
target: element,
324+
animationComplete: jasmine.createSpy('animationComplete')
325+
} as NzSafeAny;
326+
327+
component.onLeaveAnimationDone(mockEvent);
328+
329+
expect(element.classList).toContain('ant-alert-motion-leave');
330+
expect(element.classList).toContain('ant-alert-motion-leave-active');
331+
expect(component.nzOnClose.emit).not.toHaveBeenCalled();
332+
expect(mockEvent.animationComplete).not.toHaveBeenCalled();
333+
334+
const transitionEndEvent = new Event('transitionend');
335+
element.dispatchEvent(transitionEndEvent);
336+
337+
expect(component.nzOnClose.emit).toHaveBeenCalledWith(true);
338+
expect(mockEvent.animationComplete).toHaveBeenCalled();
339+
});
340+
341+
it('should handle no animation', () => {
342+
component.nzNoAnimation = true;
343+
spyOn(component.nzOnClose, 'emit');
344+
const element = fixture.nativeElement.querySelector('.ant-alert');
345+
const mockEvent = {
346+
target: element,
347+
animationComplete: jasmine.createSpy('animationComplete')
348+
} as NzSafeAny;
349+
350+
component.onLeaveAnimationDone(mockEvent);
351+
352+
expect(element.classList).not.toContain('ant-alert-motion-leave');
353+
expect(mockEvent.animationComplete).toHaveBeenCalled();
354+
});
355+
});

0 commit comments

Comments
 (0)