Skip to content

fix(material/stepper): switch away from animations module #30314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export class CdkStep implements OnChanges {
export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
private _dir = inject(Directionality, {optional: true});
private _changeDetectorRef = inject(ChangeDetectorRef);
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
protected _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);

/** Emits when the component is destroyed. */
protected readonly _destroyed = new Subject<void>();
Expand Down
9 changes: 4 additions & 5 deletions src/material/stepper/stepper-animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ import {
animateChild,
} from '@angular/animations';

export const DEFAULT_HORIZONTAL_ANIMATION_DURATION = '500ms';
export const DEFAULT_VERTICAL_ANIMATION_DURATION = '225ms';

/**
* Animations used by the Material steppers.
* @docs-private
* @deprecated No longer used, will be removed.
* @breaking-change 21.0.0
*/
export const matStepperAnimations: {
readonly horizontalStepTransition: AnimationTriggerMetadata;
Expand All @@ -43,7 +42,7 @@ export const matStepperAnimations: {
query('@*', animateChild(), {optional: true}),
]),
{
params: {'animationDuration': DEFAULT_HORIZONTAL_ANIMATION_DURATION},
params: {'animationDuration': '500ms'},
},
),
]),
Expand All @@ -63,7 +62,7 @@ export const matStepperAnimations: {
query('@*', animateChild(), {optional: true}),
]),
{
params: {'animationDuration': DEFAULT_VERTICAL_ANIMATION_DURATION},
params: {'animationDuration': '225ms'},
},
),
]),
Expand Down
55 changes: 27 additions & 28 deletions src/material/stepper/stepper.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,51 @@
@case ('horizontal') {
<div class="mat-horizontal-stepper-wrapper">
<div class="mat-horizontal-stepper-header-container">
@for (step of steps; track step; let i = $index, isLast = $last) {
@for (step of steps; track step) {
<ng-container
[ngTemplateOutlet]="stepTemplate"
[ngTemplateOutletContext]="{step: step, i: i}"></ng-container>
@if (!isLast) {
[ngTemplateOutletContext]="{step, i: $index}"/>
@if (!$last) {
<div class="mat-stepper-horizontal-line"></div>
}
}
</div>

<div class="mat-horizontal-content-container">
@for (step of steps; track step; let i = $index) {
<div class="mat-horizontal-stepper-content" role="tabpanel"
[@horizontalStepTransition]="{
'value': _getAnimationDirection(i),
'params': {'animationDuration': _getAnimationDuration()}
}"
(@horizontalStepTransition.done)="_animationDone.next($event)"
[id]="_getStepContentId(i)"
[attr.aria-labelledby]="_getStepLabelId(i)"
[class.mat-horizontal-stepper-content-inactive]="selectedIndex !== i">
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
@for (step of steps; track step) {
<div
#animatedContainer
class="mat-horizontal-stepper-content"
role="tabpanel"
[id]="_getStepContentId($index)"
[attr.aria-labelledby]="_getStepLabelId($index)"
[class]="'mat-horizontal-stepper-content-' + _getAnimationDirection($index)"
[attr.inert]="selectedIndex === $index ? null : ''">
<ng-container [ngTemplateOutlet]="step.content"/>
</div>
}
</div>
</div>
}

@case ('vertical') {
@for (step of steps; track step; let i = $index, isLast = $last) {
@for (step of steps; track step) {
<div class="mat-step">
<ng-container
[ngTemplateOutlet]="stepTemplate"
[ngTemplateOutletContext]="{step: step, i: i}"></ng-container>
<div class="mat-vertical-content-container" [class.mat-stepper-vertical-line]="!isLast">
<div class="mat-vertical-stepper-content" role="tabpanel"
[@verticalStepTransition]="{
'value': _getAnimationDirection(i),
'params': {'animationDuration': _getAnimationDuration()}
}"
(@verticalStepTransition.done)="_animationDone.next($event)"
[id]="_getStepContentId(i)"
[attr.aria-labelledby]="_getStepLabelId(i)"
[class.mat-vertical-stepper-content-inactive]="selectedIndex !== i">
[ngTemplateOutletContext]="{step, i: $index}"/>
<div
#animatedContainer
class="mat-vertical-content-container"
[class.mat-stepper-vertical-line]="!$last"
[class.mat-vertical-content-container-active]="selectedIndex === $index"
[attr.inert]="selectedIndex === $index ? null : ''">
<div class="mat-vertical-stepper-content"
role="tabpanel"
[id]="_getStepContentId($index)"
[attr.aria-labelledby]="_getStepLabelId($index)">
<div class="mat-vertical-content">
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
<ng-container [ngTemplateOutlet]="step.content"/>
</div>
</div>
</div>
Expand Down Expand Up @@ -91,5 +90,5 @@
[errorMessage]="step.errorMessage"
[iconOverrides]="_iconOverrides"
[disableRipple]="disableRipple || !_stepIsNavigable(i, step)"
[color]="step.color || color"></mat-step-header>
[color]="step.color || color"/>
</ng-template>
83 changes: 60 additions & 23 deletions src/material/stepper/stepper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -178,20 +178,34 @@
}

.mat-horizontal-stepper-content {
visibility: hidden;
overflow: hidden;
outline: 0;
height: 0;

&.mat-horizontal-stepper-content-inactive {
height: 0;
overflow: hidden;
.mat-stepper-animations-enabled & {
transition: transform var(--mat-stepper-animation-duration, 0) cubic-bezier(0.35, 0, 0.25, 1);
}

&.mat-horizontal-stepper-content-previous {
transform: translate3d(-100%, 0, 0);
}

&.mat-horizontal-stepper-content-next {
transform: translate3d(100%, 0, 0);
}

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

.mat-stepper-horizontal:not(.mat-stepper-animating) &.mat-horizontal-stepper-content-current {
overflow: visible;
}
}

Expand All @@ -209,10 +223,26 @@
}

.mat-vertical-content-container {
display: grid;
grid-template-rows: 0fr;
grid-template-columns: 100%;
margin-left: stepper-variables.$vertical-stepper-content-margin;
border: 0;
position: relative;

.mat-stepper-animations-enabled & {
transition: grid-template-rows var(--mat-stepper-animation-duration, 0)
cubic-bezier(0.4, 0, 0.2, 1);
}

&.mat-vertical-content-container-active {
grid-template-rows: 1fr;
}

.mat-step:last-child & {
border: none;
}

@include cdk.high-contrast {
outline: solid 1px;
}
Expand All @@ -221,6 +251,19 @@
margin-left: 0;
margin-right: stepper-variables.$vertical-stepper-content-margin;
}


// All the browsers we support have support for `grid` as well, but given that these styles are
// load-bearing for the stepper, we have a fallback to height which doesn't animate, just in case.
// stylelint-disable material/no-prefixes
@supports not (grid-template-rows: 0fr) {
height: 0;

&.mat-vertical-content-container-active {
height: auto;
}
}
// stylelint-enable material/no-prefixes
}

.mat-stepper-vertical-line::before {
Expand Down Expand Up @@ -252,23 +295,17 @@
.mat-vertical-stepper-content {
overflow: hidden;
outline: 0;
visibility: hidden;

.mat-stepper-animations-enabled & {
transition: visibility var(--mat-stepper-animation-duration, 0) linear;
}

// Used to avoid an issue where when the stepper is nested inside a component that
// changes the `visibility` as a part of an Angular animation, the stepper's content
// stays hidden (see #25925). The value has to be `!important` to override the incorrect
// `visibility` from the animations package. This can also be solved using `visibility: visible`
// on `.mat-vertical-stepper-content`, but it can allow tabbing into hidden content.
&:not(.mat-vertical-stepper-content-inactive) {
visibility: inherit !important;
.mat-vertical-content-container-active > & {
visibility: visible;
}
}

.mat-vertical-content {
padding: 0 stepper-variables.$side-gap stepper-variables.$side-gap stepper-variables.$side-gap;
}

.mat-step:last-child {
.mat-vertical-content-container {
border: none;
}
}
11 changes: 3 additions & 8 deletions src/material/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
inject,
signal,
} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {
AbstractControl,
AsyncValidatorFn,
Expand Down Expand Up @@ -364,7 +364,7 @@ describe('MatStepper', () => {
expect(stepperComponent._getIndicatorType(0)).toBe('done');
});

it('should emit an event when the enter animation is done', fakeAsync(() => {
it('should emit an event when the enter animation is done', () => {
const stepper = fixture.debugElement.query(By.directive(MatStepper))!.componentInstance;
const selectionChangeSpy = jasmine.createSpy('selectionChange spy');
const animationDoneSpy = jasmine.createSpy('animationDone spy');
Expand All @@ -374,17 +374,12 @@ describe('MatStepper', () => {
stepper.selectedIndex = 1;
fixture.detectChanges();

expect(selectionChangeSpy).toHaveBeenCalledTimes(1);
expect(animationDoneSpy).not.toHaveBeenCalled();

flush();

expect(selectionChangeSpy).toHaveBeenCalledTimes(1);
expect(animationDoneSpy).toHaveBeenCalledTimes(1);

selectionChangeSubscription.unsubscribe();
animationDoneSubscription.unsubscribe();
}));
});

it('should set the correct aria-posinset and aria-setsize', () => {
const headers = Array.from<HTMLElement>(
Expand Down
Loading
Loading