Skip to content

Commit dc52652

Browse files
committed
feat(material/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 71b7b15 commit dc52652

File tree

10 files changed

+152
-40
lines changed

10 files changed

+152
-40
lines changed

src/components-examples/material/stepper/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import {StepperOptionalExample} from './stepper-optional/stepper-optional-exampl
1313
import {StepperOverviewExample} from './stepper-overview/stepper-overview-example';
1414
import {StepperStatesExample} from './stepper-states/stepper-states-example';
1515
import {StepperVerticalExample} from './stepper-vertical/stepper-vertical-example';
16+
import {
17+
StepperHeaderPositionExample
18+
} from './stepper-header-position/stepper-header-position-example';
1619

1720
export {
1821
StepperEditableExample,
@@ -22,6 +25,7 @@ export {
2225
StepperOverviewExample,
2326
StepperStatesExample,
2427
StepperVerticalExample,
28+
StepperHeaderPositionExample,
2529
};
2630

2731
const EXAMPLES = [
@@ -32,6 +36,7 @@ const EXAMPLES = [
3236
StepperOverviewExample,
3337
StepperStatesExample,
3438
StepperVerticalExample,
39+
StepperHeaderPositionExample,
3540
];
3641

3742
@NgModule({
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: 38 additions & 36 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-
[color]="step.color || color">
24-
</mat-step-header>
25-
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
26-
</ng-container>
27-
</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+
[color]="step.color || color">
25+
</mat-step-header>
26+
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
27+
</ng-container>
28+
</div>
2829

29-
<div class="mat-horizontal-content-container">
30-
<div *ngFor="let step of steps; let i = index"
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>
30+
<div class="mat-horizontal-content-container">
31+
<div *ngFor="let step of steps; let i = index"
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>
3840
</div>
3941
</div>

src/material/stepper/stepper.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ This behaviour is controlled by `labelPosition` property.
3737
"file": "stepper-label-position-bottom-example.html",
3838
"region": "label-position"}) -->
3939

40+
#### Header position
41+
If you're using a horizontal stepper, you can control where the stepper's content is positioned
42+
using the `headerPosition` input. By default it's on top of the content, but it can also be placed
43+
under it.
44+
45+
<!-- example(stepper-header-position) -->
46+
4047
### Stepper buttons
4148
There are two button directives to support navigation between different steps:
4249
`matStepperPrevious` and `matStepperNext`.

src/material/stepper/stepper.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
.mat-stepper-label-position-bottom & {
1515
align-items: flex-start;
1616
}
17+
18+
.mat-stepper-header-position-bottom & {
19+
order: 1;
20+
}
1721
}
1822

1923
.mat-stepper-horizontal-line {
@@ -113,6 +117,11 @@
113117
}
114118
}
115119

120+
.mat-horizontal-stepper-wrapper {
121+
display: flex;
122+
flex-direction: column;
123+
}
124+
116125
.mat-horizontal-stepper-content {
117126
outline: 0;
118127

@@ -125,6 +134,10 @@
125134
.mat-horizontal-content-container {
126135
overflow: hidden;
127136
padding: 0 $mat-stepper-side-gap $mat-stepper-side-gap $mat-stepper-side-gap;
137+
138+
.mat-stepper-header-position-bottom & {
139+
padding: $mat-stepper-side-gap $mat-stepper-side-gap 0 $mat-stepper-side-gap;
140+
}
128141
}
129142

130143
.mat-vertical-content-container {

src/material/stepper/stepper.spec.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,21 @@ describe('MatStepper', () => {
10251025
expect(headers[2].classList.contains('mat-primary')).toBe(true);
10261026
expect(headers[1].classList.contains('mat-accent')).toBe(true);
10271027
});
1028+
1029+
it('should set a class on the host if the header is positioned at the bottom', () => {
1030+
const fixture = createComponent(SimpleMatHorizontalStepperApp);
1031+
fixture.detectChanges();
1032+
const stepperHost = fixture.nativeElement.querySelector('.mat-stepper-horizontal');
1033+
1034+
expect(stepperHost.classList).not.toContain('mat-stepper-header-position-bottom');
1035+
1036+
fixture.componentInstance.headerPosition = 'bottom';
1037+
fixture.detectChanges();
1038+
1039+
expect(stepperHost.classList).toContain('mat-stepper-header-position-bottom');
1040+
});
1041+
1042+
10281043
});
10291044

10301045
describe('linear stepper with valid step', () => {
@@ -1501,7 +1516,10 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
15011516

15021517
@Component({
15031518
template: `
1504-
<mat-horizontal-stepper [disableRipple]="disableRipple" [color]="stepperTheme">
1519+
<mat-horizontal-stepper
1520+
[disableRipple]="disableRipple"
1521+
[color]="stepperTheme"
1522+
[headerPosition]="headerPosition">
15051523
<mat-step>
15061524
<ng-template matStepLabel>Step 1</ng-template>
15071525
Content 1
@@ -1533,6 +1551,7 @@ class SimpleMatHorizontalStepperApp {
15331551
disableRipple = false;
15341552
stepperTheme: ThemePalette;
15351553
secondStepTheme: ThemePalette;
1554+
headerPosition: string;
15361555
}
15371556

15381557
@Component({

src/material/stepper/stepper.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,9 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
152152
inputs: ['selectedIndex'],
153153
host: {
154154
'class': 'mat-stepper-horizontal',
155-
'[class.mat-stepper-label-position-end]': 'labelPosition == "end"',
156-
'[class.mat-stepper-label-position-bottom]': 'labelPosition == "bottom"',
155+
'[class.mat-stepper-label-position-end]': 'labelPosition === "end"',
156+
'[class.mat-stepper-label-position-bottom]': 'labelPosition === "bottom"',
157+
'[class.mat-stepper-header-position-bottom]': 'headerPosition === "bottom"',
157158
'aria-orientation': 'horizontal',
158159
'role': 'tablist',
159160
},
@@ -170,6 +171,10 @@ export class MatHorizontalStepper extends MatStepper {
170171
@Input()
171172
labelPosition: 'bottom' | 'end' = 'end';
172173

174+
/** Position of the stepper's header. */
175+
@Input()
176+
headerPosition: 'top' | 'bottom' = 'top';
177+
173178
static ngAcceptInputType_editable: BooleanInput;
174179
static ngAcceptInputType_optional: BooleanInput;
175180
static ngAcceptInputType_completed: BooleanInput;

tools/public_api_guard/material/stepper.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ 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
static ngAcceptInputType_completed: BooleanInput;
1213
static ngAcceptInputType_editable: BooleanInput;
1314
static ngAcceptInputType_hasError: BooleanInput;
1415
static ngAcceptInputType_optional: BooleanInput;
15-
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatHorizontalStepper, "mat-horizontal-stepper", ["matHorizontalStepper"], { "selectedIndex": "selectedIndex"; "labelPosition": "labelPosition"; }, {}, never, never>;
16+
static ɵcmp: i0.ɵɵComponentDefWithMeta<MatHorizontalStepper, "mat-horizontal-stepper", ["matHorizontalStepper"], { "selectedIndex": "selectedIndex"; "labelPosition": "labelPosition"; "headerPosition": "headerPosition"; }, {}, never, never>;
1617
static ɵfac: i0.ɵɵFactoryDef<MatHorizontalStepper, never>;
1718
}
1819

0 commit comments

Comments
 (0)