Skip to content

Commit d1b0d2a

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 f0c7a25 commit d1b0d2a

File tree

10 files changed

+148
-39
lines changed

10 files changed

+148
-39
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: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
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-
class="mat-horizontal-stepper-content" role="tabpanel"
31-
[@stepTransition]="_getAnimationDirection(i)"
32-
(@stepTransition.done)="_animationDone.next($event)"
33-
[id]="_getStepContentId(i)"
34-
[attr.aria-labelledby]="_getStepLabelId(i)"
35-
[attr.aria-expanded]="selectedIndex === i">
36-
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
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>
38+
</div>
3739
</div>
3840
</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: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,21 @@ describe('MatStepper', () => {
937937

938938
expect(headerRipples.every(ripple => ripple.disabled)).toBe(true);
939939
});
940+
941+
it('should set a class on the host if the header is positioned at the bottom', () => {
942+
const fixture = createComponent(SimpleMatHorizontalStepperApp);
943+
fixture.detectChanges();
944+
const stepperHost = fixture.nativeElement.querySelector('.mat-stepper-horizontal');
945+
946+
expect(stepperHost.classList).not.toContain('mat-stepper-header-position-bottom');
947+
948+
fixture.componentInstance.headerPosition = 'bottom';
949+
fixture.detectChanges();
950+
951+
expect(stepperHost.classList).toContain('mat-stepper-header-position-bottom');
952+
});
953+
954+
940955
});
941956

942957
describe('linear stepper with valid step', () => {
@@ -1395,7 +1410,7 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
13951410

13961411
@Component({
13971412
template: `
1398-
<mat-horizontal-stepper [disableRipple]="disableRipple">
1413+
<mat-horizontal-stepper [disableRipple]="disableRipple" [headerPosition]="headerPosition">
13991414
<mat-step>
14001415
<ng-template matStepLabel>Step 1</ng-template>
14011416
Content 1
@@ -1425,6 +1440,7 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
14251440
class SimpleMatHorizontalStepperApp {
14261441
inputLabel = 'Step 3';
14271442
disableRipple = false;
1443+
headerPosition: string;
14281444
}
14291445

14301446
@Component({

src/material/stepper/stepper.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,9 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
146146
inputs: ['selectedIndex'],
147147
host: {
148148
'class': 'mat-stepper-horizontal',
149-
'[class.mat-stepper-label-position-end]': 'labelPosition == "end"',
150-
'[class.mat-stepper-label-position-bottom]': 'labelPosition == "bottom"',
149+
'[class.mat-stepper-label-position-end]': 'labelPosition === "end"',
150+
'[class.mat-stepper-label-position-bottom]': 'labelPosition === "bottom"',
151+
'[class.mat-stepper-header-position-bottom]': 'headerPosition === "bottom"',
151152
'aria-orientation': 'horizontal',
152153
'role': 'tablist',
153154
},
@@ -164,6 +165,10 @@ export class MatHorizontalStepper extends MatStepper {
164165
@Input()
165166
labelPosition: 'bottom' | 'end' = 'end';
166167

168+
/** Position of the stepper's header. */
169+
@Input()
170+
headerPosition: 'top' | 'bottom' = 'top';
171+
167172
static ngAcceptInputType_editable: BooleanInput;
168173
static ngAcceptInputType_optional: BooleanInput;
169174
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)