Skip to content

feat(material/stepper): add the ability to control the position of the header in a horizontal stepper #15509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components-examples/material/stepper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {StepperHarnessExample} from './stepper-harness/stepper-harness-example';
import {StepperIntlExample} from './stepper-intl/stepper-intl-example';
import {StepperLazyContentExample} from './stepper-lazy-content/stepper-lazy-content-example';
import {StepperResponsiveExample} from './stepper-responsive/stepper-responsive-example';
import {StepperHeaderPositionExample} from './stepper-header-position/stepper-header-position-example';

export {
StepperEditableExample,
Expand All @@ -30,6 +31,7 @@ export {
StepperVerticalExample,
StepperLazyContentExample,
StepperResponsiveExample,
StepperHeaderPositionExample,
};

const EXAMPLES = [
Expand All @@ -44,6 +46,7 @@ const EXAMPLES = [
StepperVerticalExample,
StepperLazyContentExample,
StepperResponsiveExample,
StepperHeaderPositionExample,
];

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/** No CSS for this example */
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<mat-stepper headerPosition="bottom" #stepper>
<mat-step [stepControl]="firstFormGroup">
<form [formGroup]="firstFormGroup">
<ng-template matStepLabel>Fill out your name</ng-template>
<mat-form-field>
<input matInput placeholder="Last name, First name" formControlName="firstCtrl" required>
</mat-form-field>
<div>
<button mat-button matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="secondFormGroup" optional>
<form [formGroup]="secondFormGroup">
<ng-template matStepLabel>Fill out your address</ng-template>
<mat-form-field>
<input matInput placeholder="Address" formControlName="secondCtrl" required>
</mat-form-field>
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel>Done</ng-template>
You are now done.
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button (click)="stepper.reset()">Reset</button>
</div>
</mat-step>
</mat-stepper>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Component, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';

/**
* @title Stepper header position
*/
@Component({
selector: 'stepper-header-position-example',
templateUrl: 'stepper-header-position-example.html',
styleUrls: ['stepper-header-position-example.css'],
})
export class StepperHeaderPositionExample implements OnInit {
firstFormGroup: FormGroup;
secondFormGroup: FormGroup;

constructor(private _formBuilder: FormBuilder) {}

ngOnInit() {
this.firstFormGroup = this._formBuilder.group({
firstCtrl: ['', Validators.required],
});
this.secondFormGroup = this._formBuilder.group({
secondCtrl: ['', Validators.required],
});
}
}
4 changes: 2 additions & 2 deletions src/material/stepper/stepper.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<ng-container [ngSwitch]="orientation">
<!-- Horizontal stepper -->
<ng-container *ngSwitchCase="'horizontal'">
<div class="mat-horizontal-stepper-wrapper" *ngSwitchCase="'horizontal'">
<div class="mat-horizontal-stepper-header-container">
<ng-container *ngFor="let step of steps; let i = index; let isLast = last">
<ng-container
Expand All @@ -21,7 +21,7 @@
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
</div>
</div>
</ng-container>
</div>

<!-- Vertical stepper -->
<ng-container *ngSwitchCase="'vertical'">
Expand Down
7 changes: 7 additions & 0 deletions src/material/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ This behaviour is controlled by `labelPosition` property.
"file": "stepper-label-position-bottom-example.html",
"region": "label-position"}) -->

#### Header position
If you're using a horizontal stepper, you can control where the stepper's content is positioned
using the `headerPosition` input. By default it's on top of the content, but it can also be placed
under it.

<!-- example(stepper-header-position) -->

### Stepper buttons
There are two button directives to support navigation between different steps:
`matStepperPrevious` and `matStepperNext`.
Expand Down
13 changes: 13 additions & 0 deletions src/material/stepper/stepper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
.mat-stepper-label-position-bottom & {
align-items: flex-start;
}

.mat-stepper-header-position-bottom & {
order: 1;
}
}

.mat-stepper-horizontal-line {
Expand Down Expand Up @@ -116,6 +120,11 @@
}
}

.mat-horizontal-stepper-wrapper {
display: flex;
flex-direction: column;
}

.mat-horizontal-stepper-content {
outline: 0;

Expand All @@ -132,6 +141,10 @@

overflow: hidden;
padding: 0 stepper-variables.$side-gap stepper-variables.$side-gap stepper-variables.$side-gap;

.mat-stepper-header-position-bottom & {
padding: stepper-variables.$side-gap stepper-variables.$side-gap 0 stepper-variables.$side-gap;
}
}

.mat-vertical-content-container {
Expand Down
19 changes: 18 additions & 1 deletion src/material/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,19 @@ describe('MatStepper', () => {
expect(interactedSteps).toEqual([0, 1, 2]);
subscription.unsubscribe();
});

it('should set a class on the host if the header is positioned at the bottom', () => {
const fixture = createComponent(SimpleMatHorizontalStepperApp);
fixture.detectChanges();
const stepperHost = fixture.nativeElement.querySelector('.mat-stepper-horizontal');

expect(stepperHost.classList).not.toContain('mat-stepper-header-position-bottom');

fixture.componentInstance.headerPosition = 'bottom';
fixture.detectChanges();

expect(stepperHost.classList).toContain('mat-stepper-header-position-bottom');
});
});

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

@Component({
template: `
<mat-stepper [disableRipple]="disableRipple" [color]="stepperTheme">
<mat-stepper
[disableRipple]="disableRipple"
[color]="stepperTheme"
[headerPosition]="headerPosition">
<mat-step>
<ng-template matStepLabel>Step 1</ng-template>
Content 1
Expand Down Expand Up @@ -1847,6 +1863,7 @@ class SimpleMatHorizontalStepperApp {
disableRipple = false;
stepperTheme: ThemePalette;
secondStepTheme: ThemePalette;
headerPosition: string;
}

@Component({
Expand Down
8 changes: 8 additions & 0 deletions src/material/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export class MatStep extends CdkStep implements ErrorStateMatcher, AfterContentI
'orientation === "horizontal" && labelPosition == "end"',
'[class.mat-stepper-label-position-bottom]':
'orientation === "horizontal" && labelPosition == "bottom"',
'[class.mat-stepper-header-position-bottom]': 'headerPosition === "bottom"',
'[attr.aria-orientation]': 'orientation',
'role': 'tablist',
},
Expand Down Expand Up @@ -171,6 +172,13 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
@Input()
labelPosition: 'bottom' | 'end' = 'end';

/**
* Position of the stepper's header.
* Only applies in the `horizontal` orientation.
*/
@Input()
headerPosition: 'top' | 'bottom' = 'top';

/** Consumer-specified template-refs to be used to override the header icons. */
_iconOverrides: Record<string, TemplateRef<MatStepperIconContext>> = {};

Expand Down
3 changes: 2 additions & 1 deletion tools/public_api_guard/material/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
readonly _animationDone: Subject<AnimationEvent_2>;
color: ThemePalette;
disableRipple: boolean;
headerPosition: 'top' | 'bottom';
_iconOverrides: Record<string, TemplateRef<MatStepperIconContext>>;
_icons: QueryList<MatStepperIcon>;
labelPosition: 'bottom' | 'end';
Expand All @@ -143,7 +144,7 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
readonly steps: QueryList<MatStep>;
_steps: QueryList<MatStep>;
// (undocumented)
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>;
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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatStepper, [{ optional: true; }, null, null]>;
}
Expand Down