From 4d2a3c0cf7184d5fcd339cdfb5a5527f29afa500 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 11 Mar 2022 08:40:38 +0100 Subject: [PATCH] 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. --- .../material/stepper/index.ts | 3 ++ .../stepper-header-position-example.css | 1 + .../stepper-header-position-example.html | 33 +++++++++++++++++++ .../stepper-header-position-example.ts | 26 +++++++++++++++ src/material/stepper/stepper.html | 4 +-- src/material/stepper/stepper.md | 7 ++++ src/material/stepper/stepper.scss | 13 ++++++++ src/material/stepper/stepper.spec.ts | 19 ++++++++++- src/material/stepper/stepper.ts | 8 +++++ tools/public_api_guard/material/stepper.md | 3 +- 10 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.css create mode 100644 src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.html create mode 100644 src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.ts diff --git a/src/components-examples/material/stepper/index.ts b/src/components-examples/material/stepper/index.ts index 4cf6781d0871..3c8d2c61aa36 100644 --- a/src/components-examples/material/stepper/index.ts +++ b/src/components-examples/material/stepper/index.ts @@ -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, @@ -30,6 +31,7 @@ export { StepperVerticalExample, StepperLazyContentExample, StepperResponsiveExample, + StepperHeaderPositionExample, }; const EXAMPLES = [ @@ -44,6 +46,7 @@ const EXAMPLES = [ StepperVerticalExample, StepperLazyContentExample, StepperResponsiveExample, + StepperHeaderPositionExample, ]; @NgModule({ diff --git a/src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.css b/src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.css new file mode 100644 index 000000000000..7432308753e6 --- /dev/null +++ b/src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.css @@ -0,0 +1 @@ +/** No CSS for this example */ diff --git a/src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.html b/src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.html new file mode 100644 index 000000000000..7f092843f2f7 --- /dev/null +++ b/src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.html @@ -0,0 +1,33 @@ + + +
+ Fill out your name + + + +
+ +
+
+
+ +
+ Fill out your address + + + +
+ + +
+
+
+ + Done + You are now done. +
+ + +
+
+
diff --git a/src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.ts b/src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.ts new file mode 100644 index 000000000000..62b57ed53cb3 --- /dev/null +++ b/src/components-examples/material/stepper/stepper-header-position/stepper-header-position-example.ts @@ -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], + }); + } +} diff --git a/src/material/stepper/stepper.html b/src/material/stepper/stepper.html index b074d0c2e6e2..dadc6b4f2860 100644 --- a/src/material/stepper/stepper.html +++ b/src/material/stepper/stepper.html @@ -1,6 +1,6 @@ - +
-
+ diff --git a/src/material/stepper/stepper.md b/src/material/stepper/stepper.md index 229fb7044498..d2e486892697 100644 --- a/src/material/stepper/stepper.md +++ b/src/material/stepper/stepper.md @@ -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. + + + ### Stepper buttons There are two button directives to support navigation between different steps: `matStepperPrevious` and `matStepperNext`. diff --git a/src/material/stepper/stepper.scss b/src/material/stepper/stepper.scss index 1d73e0a0178a..6d0ba7c07f20 100644 --- a/src/material/stepper/stepper.scss +++ b/src/material/stepper/stepper.scss @@ -16,6 +16,10 @@ .mat-stepper-label-position-bottom & { align-items: flex-start; } + + .mat-stepper-header-position-bottom & { + order: 1; + } } .mat-stepper-horizontal-line { @@ -116,6 +120,11 @@ } } +.mat-horizontal-stepper-wrapper { + display: flex; + flex-direction: column; +} + .mat-horizontal-stepper-content { outline: 0; @@ -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 { diff --git a/src/material/stepper/stepper.spec.ts b/src/material/stepper/stepper.spec.ts index b627a1e79f5a..cf36fd75ebee 100644 --- a/src/material/stepper/stepper.spec.ts +++ b/src/material/stepper/stepper.spec.ts @@ -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', () => { @@ -1815,7 +1828,10 @@ class MatHorizontalStepperWithErrorsApp implements OnInit { @Component({ template: ` - + Step 1 Content 1 @@ -1847,6 +1863,7 @@ class SimpleMatHorizontalStepperApp { disableRipple = false; stepperTheme: ThemePalette; secondStepTheme: ThemePalette; + headerPosition: string; } @Component({ diff --git a/src/material/stepper/stepper.ts b/src/material/stepper/stepper.ts index 55e8b6d155cb..078986f25280 100644 --- a/src/material/stepper/stepper.ts +++ b/src/material/stepper/stepper.ts @@ -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', }, @@ -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> = {}; diff --git a/tools/public_api_guard/material/stepper.md b/tools/public_api_guard/material/stepper.md index 9bd7ba21bae9..3144750cb0c2 100644 --- a/tools/public_api_guard/material/stepper.md +++ b/tools/public_api_guard/material/stepper.md @@ -132,6 +132,7 @@ export class MatStepper extends CdkStepper implements AfterContentInit { readonly _animationDone: Subject; color: ThemePalette; disableRipple: boolean; + headerPosition: 'top' | 'bottom'; _iconOverrides: Record>; _icons: QueryList; labelPosition: 'bottom' | 'end'; @@ -143,7 +144,7 @@ export class MatStepper extends CdkStepper implements AfterContentInit { readonly steps: QueryList; _steps: QueryList; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }