Skip to content

Commit 4a7643b

Browse files
committed
feat(stepper): add the ability to control the position of the header in a horizontal stepper
Allows users to control where the header of a horizontal stepper is rendered. In some cases it might make more sense to have the buttons be under the content so that the user doesn't have to go back in the page layout in order to continue to the next step.
1 parent eef132b commit 4a7643b

File tree

9 files changed

+144
-40
lines changed

9 files changed

+144
-40
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/** No CSS for this example */
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<mat-horizontal-stepper headerPosition="bottom" #stepper>
2+
<mat-step [stepControl]="firstFormGroup">
3+
<form [formGroup]="firstFormGroup">
4+
<ng-template matStepLabel>Fill out your name</ng-template>
5+
<mat-form-field>
6+
<input matInput placeholder="Last name, First name" formControlName="firstCtrl" required>
7+
</mat-form-field>
8+
<div>
9+
<button mat-button matStepperNext>Next</button>
10+
</div>
11+
</form>
12+
</mat-step>
13+
<mat-step [stepControl]="secondFormGroup" optional>
14+
<form [formGroup]="secondFormGroup">
15+
<ng-template matStepLabel>Fill out your address</ng-template>
16+
<mat-form-field>
17+
<input matInput placeholder="Address" formControlName="secondCtrl" required>
18+
</mat-form-field>
19+
<div>
20+
<button mat-button matStepperPrevious>Back</button>
21+
<button mat-button matStepperNext>Next</button>
22+
</div>
23+
</form>
24+
</mat-step>
25+
<mat-step>
26+
<ng-template matStepLabel>Done</ng-template>
27+
You are now done.
28+
<div>
29+
<button mat-button matStepperPrevious>Back</button>
30+
<button mat-button (click)="stepper.reset()">Reset</button>
31+
</div>
32+
</mat-step>
33+
</mat-horizontal-stepper>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {Component, OnInit} from '@angular/core';
2+
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
3+
4+
/**
5+
* @title Stepper header position
6+
*/
7+
@Component({
8+
selector: 'stepper-header-position-example',
9+
templateUrl: 'stepper-header-position-example.html',
10+
styleUrls: ['stepper-header-position-example.css'],
11+
})
12+
export class StepperHeaderPositionExample implements OnInit {
13+
firstFormGroup: FormGroup;
14+
secondFormGroup: FormGroup;
15+
16+
constructor(private _formBuilder: FormBuilder) {}
17+
18+
ngOnInit() {
19+
this.firstFormGroup = this._formBuilder.group({
20+
firstCtrl: ['', Validators.required]
21+
});
22+
this.secondFormGroup = this._formBuilder.group({
23+
secondCtrl: ['', Validators.required]
24+
});
25+
}
26+
}
Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,41 @@
1-
<div class="mat-horizontal-stepper-header-container">
2-
<ng-container *ngFor="let step of steps; let i = index; let isLast = last">
3-
<mat-step-header class="mat-horizontal-stepper-header"
4-
(click)="step.select()"
5-
(keydown)="_onKeydown($event)"
6-
[tabIndex]="_getFocusIndex() === i ? 0 : -1"
7-
[id]="_getStepLabelId(i)"
8-
[attr.aria-posinset]="i + 1"
9-
[attr.aria-setsize]="steps.length"
10-
[attr.aria-controls]="_getStepContentId(i)"
11-
[attr.aria-selected]="selectedIndex == i"
12-
[attr.aria-label]="step.ariaLabel || null"
13-
[attr.aria-labelledby]="(!step.ariaLabel && step.ariaLabelledby) ? step.ariaLabelledby : null"
14-
[index]="i"
15-
[state]="_getIndicatorType(i, step.state)"
16-
[label]="step.stepLabel || step.label"
17-
[selected]="selectedIndex === i"
18-
[active]="step.completed || selectedIndex === i || !linear"
19-
[optional]="step.optional"
20-
[errorMessage]="step.errorMessage"
21-
[iconOverrides]="_iconOverrides"
22-
[disableRipple]="disableRipple">
23-
</mat-step-header>
24-
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
25-
</ng-container>
26-
</div>
1+
<div class="mat-horizontal-stepper-wrapper">
2+
<div class="mat-horizontal-stepper-header-container">
3+
<ng-container *ngFor="let step of steps; let i = index; let isLast = last">
4+
<mat-step-header class="mat-horizontal-stepper-header"
5+
(click)="step.select()"
6+
(keydown)="_onKeydown($event)"
7+
[tabIndex]="_getFocusIndex() === i ? 0 : -1"
8+
[id]="_getStepLabelId(i)"
9+
[attr.aria-posinset]="i + 1"
10+
[attr.aria-setsize]="steps.length"
11+
[attr.aria-controls]="_getStepContentId(i)"
12+
[attr.aria-selected]="selectedIndex == i"
13+
[attr.aria-label]="step.ariaLabel || null"
14+
[attr.aria-labelledby]="(!step.ariaLabel && step.ariaLabelledby) ? step.ariaLabelledby : null"
15+
[index]="i"
16+
[state]="_getIndicatorType(i, step.state)"
17+
[label]="step.stepLabel || step.label"
18+
[selected]="selectedIndex === i"
19+
[active]="step.completed || selectedIndex === i || !linear"
20+
[optional]="step.optional"
21+
[errorMessage]="step.errorMessage"
22+
[iconOverrides]="_iconOverrides"
23+
[disableRipple]="disableRipple">
24+
</mat-step-header>
25+
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
26+
</ng-container>
27+
</div>
2728

28-
<div class="mat-horizontal-content-container">
29-
<div *ngFor="let step of steps; let i = index"
30-
[attr.tabindex]="selectedIndex === i ? 0 : null"
31-
class="mat-horizontal-stepper-content" role="tabpanel"
32-
[@stepTransition]="_getAnimationDirection(i)"
33-
(@stepTransition.done)="_animationDone.next($event)"
34-
[id]="_getStepContentId(i)"
35-
[attr.aria-labelledby]="_getStepLabelId(i)"
36-
[attr.aria-expanded]="selectedIndex === i">
37-
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
38-
</div>
29+
<div class="mat-horizontal-content-container">
30+
<div *ngFor="let step of steps; let i = index"
31+
[attr.tabindex]="selectedIndex === i ? 0 : null"
32+
class="mat-horizontal-stepper-content" role="tabpanel"
33+
[@stepTransition]="_getAnimationDirection(i)"
34+
(@stepTransition.done)="_animationDone.next($event)"
35+
[id]="_getStepContentId(i)"
36+
[attr.aria-labelledby]="_getStepLabelId(i)"
37+
[attr.aria-expanded]="selectedIndex === i">
38+
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
39+
</div>
40+
</div>
3941
</div>

src/material/stepper/stepper.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ This behaviour is controlled by `labelPosition` property.
4747

4848
<!-- example(stepper-label-position-bottom) -->
4949

50+
#### Header position
51+
If you're using a horizontal stepper, you can control where the stepper's content is positioned
52+
using the `headerPosition` input. By default it's on top of the content, but it can also be placed
53+
under it.
54+
55+
<!-- example(stepper-header-position) -->
56+
5057
### Stepper buttons
5158
There are two button directives to support navigation between different steps:
5259
`matStepperPrevious` and `matStepperNext`.

src/material/stepper/stepper.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ $mat-stepper-line-gap: 8px !default;
2121
.mat-stepper-label-position-bottom & {
2222
align-items: flex-start;
2323
}
24+
25+
.mat-stepper-header-position-bottom & {
26+
order: 1;
27+
}
2428
}
2529

2630
.mat-stepper-horizontal-line {
@@ -124,6 +128,11 @@ $mat-stepper-line-gap: 8px !default;
124128
}
125129
}
126130

