99import { CdkStep , CdkStepper } from '@angular/cdk/stepper' ;
1010import {
1111 AfterContentInit ,
12+ AfterViewInit ,
1213 ANIMATION_MODULE_TYPE ,
1314 ChangeDetectionStrategy ,
1415 Component ,
@@ -23,6 +24,7 @@ import {
2324 Output ,
2425 QueryList ,
2526 Renderer2 ,
27+ signal ,
2628 TemplateRef ,
2729 ViewChildren ,
2830 ViewContainerRef ,
@@ -127,6 +129,7 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI
127129 '[class.mat-stepper-label-position-bottom]' :
128130 'orientation === "horizontal" && labelPosition == "bottom"' ,
129131 '[class.mat-stepper-header-position-bottom]' : 'headerPosition === "bottom"' ,
132+ '[class.mat-stepper-animating]' : '_isAnimating()' ,
130133 '[style.--mat-stepper-animation-duration]' : '_getAnimationDuration()' ,
131134 '[attr.aria-orientation]' : 'orientation' ,
132135 'role' : 'tablist' ,
@@ -136,11 +139,12 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI
136139 changeDetection : ChangeDetectionStrategy . OnPush ,
137140 imports : [ NgTemplateOutlet , MatStepHeader ] ,
138141} )
139- export class MatStepper extends CdkStepper implements AfterContentInit , OnDestroy {
142+ export class MatStepper extends CdkStepper implements AfterViewInit , AfterContentInit , OnDestroy {
140143 private _ngZone = inject ( NgZone ) ;
141144 private _renderer = inject ( Renderer2 ) ;
142145 private _animationsModule = inject ( ANIMATION_MODULE_TYPE , { optional : true } ) ;
143146 private _cleanupTransition : ( ( ) => void ) | undefined ;
147+ protected _isAnimating = signal ( false ) ;
144148
145149 /** The list of step headers of the steps in the stepper. */
146150 @ViewChildren ( MatStepHeader ) override _stepHeader : QueryList < MatStepHeader > = undefined ! ;
@@ -223,7 +227,9 @@ export class MatStepper extends CdkStepper implements AfterContentInit, OnDestro
223227 this . selectedIndexChange . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => {
224228 const duration = this . _getAnimationDuration ( ) ;
225229 if ( duration === '0ms' || duration === '0s' ) {
226- this . animationDone . emit ( ) ;
230+ this . _onAnimationDone ( ) ;
231+ } else {
232+ this . _isAnimating . set ( true ) ;
227233 }
228234 } ) ;
229235
@@ -244,6 +250,23 @@ export class MatStepper extends CdkStepper implements AfterContentInit, OnDestro
244250 } ) ;
245251 }
246252
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+
247270 override ngOnDestroy ( ) : void {
248271 super . ngOnDestroy ( ) ;
249272 this . _cleanupTransition ?.( ) ;
@@ -292,7 +315,12 @@ export class MatStepper extends CdkStepper implements AfterContentInit, OnDestro
292315 this . _animatedContainers . find ( ref => ref . nativeElement === target ) ;
293316
294317 if ( shouldEmit ) {
295- this . animationDone . emit ( ) ;
318+ this . _onAnimationDone ( ) ;
296319 }
297320 } ;
321+
322+ private _onAnimationDone ( ) {
323+ this . _isAnimating . set ( false ) ;
324+ this . animationDone . emit ( ) ;
325+ }
298326}
0 commit comments