Skip to content

Commit 8604c78

Browse files
committed
fix(material/stepper): switch away from animations module
Reworks the stepper so it uses CSS directly to animate, instead of going through the animations module. This both simplifies the setup and allows us to avoid the issues that come with the animations module.
1 parent a11b03b commit 8604c78

File tree

8 files changed

+177
-98
lines changed

8 files changed

+177
-98
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ export class CdkStep implements OnChanges {
255255
export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
256256
private _dir = inject(Directionality, {optional: true});
257257
private _changeDetectorRef = inject(ChangeDetectorRef);
258-
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
258+
protected _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
259259

260260
/** Emits when the component is destroyed. */
261261
protected readonly _destroyed = new Subject<void>();

src/material/stepper/stepper-animations.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ import {
1717
animateChild,
1818
} from '@angular/animations';
1919

20-
export const DEFAULT_HORIZONTAL_ANIMATION_DURATION = '500ms';
21-
export const DEFAULT_VERTICAL_ANIMATION_DURATION = '225ms';
22-
2320
/**
2421
* Animations used by the Material steppers.
2522
* @docs-private
23+
* @deprecated No longer used, will be removed.
24+
* @breaking-change 21.0.0
2625
*/
2726
export const matStepperAnimations: {
2827
readonly horizontalStepTransition: AnimationTriggerMetadata;
@@ -43,7 +42,7 @@ export const matStepperAnimations: {
4342
query('@*', animateChild(), {optional: true}),
4443
]),
4544
{
46-
params: {'animationDuration': DEFAULT_HORIZONTAL_ANIMATION_DURATION},
45+
params: {'animationDuration': '500ms'},
4746
},
4847
),
4948
]),
@@ -63,7 +62,7 @@ export const matStepperAnimations: {
6362
query('@*', animateChild(), {optional: true}),
6463
]),
6564
{
66-
params: {'animationDuration': DEFAULT_VERTICAL_ANIMATION_DURATION},
65+
params: {'animationDuration': '225ms'},
6766
},
6867
),
6968
]),

