Skip to content

Commit fc5f8b0

Browse files
authored
feat(material/stepper): add the ability to control the position of the header in a horizontal stepper (#15509)
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 45fae71 commit fc5f8b0

File tree

10 files changed

+113
-4
lines changed

10 files changed

+113
-4
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {StepperHarnessExample} from './stepper-harness/stepper-harness-example';
1717
import {StepperIntlExample} from './stepper-intl/stepper-intl-example';
1818
import {StepperLazyContentExample} from './stepper-lazy-content/stepper-lazy-content-example';
1919
import {StepperResponsiveExample} from './stepper-responsive/stepper-responsive-example';
20+
import {StepperHeaderPositionExample} from './stepper-header-position/stepper-header-position-example';
2021

2122
export {
2223
StepperEditableExample,
@@ -30,6 +31,7 @@ export {
3031
StepperVerticalExample,
3132
StepperLazyContentExample,
3233
StepperResponsiveExample,
34+
StepperHeaderPositionExample,
3335
};
3436

3537
const EXAMPLES = [
@@ -44,6 +46,7 @@ const EXAMPLES = [
4446
StepperVerticalExample,
4547
StepperLazyContentExample,
4648
StepperResponsiveExample,
49+
StepperHeaderPositionExample,
4750
];
4851

4952
@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-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-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+
}

src/material/stepper/stepper.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<ng-container [ngSwitch]="orientation">
22
<!-- Horizontal stepper -->
3-
<ng-container *ngSwitchCase="'horizontal'">
3+
<div class="mat-horizontal-stepper-wrapper" *ngSwitchCase="'horizontal'">
44
<div class="mat-horizontal-stepper-header-container">
55
<ng-container *ngFor="let step of steps; let i = index; let isLast = last">
66
<ng-container
@@ -21,7 +21,7 @@
2121
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
2222
</div>
2323
</div>
24-
</ng-container>
24+
</div>
2525

2626
<!-- Vertical stepper -->
2727
<ng-container *ngSwitchCase="'vertical'">

src/material/stepper/stepper.md

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

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

src/material/stepper/stepper.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
.mat-stepper-label-position-bottom & {
1717
align-items: flex-start;
1818
}
19+
20+
.mat-stepper-header-position-bottom & {
21+
order: 1;
22+
}
1923
}
2024

2125
.mat-stepper-horizontal-line {
@@ -116,6 +120,11 @@
116120
}
117121
}
118122

123+
.mat-horizontal-stepper-wrapper {
124+
display: flex;
125+
flex-direction: column;
126+
}
127+
119128
.mat-horizontal-stepper-content {
120129
outline: 0;
121130

@@ -132,6 +141,10 @@
132141

133142
overflow: hidden;
134143
padding: 0 stepper-variables.$side-gap stepper-variables.$side-gap stepper-variables.$side-gap;
144+
145+
.mat-stepper-header-position-bottom & {
146+
padding: stepper-variables.$side-gap stepper-variables.$side-gap 0 stepper-variables.$side-gap;
147+
}
135148
}
136149

137150
.mat-vertical-content-container {

src/material/stepper/stepper.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,19 @@ describe('MatStepper', () => {
12381238
expect(interactedSteps).toEqual([0, 1, 2]);
12391239
subscription.unsubscribe();
12401240
});
1241+
1242+
it('should set a class on the host if the header is positioned at the bottom', () => {
1243+
const fixture = createComponent(SimpleMatHorizontalStepperApp);
1244+
fixture.detectChanges();
1245+
const stepperHost = fixture.nativeElement.querySelector('.mat-stepper-horizontal');
1246+
1247+
expect(stepperHost.classList).not.toContain('mat-stepper-header-position-bottom');
1248+
1249+
fixture.componentInstance.headerPosition = 'bottom';
1250+
fixture.detectChanges();
1251+
1252+
expect(stepperHost.classList).toContain('mat-stepper-header-position-bottom');
1253+
});
12411254
});
12421255

12431256
describe('linear stepper with valid step', () => {
@@ -1815,7 +1828,10 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
18151828

18161829
@Component({
18171830
template: `
1818-
<mat-stepper [disableRipple]="disableRipple" [color]="stepperTheme">
1831+
<mat-stepper
1832+
[disableRipple]="disableRipple"
1833+
[color]="stepperTheme"
1834+
[headerPosition]="headerPosition">
18191835
<mat-step>
18201836
<ng-template matStepLabel>Step 1</ng-template>
18211837
Content 1
@@ -1847,6 +1863,7 @@ class SimpleMatHorizontalStepperApp {
18471863
disableRipple = false;
18481864
stepperTheme: ThemePalette;
18491865
secondStepTheme: ThemePalette;
1866+
headerPosition: string;
18501867
}
18511868

18521869
@Component({

src/material/stepper/stepper.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI
131131
'orientation === "horizontal" && labelPosition == "end"',
132132
'[class.mat-stepper-label-position-bottom]':
133133
'orientation === "horizontal" && labelPosition == "bottom"',
134+
'[class.mat-stepper-header-position-bottom]': 'headerPosition === "bottom"',
134135
'[attr.aria-orientation]': 'orientation',
135136
'role': 'tablist',
136137
},
@@ -171,6 +172,13 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
171172
@Input()
172173
labelPosition: 'bottom' | 'end' = 'end';
173174

175+
/**
176+
* Position of the stepper's header.
177+
* Only applies in the `horizontal` orientation.
178+
*/
179+
@Input()
180+
headerPosition: 'top' | 'bottom' = 'top';
181+
174182
/** Consumer-specified template-refs to be used to override the header icons. */
175183
_iconOverrides: Record<string, TemplateRef<MatStepperIconContext>> = {};
176184

tools/public_api_guard/material/stepper.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
132132
readonly _animationDone: Subject<AnimationEvent_2>;
133133
color: ThemePalette;
134134
disableRipple: boolean;
135+
headerPosition: 'top' | 'bottom';
135136
_iconOverrides: Record<string, TemplateRef<MatStepperIconContext>>;
136137
_icons: QueryList<MatStepperIcon>;
137138
labelPosition: 'bottom' | 'end';
@@ -143,7 +144,7 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
143144
readonly steps: QueryList<MatStep>;
144145
_steps: QueryList<MatStep>;
145146
// (undocumented)
146-
static ɵcmp: i0.ɵɵComponentDeclaration<MatStepper, "mat-stepper, mat-vertical-stepper, mat-horizontal-stepper, [matStepper]", ["matStepper", "matVerticalStepper", "matHorizontalStepper"], { "selectedIndex": "selectedIndex"; "disableRipple": "disableRipple"; "color": "color"; "labelPosition": "labelPosition"; }, { "animationDone": "animationDone"; }, ["_steps", "_icons"], never>;
147+
static ɵcmp: i0.ɵɵComponentDeclaration<MatStepper, "mat-stepper, mat-vertical-stepper, mat-horizontal-stepper, [matStepper]", ["matStepper", "matVerticalStepper", "matHorizontalStepper"], { "selectedIndex": "selectedIndex"; "disableRipple": "disableRipple"; "color": "color"; "labelPosition": "labelPosition"; "headerPosition": "headerPosition"; }, { "animationDone": "animationDone"; }, ["_steps", "_icons"], never>;
147148
// (undocumented)
148149
static ɵfac: i0.ɵɵFactoryDeclaration<MatStepper, [{ optional: true; }, null, null]>;
149150
}

0 commit comments

Comments
 (0)