Skip to content

Commit cd5896f

Browse files
committed
feat(material/stepper): allow for orientation to be changed dynamically
Combines `mat-vertical-stepper` and `mat-horizontal-stepper` into a single `mat-stepper` class in order to allow for the orientation to be changed dynamically. Also deprecates `MatVerticalStepper` and `MatHorizontalStepper`. This is a reimplementation of #9173, however this time I took a different approach which should make it easier to maintain and eventually remove the two separate steppers. It should result in a smaller bundle as well. The main differences are: 1. Rather than have 3 components (`MatStepper`, `MatVerticalStepper` and `MatHorizontalStepper`), these changes combine everything into `MatStepper` while `MatVerticalStepper` and `MatHorizontalStepper` are only used as injection tokens for backwards compatibility. The `selector` and `exportAs` of `MatStepper` is changed to match the two individual steppers and the orientation is inferred from the tag name. This will make it much easier to remove the deprecated directives. Furthermore, it should result in a smaller bundle since the template and styles only need to be inlined in one place. 2. `MatVerticalStepper` and `MatHorizontalStepper` are turned into very basic directives that have the same public API as `MatStepper` and they proxy everything to it. This is primarily so that if somebody managed to get a hold of a `MatVerticalStepper` or `MatHorizontalStepper` instance, or they used the old classes to type their own code, it wouldn't result in a breaking change. Relates to #7700.
1 parent 9f879b2 commit cd5896f

File tree

12 files changed

+281
-216
lines changed

12 files changed

+281
-216
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,16 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
315315
/** Used to track unique ID for each stepper component. */
316316
_groupId: number;
317317

318-
// Note that this isn't an `Input` so it doesn't bleed into the Material stepper.
319318
/** Orientation of the stepper. */
319+
@Input()
320320
get orientation(): StepperOrientation { return this._orientation; }
321321
set orientation(value: StepperOrientation) {
322-
this._updateOrientation(value);
322+
// This is a protected method so that `MatSteppter` can hook into it.
323+
this._orientation = value;
324+
325+
if (this._keyManager) {
326+
this._keyManager.withVerticalOrientation(value === 'vertical');
327+
}
323328
}
324329