src/material/stepper/stepper.html

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,52 +12,51 @@
1212
@case ('horizontal') {
1313
<div class="mat-horizontal-stepper-wrapper">
1414
<div class="mat-horizontal-stepper-header-container">
15-
@for (step of steps; track step; let i = $index, isLast = $last) {
15+
@for (step of steps; track step) {
1616
<ng-container
1717
[ngTemplateOutlet]="stepTemplate"
18-
[ngTemplateOutletContext]="{step: step, i: i}"></ng-container>
19-
@if (!isLast) {
18+
[ngTemplateOutletContext]="{step, i: $index}"/>
19+
@if (!$last) {
2020
<div class="mat-stepper-horizontal-line"></div>
2121
}
2222
}
2323
</div>
2424

2525
<div class="mat-horizontal-content-container">
26-
@for (step of steps; track step; let i = $index) {
27-
<div class="mat-horizontal-stepper-content" role="tabpanel"
28-
[@horizontalStepTransition]="{
29-
'value': _getAnimationDirection(i),
30-
'params': {'animationDuration': _getAnimationDuration()}
31-
}"
32-
(@horizontalStepTransition.done)="_animationDone.next($event)"
33-
[id]="_getStepContentId(i)"
34-
[attr.aria-labelledby]="_getStepLabelId(i)"
35-
[class.mat-horizontal-stepper-content-inactive]="selectedIndex !== i">
36-
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
26+
@for (step of steps; track step) {
27+
<div
28+
#animatedContainer
29+
class="mat-horizontal-stepper-content"
30+
role="tabpanel"
31+
[id]="_getStepContentId($index)"
32+
[attr.aria-labelledby]="_getStepLabelId($index)"
33+
[class]="'mat-horizontal-content-' + _getAnimationDirection($index)"
34+
[attr.inert]="selectedIndex === $index ? null : ''">
35+
<ng-container [ngTemplateOutlet]="step.content"/>
3736
</div>
3837
}
3938
</div>
4039
</div>
4140
}
4241

4342
@case ('vertical') {
44-
@for (step of steps; track step; let i = $index, isLast = $last) {
43+
@for (step of steps; track step) {
4544
<div class="mat-step">
4645
<ng-container
4746
[ngTemplateOutlet]="stepTemplate"
48-
[ngTemplateOutletContext]="{step: step, i: i}"></ng-container>
49-
<div class="mat-vertical-content-container" [class.mat-stepper-vertical-line]="!isLast">
50-
<div class="mat-vertical-stepper-content" role="tabpanel"
51-
[@verticalStepTransition]="{
52-
'value': _getAnimationDirection(i),
53-
'params': {'animationDuration': _getAnimationDuration()}
54-
}"
55-
(@verticalStepTransition.done)="_animationDone.next($event)"
56-
[id]="_getStepContentId(i)"
57-
[attr.aria-labelledby]="_getStepLabelId(i)"
58-
[class.mat-vertical-stepper-content-inactive]="selectedIndex !== i">
47+
[ngTemplateOutletContext]="{step, i: $index}"/>
48+
<div
49+
#animatedContainer
50+
class="mat-vertical-content-container"
51+
[class.mat-stepper-vertical-line]="!$last"
52+
[class.mat-vertical-content-container-active]="selectedIndex === $index"
53+
[attr.inert]="selectedIndex === $index ? null : ''">
54+
<div class="mat-vertical-stepper-content"
55+
role="tabpanel"
56+
[id]="_getStepContentId($index)"
57+
[attr.aria-labelledby]="_getStepLabelId($index)">
5958
<div class="mat-vertical-content">
60-
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
59+
<ng-container [ngTemplateOutlet]="step.content"/>
6160
</div>
6261
</div>
6362
</div>
@@ -91,5 +90,5 @@
9190
[errorMessage]="step.errorMessage"
9291
[iconOverrides]="_iconOverrides"
9392
[disableRipple]="disableRipple || !_stepIsNavigable(i, step)"
94-
[color]="step.color || color"></mat-step-header>
93+
[color]="step.color || color"/>
9594
</ng-template>

src/material/stepper/stepper.scss

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -178,20 +178,30 @@
178178
}
179179

180180
.mat-horizontal-stepper-content {
181+
visibility: hidden;
182+
overflow: hidden;
181183
outline: 0;
184+
height: 0;
182185

183-
&.mat-horizontal-stepper-content-inactive {
184-
height: 0;
185-
overflow: hidden;
186+
.mat-stepper-animations-enabled & {
187+
transition: transform var(--mat-stepper-animation-duration, 0) cubic-bezier(0.35, 0, 0.25, 1);
188+
}
189+
190+
&.mat-horizontal-content-previous {
191+
transform: translate3d(-100%, 0, 0);
192+
}
193+
194+
&.mat-horizontal-content-next {
195+
transform: translate3d(100%, 0, 0);
186196
}
187197

188-
// Used to avoid an issue where when the stepper is nested inside a component that
189-
// changes the `visibility` as a part of an Angular animation, the stepper's content
190-
// stays hidden (see #25925). The value has to be `!important` to override the incorrect
191-
// `visibility` from the animations package. This can also be solved using `visibility: visible`
192-
// on `.mat-horizontal-stepper-content`, but it can allow tabbing into hidden content.
193-
&:not(.mat-horizontal-stepper-content-inactive) {
194-
visibility: inherit !important;
198+
&.mat-horizontal-content-current {
199+
// TODO(crisbeto): the height and visibility switches are a bit jarring, but that's how the
200+
// animation was set up when we still used the Animations module. We should be able to make
201+
// it a bit smoother.
202+
visibility: visible;
203+
transform: none;
204+
height: auto;
195205
}
196206
}
197207

@@ -209,10 +219,26 @@
209219
}
210220

211221
.mat-vertical-content-container {
222+
display: grid;
223+
grid-template-rows: 0fr;
224+
grid-template-columns: 100%;
212225
margin-left: stepper-variables.$vertical-stepper-content-margin;
213226
border: 0;
214227
position: relative;
215228

229+
.mat-stepper-animations-enabled & {
230+
transition: grid-template-rows var(--mat-stepper-animation-duration, 0)
231+
cubic-bezier(0.4, 0, 0.2, 1);
232+
}
233+
234+
&.mat-vertical-content-container-active {
235+
grid-template-rows: 1fr;
236+
}
237+
238+
.mat-step:last-child & {
239+
border: none;
240+
}
241+
216242
@include cdk.high-contrast {
217243
outline: solid 1px;
218244
}
@@ -221,6 +247,19 @@
221247
margin-left: 0;
222248
margin-right: stepper-variables.$vertical-stepper-content-margin;
223249
}
250+
251+
252+
// All the browsers we support have support for `grid` as well, but given that these styles are
253+
// load-bearing for the stepper, we have a fallback to height which doesn't animate, just in case.
254+
// stylelint-disable material/no-prefixes
255+
@supports not (grid-template-rows: 0fr) {
256+
height: 0;
257+
258+
&.mat-vertical-content-container-active {
259+
height: auto;
260+
}
261+
}
262+
// stylelint-enable material/no-prefixes
224263
}
225264

226265
.mat-stepper-vertical-line::before {
@@ -252,23 +291,17 @@
252291
.mat-vertical-stepper-content {
253292
overflow: hidden;
254293
outline: 0;
294+
visibility: hidden;
255295

256-
// Used to avoid an issue where when the stepper is nested inside a component that
257-
// changes the `visibility` as a part of an Angular animation, the stepper's content
258-
// stays hidden (see #25925). The value has to be `!important` to override the incorrect
259-
// `visibility` from the animations package. This can also be solved using `visibility: visible`
260-
// on `.mat-vertical-stepper-content`, but it can allow tabbing into hidden content.
261-
&:not(.mat-vertical-stepper-content-inactive) {
262-
visibility: inherit !important;
296+
.mat-stepper-animations-enabled & {
297+
transition: visibility var(--mat-stepper-animation-duration, 0) linear;
298+
}
299+
300+
.mat-vertical-content-container-active > & {
301+
visibility: visible;
263302
}
264303
}
265304

266305
.mat-vertical-content {
267306
padding: 0 stepper-variables.$side-gap stepper-variables.$side-gap stepper-variables.$side-gap;
268307
}
269-
270-
.mat-step:last-child {
271-
.mat-vertical-content-container {
272-
border: none;
273-
}
274-
}

src/material/stepper/stepper.spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -374,11 +374,6 @@ describe('MatStepper', () => {
374374
stepper.selectedIndex = 1;
375375
fixture.detectChanges();
376376

377-
expect(selectionChangeSpy).toHaveBeenCalledTimes(1);
378-
expect(animationDoneSpy).not.toHaveBeenCalled();
379-
380-
flush();
381-
382377
expect(selectionChangeSpy).toHaveBeenCalledTimes(1);
383378
expect(animationDoneSpy).toHaveBeenCalledTimes(1);
384379

0 commit comments

Comments
 (0)