Skip to content

Commit acdd568

Browse files
committed
feat(stepper): allow for orientation to be changed dynamically
* Turns the `MatStepper` into a proper component that allows consumers to switch between `horizontal` and `vertical` dynamically, allowing for use cases like having a different layout depending on the screen size. * Combines the `mat-vertical-stepper` and `mat-horizontal-stepper` templates into a single file to avoid all the code duplication. Relates to #7700.
1 parent c3d7cd9 commit acdd568

File tree

7 files changed

+150
-97
lines changed

7 files changed

+150
-97
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
<mat-checkbox [(ngModel)]="isNonLinear">Disable linear mode</mat-checkbox>
2+
<mat-checkbox [(ngModel)]="isVertical">Vertical</mat-checkbox>
23

3-
<h3>Linear Vertical Stepper Demo using a single form</h3>
4+
<h3>Linear Stepper Demo using a single form</h3>
45
<form [formGroup]="formGroup">
5-
<mat-vertical-stepper formArrayName="formArray" [linear]="!isNonLinear">
6+
<mat-stepper [orientation]="isVertical ? 'vertical' : 'horizontal'" formArrayName="formArray"
7+
[linear]="!isNonLinear">
68
<mat-step formGroupName="0" [stepControl]="formArray?.get([0])">
79
<ng-template matStepLabel>Fill out your name</ng-template>
810
<mat-form-field>
@@ -40,7 +42,7 @@ <h3>Linear Vertical Stepper Demo using a single form</h3>
4042
<button mat-button>Done</button>
4143
</div>
4244
</mat-step>
43-
</mat-vertical-stepper>
45+
</mat-stepper>
4446
</form>
4547

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class StepperDemo {
1818
formGroup: FormGroup;
1919
isNonLinear = false;
2020
isNonEditable = false;
21+
isVertical = true;
2122

2223
nameFormGroup: FormGroup;
2324
emailFormGroup: FormGroup;

src/lib/stepper/stepper-animations.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
animate,
11+
state,
12+
style,
13+
transition,
14+
trigger,
15+
AnimationTriggerMetadata,
16+
} from '@angular/animations';
17+
18+
/**
19+
* Animations used by the Material stepper.
20+
* @docs-private
21+
*/
22+
export const matStepperAnimations: AnimationTriggerMetadata[] = [
23+
trigger('horizontalStepTransition', [
24+
state('previous', style({transform: 'translate3d(-100%, 0, 0)', visibility: 'hidden'})),
25+
state('current', style({transform: 'none', visibility: 'visible'})),
26+
state('next', style({transform: 'translate3d(100%, 0, 0)', visibility: 'hidden'})),
27+
transition('* => *', animate('500ms cubic-bezier(0.35, 0, 0.25, 1)'))
28+
]),
29+
trigger('verticalStepTransition', [
30+
state('previous', style({height: '0px', visibility: 'hidden'})),
31+
state('next', style({height: '0px', visibility: 'hidden'})),
32+
state('current', style({height: '*', visibility: 'visible'})),
33+
transition('* <=> current', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
34+
])
35+
];

src/lib/stepper/stepper-horizontal.html

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

src/lib/stepper/stepper-vertical.html

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

src/lib/stepper/stepper.html

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<ng-container [ngSwitch]="orientation">
2+
<!-- Vertical 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 *ngTemplateOutlet="stepTemplate; context: {$implicit: step, i: i}"></ng-container>
7+
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
8+
</ng-container>
9+
</div>
10+
11+
<div class="mat-horizontal-content-container">
12+
<div *ngFor="let step of _steps; let i = index"
13+
class="mat-horizontal-stepper-content" role="tabpanel"
14+
[@horizontalStepTransition]="_getAnimationDirection(i)"
15+
[id]="_getStepContentId(i)"
16+
[attr.aria-labelledby]="_getStepLabelId(i)"
17+
[attr.aria-expanded]="selectedIndex === i">
18+
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
19+
</div>
20+
</div>
21+
</ng-container>
22+
23+
<!-- Horizontal stepper -->
24+
<ng-container *ngSwitchDefault>
25+
<div class="mat-step" *ngFor="let step of _steps; let i = index; let isLast = last">
26+
<ng-container *ngTemplateOutlet="stepTemplate; context: {$implicit: step, i: i}"></ng-container>
27+
28+
<div class="mat-vertical-content-container" [class.mat-stepper-vertical-line]="!isLast">
29+
<div class="mat-vertical-stepper-content" role="tabpanel"
30+
[@verticalStepTransition]="_getAnimationDirection(i)"
31+
[id]="_getStepContentId(i)"
32+
[attr.aria-labelledby]="_getStepLabelId(i)"
33+
[attr.aria-expanded]="selectedIndex === i">
34+
<div class="mat-vertical-content">
35+
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
36+
</div>
37+
</div>
38+
</div>
39+
</div>
40+
</ng-container>
41+
</ng-container>
42+
43+
<!-- Common step tempating -->
44+
<ng-template let-step let-i="i" #stepTemplate>
45+
<mat-step-header [ngClass]="'mat-' + orientation + '-stepper-header'"
46+
(click)="step.select()"
47+
(keydown)="_onKeydown($event)"
48+
[tabIndex]="_focusIndex == i ? 0 : -1"
49+
[id]="_getStepLabelId(i)"
50+
[attr.aria-controls]="_getStepContentId(i)"
51+
[attr.aria-selected]="selectedIndex === i"
52+
[index]="i"
53+
[icon]="_getIndicatorType(i)"
54+
[label]="step.stepLabel || step.label"
55+
[selected]="selectedIndex === i"
56+
[active]="step.completed || selectedIndex === i || !linear"
57+
[optional]="step.optional">
58+
</mat-step-header>
59+
</ng-template>

src/lib/stepper/stepper.ts

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,33 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {animate, state, style, transition, trigger} from '@angular/animations';
109
import {CdkStep, CdkStepper} from '@angular/cdk/stepper';
1110
import {
1211
AfterContentInit,
1312
Component,
1413
ContentChild,
1514
ContentChildren,
16-
Directive,
1715
ElementRef,
1816
forwardRef,
1917
Inject,
18+
Input,
2019
QueryList,
2120
SkipSelf,
2221
ViewChildren,
2322
ViewEncapsulation,
2423
ChangeDetectionStrategy,
24+
OnInit,
2525
} from '@angular/core';
2626
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
2727
import {ErrorStateMatcher} from '@angular/material/core';
2828
import {MatStepHeader} from './step-header';
2929
import {MatStepLabel} from './step-label';
30+
import {matStepperAnimations} from './stepper-animations';
3031
import {takeUntil} from 'rxjs/operators/takeUntil';
3132

33+
/** Possible orientations for the Material stepper. */
34+
export type MatStepperOrientation = 'horizontal' | 'vertical';
35+
3236
@Component({
3337
moduleId: module.id,
3438
selector: 'mat-step',
@@ -61,8 +65,23 @@ export class MatStep extends CdkStep implements ErrorStateMatcher {
6165
}
6266
}
6367

64-
@Directive({
65-
selector: '[matStepper]'
68+
@Component({
69+
moduleId: module.id,
70+
selector: 'mat-stepper, [matStepper]',
71+
templateUrl: 'stepper.html',
72+
styleUrls: ['stepper.css'],
73+
inputs: ['selectedIndex'],
74+
exportAs: 'matStepper',
75+
encapsulation: ViewEncapsulation.None,
76+
preserveWhitespaces: false,
77+
changeDetection: ChangeDetectionStrategy.OnPush,
78+
animations: matStepperAnimations,
79+
host: {
80+
'[class.mat-stepper-horizontal]': 'orientation === "horizontal"',
81+
'[class.mat-stepper-vertical]': 'orientation === "vertical"',
82+
'[attr.aria-orientation]': 'orientation',
83+
'role': 'tablist',
84+
},
6685
})
6786
export class MatStepper extends CdkStepper implements AfterContentInit {
6887
/** The list of step headers of the steps in the stepper. */
@@ -71,6 +90,9 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
7190
/** Steps that the stepper holds. */
7291
@ContentChildren(MatStep) _steps: QueryList<MatStep>;
7392

93+
/** Orientation of the stepper. */
94+
@Input() orientation: MatStepperOrientation = 'vertical';
95+
7496
ngAfterContentInit() {
7597
// Mark the component for change detection whenever the content children query changes
7698
this._steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => this._stateChanged());
@@ -79,54 +101,46 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
79101

80102
@Component({
81103
moduleId: module.id,
104+
encapsulation: ViewEncapsulation.None,
105+
preserveWhitespaces: false,
106+
changeDetection: ChangeDetectionStrategy.OnPush,
107+
templateUrl: 'stepper.html',
108+
styleUrls: ['stepper.css'],
82109
selector: 'mat-horizontal-stepper',
83110
exportAs: 'matHorizontalStepper',
84-
templateUrl: 'stepper-horizontal.html',
85-
styleUrls: ['stepper.css'],
86-
inputs: ['selectedIndex'],
111+
animations: matStepperAnimations,
112+
providers: [{provide: MatStepper, useExisting: MatHorizontalStepper}],
87113
host: {
88114
'class': 'mat-stepper-horizontal',
89115
'aria-orientation': 'horizontal',
90116
'role': 'tablist',
91117
},
92-
animations: [
93-
trigger('stepTransition', [
94-
state('previous', style({transform: 'translate3d(-100%, 0, 0)', visibility: 'hidden'})),
95-
state('current', style({transform: 'none', visibility: 'visible'})),
96-
state('next', style({transform: 'translate3d(100%, 0, 0)', visibility: 'hidden'})),
97-
transition('* => *', animate('500ms cubic-bezier(0.35, 0, 0.25, 1)'))
98-
])
99-
],
100-
providers: [{provide: MatStepper, useExisting: MatHorizontalStepper}],
101-
encapsulation: ViewEncapsulation.None,
102-
preserveWhitespaces: false,
103-
changeDetection: ChangeDetectionStrategy.OnPush,
104118
})
105-
export class MatHorizontalStepper extends MatStepper { }
119+
export class MatHorizontalStepper extends MatStepper implements OnInit {
120+
ngOnInit() {
121+
this.orientation = 'horizontal';
122+
}
123+
}
106124

107125
@Component({
108126
moduleId: module.id,
127+
encapsulation: ViewEncapsulation.None,
128+
preserveWhitespaces: false,
129+
changeDetection: ChangeDetectionStrategy.OnPush,
130+
templateUrl: 'stepper.html',
131+
styleUrls: ['stepper.css'],
109132
selector: 'mat-vertical-stepper',
110133
exportAs: 'matVerticalStepper',
111-
templateUrl: 'stepper-vertical.html',
112-
styleUrls: ['stepper.css'],
113-
inputs: ['selectedIndex'],
134+
providers: [{provide: MatStepper, useExisting: MatVerticalStepper}],
135+
animations: matStepperAnimations,
114136
host: {
115137
'class': 'mat-stepper-vertical',
116138
'aria-orientation': 'vertical',
117139
'role': 'tablist',
118140
},
119-
animations: [
120-
trigger('stepTransition', [
121-
state('previous', style({height: '0px', visibility: 'hidden'})),
122-
state('next', style({height: '0px', visibility: 'hidden'})),
123-
state('current', style({height: '*', visibility: 'visible'})),
124-
transition('* <=> current', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
125-
])
126-
],
127-
providers: [{provide: MatStepper, useExisting: MatVerticalStepper}],
128-
encapsulation: ViewEncapsulation.None,
129-
preserveWhitespaces: false,
130-
changeDetection: ChangeDetectionStrategy.OnPush,
131141
})
132-
export class MatVerticalStepper extends MatStepper { }
142+
export class MatVerticalStepper extends MatStepper implements OnInit {
143+
ngOnInit() {
144+
this.orientation = 'vertical';
145+
}
146+
}

0 commit comments

Comments
 (0)