325330
/**
@@ -432,16 +437,6 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
432437
this._getGuidelineLogic(step, isCurrentStep, state);
433438
}
434439

435-
/** Updates the stepper orientation. */
436-
protected _updateOrientation(value: StepperOrientation) {
437-
// This is a protected method so that `MatSteppter` can hook into it.
438-
this._orientation = value;
439-
440-
if (this._keyManager) {
441-
this._keyManager.withVerticalOrientation(value === 'vertical');
442-
}
443-
}
444-
445440
private _getDefaultIndicatorLogic(step: CdkStep, isCurrentStep: boolean): StepState {
446441
if (step._showError && step.hasError && !isCurrentStep) {
447442
return STEP_STATE.ERROR;

src/dev-app/stepper/stepper-demo.html

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<p>
55
<mat-checkbox [(ngModel)]="disableRipple">Disable header ripple</mat-checkbox>
66
</p>
7+
<p>
8+
<mat-checkbox [(ngModel)]="isVertical">Vertical</mat-checkbox>
9+
</p>
710
<p>
811
<button mat-stroked-button (click)="showLabelBottom = !showLabelBottom">
912
Toggle label position
@@ -18,10 +21,15 @@
1821
</mat-form-field>
1922
</p>
2023

21-
<h3>Linear Vertical Stepper Demo using a single form</h3>
24+
<h3>Linear Stepper Demo using a single form</h3>
2225
<form [formGroup]="formGroup">
23-
<mat-vertical-stepper #linearVerticalStepper="matVerticalStepper" formArrayName="formArray"
24-
[linear]="!isNonLinear" [disableRipple]="disableRipple" [color]="theme">
26+
<mat-stepper
27+
#linearStepper="matVerticalStepper"
28+
formArrayName="formArray"
29+
[orientation]="isVertical ? 'vertical' : 'horizontal'"
30+
[linear]="!isNonLinear"
31+
[disableRipple]="disableRipple"
32+
[color]="theme">
2533
<mat-step formGroupName="0" [stepControl]="formArray?.get([0]) === null ? undefined! : formArray?.get([0])!">
2634
<ng-template matStepLabel>Fill out your name</ng-template>
2735
<mat-form-field>
@@ -61,10 +69,10 @@ <h3>Linear Vertical Stepper Demo using a single form</h3>
6169
Everything seems correct.
6270
<div>
6371
<button mat-button>Done</button>
64-
<button type="button" mat-button (click)="linearVerticalStepper.reset()">Reset</button>
72+
<button type="button" mat-button (click)="linearStepper.reset()">Reset</button>
6573
</div>
6674
</mat-step>
67-
</mat-vertical-stepper>
75+
</mat-stepper>
6876
</form>
6977

7078
<h3>Linear Horizontal Stepper Demo using a different form for each step</h3>

src/dev-app/stepper/stepper-demo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class StepperDemo implements OnInit {
2121
isNonEditable = false;
2222
disableRipple = false;
2323
showLabelBottom = false;
24+
isVertical = false;
2425

2526
nameFormGroup: FormGroup;
2627
emailFormGroup: FormGroup;

src/material/stepper/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
export {StepperOrientation} from '@angular/cdk/stepper';
910
export * from './stepper-module';
1011
export * from './step-label';
1112
export * from './stepper';

src/material/stepper/stepper-animations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const matStepperAnimations: {
2323
readonly verticalStepTransition: AnimationTriggerMetadata;
2424
} = {
2525
/** Animation that transitions the step along the X axis in a horizontal stepper. */
26-
horizontalStepTransition: trigger('stepTransition', [
26+
horizontalStepTransition: trigger('horizontalStepTransition', [
2727
state('previous', style({transform: 'translate3d(-100%, 0, 0)', visibility: 'hidden'})),
2828
// Transition to '', rather than `visible`, because visibility on a child element overrides
2929
// the one from the parent, making this element focusable inside of a `hidden` element.
@@ -33,7 +33,7 @@ export const matStepperAnimations: {
3333
]),
3434

3535
/** Animation that transitions the step along the Y axis in a vertical stepper. */
36-
verticalStepTransition: trigger('stepTransition', [
36+
verticalStepTransition: trigger('verticalStepTransition', [
3737
state('previous', style({height: '0px', visibility: 'hidden'})),
3838
state('next', style({height: '0px', visibility: 'hidden'})),
3939
// Transition to '', rather than `visible`, because visibility on a child element overrides

src/material/stepper/stepper-horizontal.html

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/material/stepper/stepper-vertical.html

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/material/stepper/stepper.html

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<ng-container [ngSwitch]="orientation">
2+
<!-- Horizontal stepper -->
3+
<ng-container *ngSwitchCase="'horizontal'">
4+
<div class="mat-horizontal-stepper-header-container">
5+
<ng-container *ngFor="let step of steps; let i = index; let isLast = last">
6+
<ng-container
7+
[ngTemplateOutlet]="stepTemplate"
8+
[ngTemplateOutletContext]="{step: step, i: i}"></ng-container>
9+
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
10+
</ng-container>
11+
</div>
12+
13+
<div class="mat-horizontal-content-container">
14+
<div *ngFor="let step of steps; let i = index"
15+
class="mat-horizontal-stepper-content" role="tabpanel"
16+
[@horizontalStepTransition]="_getAnimationDirection(i)"
17+
(@horizontalStepTransition.done)="_animationDone.next($event)"
18+
[id]="_getStepContentId(i)"
19+
[attr.aria-labelledby]="_getStepLabelId(i)"
20+
[attr.aria-expanded]="selectedIndex === i">
21+
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
22+
</div>
23+
</div>
24+
</ng-container>
25+
26+
<!-- Vertical stepper -->
27+
<ng-container *ngSwitchCase="'vertical'">
28+
<div class="mat-step" *ngFor="let step of steps; let i = index; let isLast = last">
29+
<ng-container
30+
[ngTemplateOutlet]="stepTemplate"
31+
[ngTemplateOutletContext]="{step: step, i: i}"></ng-container>
32+
<div class="mat-vertical-content-container" [class.mat-stepper-vertical-line]="!isLast">
33+
<div class="mat-vertical-stepper-content" role="tabpanel"
34+
[@verticalStepTransition]="_getAnimationDirection(i)"
35+
(@verticalStepTransition.done)="_animationDone.next($event)"
36+
[id]="_getStepContentId(i)"
37+
[attr.aria-labelledby]="_getStepLabelId(i)"
38+
[attr.aria-expanded]="selectedIndex === i">
39+
<div class="mat-vertical-content">
40+
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
</ng-container>
46+
47+
</ng-container>
48+
49+
<!-- Common step templating -->
50+
<ng-template let-step="step" let-i="i" #stepTemplate>
51+
<mat-step-header
52+
[class.mat-horizontal-stepper-header]="orientation === 'horizontal'"
53+
[class.mat-vertical-stepper-header]="orientation === 'vertical'"
54+
(click)="step.select()"
55+
(keydown)="_onKeydown($event)"
56+
[tabIndex]="_getFocusIndex() === i ? 0 : -1"
57+
[id]="_getStepLabelId(i)"
58+
[attr.aria-posinset]="i + 1"
59+
[attr.aria-setsize]="steps.length"
60+
[attr.aria-controls]="_getStepContentId(i)"
61+
[attr.aria-selected]="selectedIndex == i"
62+
[attr.aria-label]="step.ariaLabel || null"
63+
[attr.aria-labelledby]="(!step.ariaLabel && step.ariaLabelledby) ? step.ariaLabelledby : null"
64+
[index]="i"
65+
[state]="_getIndicatorType(i, step.state)"
66+
[label]="step.stepLabel || step.label"
67+
[selected]="selectedIndex === i"
68+
[active]="step.completed || selectedIndex === i || !linear"
69+
[optional]="step.optional"
70+
[errorMessage]="step.errorMessage"
71+
[iconOverrides]="_iconOverrides"
72+
[disableRipple]="disableRipple"
73+
[color]="step.color || color"></mat-step-header>
74+
</ng-template>

src/material/stepper/stepper.spec.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import {NoopAnimationsModule} from '@angular/platform-browser/animations';
4848
import {Observable, Subject} from 'rxjs';
4949
import {map, take} from 'rxjs/operators';
5050
import {MatStepHeader, MatStepperModule} from './index';
51-
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
51+
import {MatStep, MatStepper} from './stepper';
5252
import {MatStepperNext, MatStepperPrevious} from './stepper-button';
5353
import {MatStepperIntl} from './stepper-intl';
5454
import {MatFormFieldModule} from '@angular/material/form-field';
@@ -81,7 +81,7 @@ describe('MatStepper', () => {
8181
});
8282

8383
it('should throw when a negative `selectedIndex` is assigned', () => {
84-
const stepperComponent: MatVerticalStepper = fixture.debugElement
84+
const stepperComponent: MatStepper = fixture.debugElement
8585
.query(By.css('mat-vertical-stepper'))!.componentInstance;
8686

8787
expect(() => {
@@ -91,7 +91,7 @@ describe('MatStepper', () => {
9191
});
9292

9393
it('should throw when an out-of-bounds `selectedIndex` is assigned', () => {
94-
const stepperComponent: MatVerticalStepper = fixture.debugElement
94+
const stepperComponent: MatStepper = fixture.debugElement
9595
.query(By.css('mat-vertical-stepper'))!.componentInstance;
9696

9797
expect(() => {
@@ -360,7 +360,7 @@ describe('MatStepper', () => {
360360
});
361361

362362
it('should adjust the index when removing a step before the current one', () => {
363-
const stepperComponent: MatVerticalStepper = fixture.debugElement
363+
const stepperComponent: MatStepper = fixture.debugElement
364364
.query(By.css('mat-vertical-stepper'))!.componentInstance;
365365

366366
stepperComponent.selectedIndex = 2;
@@ -395,20 +395,12 @@ describe('MatStepper', () => {
395395
.every(element => element.classList.contains('mat-focus-indicator'))).toBe(true);
396396
});
397397

398-
it('should throw when trying to change the orientation of a stepper', () => {
399-
const stepperComponent: MatStepper = fixture.debugElement
400-
.query(By.css('mat-vertical-stepper'))!.componentInstance;
401-
402-
expect(() => stepperComponent.orientation = 'horizontal')
403-
.toThrowError('Updating the orientation of a Material stepper is not supported.');
404-
});
405-
406398
});
407399

408400
describe('basic stepper when attempting to set the selected step too early', () => {
409401
it('should not throw', () => {
410402
const fixture = createComponent(SimpleMatVerticalStepperApp);
411-
const stepperComponent: MatVerticalStepper = fixture.debugElement
403+
const stepperComponent: MatStepper = fixture.debugElement
412404
.query(By.css('mat-vertical-stepper'))!.componentInstance;
413405

414406
expect(() => stepperComponent.selected).not.toThrow();
@@ -418,7 +410,7 @@ describe('MatStepper', () => {
418410
describe('basic stepper when attempting to set the selected step too early', () => {
419411
it('should not throw', () => {
420412
const fixture = createComponent(SimpleMatVerticalStepperApp);
421-
const stepperComponent: MatVerticalStepper = fixture.debugElement
413+
const stepperComponent: MatStepper = fixture.debugElement
422414
.query(By.css('mat-vertical-stepper'))!.componentInstance;
423415

424416
expect(() => stepperComponent.selected = null!).not.toThrow();
@@ -529,7 +521,7 @@ describe('MatStepper', () => {
529521
describe('linear stepper', () => {
530522
let fixture: ComponentFixture<LinearMatVerticalStepperApp>;
531523
let testComponent: LinearMatVerticalStepperApp;
532-
let stepperComponent: MatVerticalStepper;
524+
let stepperComponent: MatStepper;
533525

534526
beforeEach(() => {
535527
fixture = createComponent(LinearMatVerticalStepperApp);
@@ -769,13 +761,13 @@ describe('MatStepper', () => {
769761

770762
describe('linear stepper with a pre-defined selectedIndex', () => {
771763
let preselectedFixture: ComponentFixture<SimplePreselectedMatHorizontalStepperApp>;
772-
let stepper: MatHorizontalStepper;
764+
let stepper: MatStepper;
773765

774766
beforeEach(() => {
775767
preselectedFixture = createComponent(SimplePreselectedMatHorizontalStepperApp);
776768
preselectedFixture.detectChanges();
777769
stepper = preselectedFixture.debugElement
778-
.query(By.directive(MatHorizontalStepper))!.componentInstance;
770+
.query(By.directive(MatStepper))!.componentInstance;
779771
});
780772

781773
it('should not throw', () => {
@@ -798,8 +790,8 @@ describe('MatStepper', () => {
798790
noStepControlFixture.detectChanges();
799791
});
800792
it('should not move to the next step if the current one is not completed ', () => {
801-
const stepper: MatHorizontalStepper = noStepControlFixture.debugElement
802-
.query(By.directive(MatHorizontalStepper))!.componentInstance;
793+
const stepper: MatStepper = noStepControlFixture.debugElement
794+
.query(By.directive(MatStepper))!.componentInstance;
803795

804796
const headers = noStepControlFixture.debugElement
805797
.queryAll(By.css('.mat-horizontal-stepper-header'));
@@ -825,8 +817,8 @@ describe('MatStepper', () => {
825817
expect(controlAndBindingFixture.componentInstance.steps[0].control.valid).toBe(true);
826818
expect(controlAndBindingFixture.componentInstance.steps[0].completed).toBe(false);
827819

828-
const stepper: MatHorizontalStepper = controlAndBindingFixture.debugElement
829-
.query(By.directive(MatHorizontalStepper))!.componentInstance;
820+
const stepper: MatStepper = controlAndBindingFixture.debugElement
821+
.query(By.directive(MatStepper))!.componentInstance;
830822

831823
const headers = controlAndBindingFixture.debugElement
832824
.queryAll(By.css('.mat-horizontal-stepper-header'));

0 commit comments

Comments
 (0)