Skip to content
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

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

Merged
merged 1 commit into from
Jan 16, 2025
Merged
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
@@ -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>();
9 changes: 4 additions & 5 deletions src/material/stepper/stepper-animations.ts
Original file line number Diff line number Diff line change
@@ -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;
@@ -43,7 +42,7 @@ export const matStepperAnimations: {
query('@*', animateChild(), {optional: true}),
]),
{
params: {'animationDuration': DEFAULT_HORIZONTAL_ANIMATION_DURATION},
params: {'animationDuration': '500ms'},
},
),
]),
@@ -63,7 +62,7 @@ export const matStepperAnimations: {
query('@*', animateChild(), {optional: true}),
]),
{
params: {'animationDuration': DEFAULT_VERTICAL_ANIMATION_DURATION},
params: {'animationDuration': '225ms'},
},
),
]),
55 changes: 27 additions & 28 deletions src/material/stepper/stepper.html
Original file line number Diff line number Diff line change
@@ -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>
@@ -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
@@ -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;
}
}

@@ -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;
}
@@ -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 {
@@ -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
@@ -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,
@@ -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');
@@ -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>(
Loading