Skip to content

Commit 99e243d

Browse files
committed
fixup! fix(material/stepper): switch away from animations module
1 parent 7c958f4 commit 99e243d

File tree

3 files changed

+53
-4
lines changed

3 files changed

+53
-4
lines changed

src/material/stepper/stepper.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@
203203
transform: none;
204204
height: auto;
205205
}
206+
207+
.mat-stepper-horizontal:not(.mat-stepper-animating) &.mat-horizontal-stepper-content-current {
208+
overflow: visible;
209+
}
206210
}
207211

208212
.mat-horizontal-content-container {

src/material/stepper/stepper.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {CdkStep, CdkStepper} from '@angular/cdk/stepper';
1010
import {
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,35 @@ 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+
let hasEmittedInitial = false;
265+
this._animatedContainers.changes
266+
.pipe(startWith(null), takeUntil(this._destroyed))
267+
.subscribe(() =>
268+
queueMicrotask(() => {
269+
// Simulate the initial `animationDone` event
270+
// that gets emitted by the animations module.
271+
if (!hasEmittedInitial) {
272+
hasEmittedInitial = true;
273+
this.animationDone.emit();
274+
}
275+
276+
this._stateChanged();
277+
}),
278+
);
279+
}
280+
}
281+
247282
override ngOnDestroy(): void {
248283
super.ngOnDestroy();
249284
this._cleanupTransition?.();
@@ -292,7 +327,12 @@ export class MatStepper extends CdkStepper implements AfterContentInit, OnDestro
292327
this._animatedContainers.find(ref => ref.nativeElement === target);
293328

294329
if (shouldEmit) {
295-
this.animationDone.emit();
330+
this._onAnimationDone();
296331
}
297332
};
333+
334+
private _onAnimationDone() {
335+
this._isAnimating.set(false);
336+
this.animationDone.emit();
337+
}
298338
}

tools/public_api_guard/material/stepper.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { Subject } from 'rxjs';
3434
import { TemplatePortal } from '@angular/cdk/portal';
3535
import { TemplateRef } from '@angular/core';
3636
import { ThemePalette } from '@angular/material/core';
37+
import { WritableSignal } from '@angular/core';
3738

3839
// @public
3940
export const MAT_STEPPER_INTL_PROVIDER: {
@@ -116,7 +117,7 @@ export class MatStepLabel extends CdkStepLabel {
116117
}
117118

118119
// @public (undocumented)
119-
export class MatStepper extends CdkStepper implements AfterContentInit, OnDestroy {
120+
export class MatStepper extends CdkStepper implements AfterViewInit, AfterContentInit, OnDestroy {
120121
constructor(...args: unknown[]);
121122
_animatedContainers: QueryList<ElementRef>;
122123
readonly animationDone: EventEmitter<void>;
@@ -129,11 +130,15 @@ export class MatStepper extends CdkStepper implements AfterContentInit, OnDestro
129130
headerPosition: 'top' | 'bottom';
130131
_iconOverrides: Record<string, TemplateRef<MatStepperIconContext>>;
131132
_icons: QueryList<MatStepperIcon>;
133+
// (undocumented)
134+
protected _isAnimating: WritableSignal<boolean>;
132135
protected _isServer: boolean;
133136
labelPosition: 'bottom' | 'end';
134137
// (undocumented)
135138
ngAfterContentInit(): void;
136139
// (undocumented)
140+
ngAfterViewInit(): void;
141+
// (undocumented)
137142
ngOnDestroy(): void;
138143
_stepHeader: QueryList<MatStepHeader>;
139144
// (undocumented)

0 commit comments

Comments
 (0)