9
9
import { CdkStep , CdkStepper } from '@angular/cdk/stepper' ;
10
10
import {
11
11
AfterContentInit ,
12
+ AfterViewInit ,
12
13
ANIMATION_MODULE_TYPE ,
13
14
ChangeDetectionStrategy ,
14
15
Component ,
@@ -23,6 +24,7 @@ import {
23
24
Output ,
24
25
QueryList ,
25
26
Renderer2 ,
27
+ signal ,
26
28
TemplateRef ,
27
29
ViewChildren ,
28
30
ViewContainerRef ,
@@ -127,6 +129,7 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI
127
129
'[class.mat-stepper-label-position-bottom]' :
128
130
'orientation === "horizontal" && labelPosition == "bottom"' ,
129
131
'[class.mat-stepper-header-position-bottom]' : 'headerPosition === "bottom"' ,
132
+ '[class.mat-stepper-animating]' : '_isAnimating()' ,
130
133
'[style.--mat-stepper-animation-duration]' : '_getAnimationDuration()' ,
131
134
'[attr.aria-orientation]' : 'orientation' ,
132
135
'role' : 'tablist' ,
@@ -136,11 +139,12 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI
136
139
changeDetection : ChangeDetectionStrategy . OnPush ,
137
140
imports : [ NgTemplateOutlet , MatStepHeader ] ,
138
141
} )
139
- export class MatStepper extends CdkStepper implements AfterContentInit , OnDestroy {
142
+ export class MatStepper extends CdkStepper implements AfterViewInit , AfterContentInit , OnDestroy {
140
143
private _ngZone = inject ( NgZone ) ;
141
144
private _renderer = inject ( Renderer2 ) ;
142
145
private _animationsModule = inject ( ANIMATION_MODULE_TYPE , { optional : true } ) ;
143
146
private _cleanupTransition : ( ( ) => void ) | undefined ;
147
+ protected _isAnimating = signal ( false ) ;
144
148
145
149
/** The list of step headers of the steps in the stepper. */
146
150
@ViewChildren ( MatStepHeader ) override _stepHeader : QueryList < MatStepHeader > = undefined ! ;
@@ -223,7 +227,9 @@ export class MatStepper extends CdkStepper implements AfterContentInit, OnDestro
223
227
this . selectedIndexChange . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => {
224
228
const duration = this . _getAnimationDuration ( ) ;
225
229
if ( duration === '0ms' || duration === '0s' ) {
226
- this . animationDone . emit ( ) ;
230
+ this . _onAnimationDone ( ) ;
231
+ } else {
232
+ this . _isAnimating . set ( true ) ;
227
233
}
228
234
} ) ;
229
235
@@ -244,6 +250,23 @@ export class MatStepper extends CdkStepper implements AfterContentInit, OnDestro
244
250
} ) ;
245
251
}
246
252
253
+ override ngAfterViewInit ( ) : void {
254
+ super . ngAfterViewInit ( ) ;
255
+
256
+ // Prior to #30314 the stepper had animation `done` events bound to each animated container.
257
+ // The animations module was firing them on initialization and for each subsequent animation.
258
+ // Since the events were bound in the template, it had the unintended side-effect of triggering
259
+ // change detection as well. It appears that this side-effect ended up being load-bearing,
260
+ // because it was ensuring that the content elements (e.g. `matStepLabel`) that are defined
261
+ // in sub-components actually get picked up in a timely fashion. This subscription simulates
262
+ // the same change detection by using `queueMicrotask` similarly to the animations module.
263
+ if ( typeof queueMicrotask === 'function' ) {
264
+ this . _animatedContainers . changes
265
+ . pipe ( startWith ( null ) , takeUntil ( this . _destroyed ) )
266
+ . subscribe ( ( ) => queueMicrotask ( ( ) => this . _stateChanged ( ) ) ) ;
267
+ }
268
+ }
269
+
247
270
override ngOnDestroy ( ) : void {
248
271
super . ngOnDestroy ( ) ;
249
272
this . _cleanupTransition ?.( ) ;
@@ -292,7 +315,12 @@ export class MatStepper extends CdkStepper implements AfterContentInit, OnDestro
292
315
this . _animatedContainers . find ( ref => ref . nativeElement === target ) ;
293
316
294
317
if ( shouldEmit ) {
295
- this . animationDone . emit ( ) ;
318
+ this . _onAnimationDone ( ) ;
296
319
}
297
320
} ;
321
+
322
+ private _onAnimationDone ( ) {
323
+ this . _isAnimating . set ( false ) ;
324
+ this . animationDone . emit ( ) ;
325
+ }
298
326
}
0 commit comments