131+
.mat-horizontal-stepper-wrapper {
132+
display: flex;
133+
flex-direction: column;
134+
}
135+
127136
.mat-horizontal-stepper-content {
128137
outline: 0;
129138

@@ -136,6 +145,10 @@ $mat-stepper-line-gap: 8px !default;
136145
.mat-horizontal-content-container {
137146
overflow: hidden;
138147
padding: 0 $mat-stepper-side-gap $mat-stepper-side-gap $mat-stepper-side-gap;
148+
149+
.mat-stepper-header-position-bottom & {
150+
padding: $mat-stepper-side-gap $mat-stepper-side-gap 0 $mat-stepper-side-gap;
151+
}
139152
}
140153

141154
.mat-vertical-content-container {

src/material/stepper/stepper.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,21 @@ describe('MatStepper', () => {
899899

900900
expect(headerRipples.every(ripple => ripple.disabled)).toBe(true);
901901
});
902+
903+
it('should set a class on the host if the header is positioned at the bottom', () => {
904+
const fixture = createComponent(SimpleMatHorizontalStepperApp);
905+
fixture.detectChanges();
906+
const stepperHost = fixture.nativeElement.querySelector('.mat-stepper-horizontal');
907+
908+
expect(stepperHost.classList).not.toContain('mat-stepper-header-position-bottom');
909+
910+
fixture.componentInstance.headerPosition = 'bottom';
911+
fixture.detectChanges();
912+
913+
expect(stepperHost.classList).toContain('mat-stepper-header-position-bottom');
914+
});
915+
916+
902917
});
903918

904919
describe('linear stepper with valid step', () => {
@@ -1263,7 +1278,7 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
12631278

12641279
@Component({
12651280
template: `
1266-
<mat-horizontal-stepper [disableRipple]="disableRipple">
1281+
<mat-horizontal-stepper [disableRipple]="disableRipple" [headerPosition]="headerPosition">
12671282
<mat-step>
12681283
<ng-template matStepLabel>Step 1</ng-template>
12691284
Content 1
@@ -1293,6 +1308,7 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
12931308
class SimpleMatHorizontalStepperApp {
12941309
inputLabel = 'Step 3';
12951310
disableRipple = false;
1311+
headerPosition: string;
12961312
}
12971313

12981314
@Component({

src/material/stepper/stepper.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,9 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
133133
inputs: ['selectedIndex'],
134134
host: {
135135
'class': 'mat-stepper-horizontal',
136-
'[class.mat-stepper-label-position-end]': 'labelPosition == "end"',
137-
'[class.mat-stepper-label-position-bottom]': 'labelPosition == "bottom"',
136+
'[class.mat-stepper-label-position-end]': 'labelPosition === "end"',
137+
'[class.mat-stepper-label-position-bottom]': 'labelPosition === "bottom"',
138+
'[class.mat-stepper-header-position-bottom]': 'headerPosition === "bottom"',
138139
'aria-orientation': 'horizontal',
139140
'role': 'tablist',
140141
},
@@ -150,6 +151,10 @@ export class MatHorizontalStepper extends MatStepper {
150151
/** Whether the label should display in bottom or end position. */
151152
@Input()
152153
labelPosition: 'bottom' | 'end' = 'end';
154+
155+
/** Position of the stepper's header. */
156+
@Input()
157+
headerPosition: 'top' | 'bottom' = 'top';
153158
}
154159

155160
@Component({

tools/public_api_guard/material/stepper.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export declare const MAT_STEPPER_INTL_PROVIDER: {
77
export declare function MAT_STEPPER_INTL_PROVIDER_FACTORY(parentIntl: MatStepperIntl): MatStepperIntl;
88

99
export declare class MatHorizontalStepper extends MatStepper {
10+
headerPosition: 'top' | 'bottom';
1011
labelPosition: 'bottom' | 'end';
1112
}
1213

0 commit comments

Comments
 (0)