Skip to content

Commit 484f331

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 29e74eb commit 484f331

File tree

10 files changed

+150
-41
lines changed

10 files changed

+150
-41
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: 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
@@ -945,6 +945,21 @@ describe('MatStepper', () => {
945945

946946
expect(headerRipples.every(ripple => ripple.disabled)).toBe(true);
947947
});
948+
949+
it('should set a class on the host if the header is positioned at the bottom', () => {
950+
const fixture = createComponent(SimpleMatHorizontalStepperApp);
951+
fixture.detectChanges();
952+
const stepperHost = fixture.nativeElement.querySelector('.mat-stepper-horizontal');
953+
954+
expect(stepperHost.classList).not.toContain('mat-stepper-header-position-bottom');
955+
956+
fixture.componentInstance.headerPosition = 'bottom';
957+
fixture.detectChanges();
958+
959+
expect(stepperHost.classList).toContain('mat-stepper-header-position-bottom');
960+
});
961+
962+
948963
});
949964

950965
describe('linear stepper with valid step', () => {
@@ -1394,7 +1409,7 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
13941409

13951410
@Component({
13961411
template: `
1397-
<mat-horizontal-stepper [disableRipple]="disableRipple">
1412+
<mat-horizontal-stepper [disableRipple]="disableRipple" [headerPosition]="headerPosition">
13981413
<mat-step>
13991414
<ng-template matStepLabel>Step 1</ng-template>
14001415
Content 1
@@ -1424,6 +1439,7 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
14241439
class SimpleMatHorizontalStepperApp {
14251440
inputLabel = 'Step 3';
14261441
disableRipple = false;
1442+
headerPosition: string;
14271443
}
14281444

14291445
@Component({

src/material/stepper/stepper.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,9 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
142142
inputs: ['selectedIndex'],
143143
host: {
144144
'class': 'mat-stepper-horizontal',
145-
'[class.mat-stepper-label-position-end]': 'labelPosition == "end"',
146-
'[class.mat-stepper-label-position-bottom]': 'labelPosition == "bottom"',
145+
'[class.mat-stepper-label-position-end]': 'labelPosition === "end"',
146+
'[class.mat-stepper-label-position-bottom]': 'labelPosition === "bottom"',
147+
'[class.mat-stepper-header-position-bottom]': 'headerPosition === "bottom"',
147148
'aria-orientation': 'horizontal',
148149
'role': 'tablist',
149150
},
@@ -160,6 +161,10 @@ export class MatHorizontalStepper extends MatStepper {
160161
@Input()
161162
labelPosition: 'bottom' | 'end' = 'end';
162163

164+
/** Position of the stepper's header. */
165+
@Input()
166+
headerPosition: 'top' | 'bottom' = 'top';
167+
163168
static ngAcceptInputType_editable: BooleanInput;
164169
static ngAcceptInputType_optional: BooleanInput;
165170
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)