Skip to content

Commit e545b56

Browse files
authored
feat(stepper): add theming support (#20424)
Adds `color` inputs to `mat-step`, `mat-vertical-stepper` and `mat-horizontal-stepper` so that their color can be changed from the default `primary`. Fixes #20416.
1 parent 6e957c1 commit e545b56

File tree

13 files changed

+179
-24
lines changed

13 files changed

+179
-24
lines changed

src/cdk/stepper/step-header.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {FocusableOption} from '@angular/cdk/a11y';
1717
},
1818
})
1919
export class CdkStepHeader implements FocusableOption {
20-
constructor(protected _elementRef: ElementRef<HTMLElement>) {}
20+
constructor(public _elementRef: ElementRef<HTMLElement>) {}
2121

2222
/** Focuses the step header. */
2323
focus() {

src/dev-app/stepper/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ ng_module(
99
deps = [
1010
"//src/material/button",
1111
"//src/material/checkbox",
12+
"//src/material/core",
1213
"//src/material/form-field",
1314
"//src/material/input",
15+
"//src/material/select",
1416
"//src/material/stepper",
1517
"@npm//@angular/forms",
1618
"@npm//@angular/router",

src/dev-app/stepper/stepper-demo-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {MatCheckboxModule} from '@angular/material/checkbox';
1414
import {MatFormFieldModule} from '@angular/material/form-field';
1515
import {MatInputModule} from '@angular/material/input';
1616
import {MatStepperModule} from '@angular/material/stepper';
17+
import {MatSelectModule} from '@angular/material/select';
1718
import {RouterModule} from '@angular/router';
1819
import {StepperDemo} from './stepper-demo';
1920

@@ -26,6 +27,7 @@ import {StepperDemo} from './stepper-demo';
2627
MatFormFieldModule,
2728
MatInputModule,
2829
MatStepperModule,
30+
MatSelectModule,
2931
ReactiveFormsModule,
3032
RouterModule.forChild([{path: '', component: StepperDemo}]),
3133
],

src/dev-app/stepper/stepper-demo.html

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1-
<mat-checkbox [(ngModel)]="isNonLinear">Disable linear mode</mat-checkbox>
2-
<mat-checkbox [(ngModel)]="disableRipple">Disable header ripple</mat-checkbox>
1+
<p>
2+
<mat-checkbox [(ngModel)]="isNonLinear">Disable linear mode</mat-checkbox>
3+
</p>
4+
<p>
5+
<mat-checkbox [(ngModel)]="disableRipple">Disable header ripple</mat-checkbox>
6+
</p>
37
<p>
48
<button mat-stroked-button (click)="showLabelBottom = !showLabelBottom">
59
Toggle label position
610
</button>
711
</p>
12+
<p>
13+
<mat-form-field>
14+
<mat-label>Theme</mat-label>
15+
<mat-select [(ngModel)]="theme">
16+
<mat-option *ngFor="let theme of availableThemes" [value]="theme.value">{{theme.name}}</mat-option>
17+
</mat-select>
18+
</mat-form-field>
19+
</p>
820

921
<h3>Linear Vertical Stepper Demo using a single form</h3>
1022
<form [formGroup]="formGroup">
1123
<mat-vertical-stepper #linearVerticalStepper="matVerticalStepper" formArrayName="formArray"
12-
[linear]="!isNonLinear" [disableRipple]="disableRipple">
24+
[linear]="!isNonLinear" [disableRipple]="disableRipple" [color]="theme">
1325
<mat-step formGroupName="0" [stepControl]="formArray?.get([0]) === null ? undefined! : formArray?.get([0])!">
1426
<ng-template matStepLabel>Fill out your name</ng-template>
1527
<mat-form-field>
@@ -58,7 +70,8 @@ <h3>Linear Vertical Stepper Demo using a single form</h3>
5870
<h3>Linear Horizontal Stepper Demo using a different form for each step</h3>
5971
<mat-horizontal-stepper #linearHorizontalStepper="matHorizontalStepper" [linear]="!isNonLinear"
6072
[disableRipple]="disableRipple"
61-
[labelPosition]="showLabelBottom ? 'bottom' : 'end'">
73+
[labelPosition]="showLabelBottom ? 'bottom' : 'end'"
74+
[color]="theme">
6275
<mat-step [stepControl]="nameFormGroup">
6376
<form [formGroup]="nameFormGroup">
6477
<ng-template matStepLabel>Fill out your name</ng-template>
@@ -107,7 +120,7 @@ <h3>Linear Horizontal Stepper Demo using a different form for each step</h3>
107120

108121
<h3>Vertical Stepper Demo</h3>
109122
<mat-checkbox [(ngModel)]="isNonEditable">Make steps non-editable</mat-checkbox>
110-
<mat-vertical-stepper>
123+
<mat-vertical-stepper [color]="theme">
111124
<mat-step [editable]="!isNonEditable">
112125
<ng-template matStepLabel>Fill out your name</ng-template>
113126
<mat-form-field>
@@ -162,7 +175,7 @@ <h3>Vertical Stepper Demo</h3>
162175
</mat-vertical-stepper>
163176

164177
<h3>Horizontal Stepper Demo with Text Label</h3>
165-
<mat-horizontal-stepper>
178+
<mat-horizontal-stepper [color]="theme">
166179
<mat-step label="Fill out your name">
167180
<mat-form-field>
168181
<mat-label>First name</mat-label>
@@ -209,7 +222,7 @@ <h3>Horizontal Stepper Demo with Text Label</h3>
209222
</mat-horizontal-stepper>
210223

211224
<h3>Horizontal Stepper Demo with Templated Label</h3>
212-
<mat-horizontal-stepper>
225+
<mat-horizontal-stepper [color]="theme">
213226
<mat-step *ngFor="let step of steps">
214227
<ng-template matStepLabel>{{step.label}}</ng-template>
215228
<mat-form-field>
@@ -224,7 +237,7 @@ <h3>Horizontal Stepper Demo with Templated Label</h3>
224237
</mat-horizontal-stepper>
225238

226239
<h3>Stepper with autosize textarea</h3>
227-
<mat-horizontal-stepper>
240+
<mat-horizontal-stepper [color]="theme">
228241
<mat-step label="Step 1">
229242
<mat-form-field>
230243
<mat-label>Autosize textarea</mat-label>

src/dev-app/stepper/stepper-demo.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {Component, OnInit} from '@angular/core';
10+
import {ThemePalette} from '@angular/material/core';
1011
import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms';
1112

1213

@@ -31,6 +32,14 @@ export class StepperDemo implements OnInit {
3132
{label: 'You are now done', content: 'Finished!'}
3233
];
3334

35+
availableThemes: {value: ThemePalette, name: string}[] = [
36+
{value: 'primary', name: 'Primary'},
37+
{value: 'accent', name: 'Accent'},
38+
{value: 'warn', name: 'Warn'}
39+
];
40+
41+
theme = this.availableThemes[0].value;
42+
3443
/** Returns a FormArray with the name 'formArray'. */
3544
get formArray(): AbstractControl | null { return this.formGroup.get('formArray'); }
3645

src/material/stepper/_stepper-theme.scss

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
$foreground: map-get($config, foreground);
1111
$background: map-get($config, background);
1212
$primary: map-get($config, primary);
13+
$accent: map-get($config, accent);
1314
$warn: map-get($config, warn);
1415

1516
.mat-step-header {
@@ -49,6 +50,32 @@
4950
color: mat-color($primary, default-contrast);
5051
}
5152

53+
&.mat-accent {
54+
.mat-step-icon {
55+
color: mat-color($accent, default-contrast);
56+
}
57+
58+
.mat-step-icon-selected,
59+
.mat-step-icon-state-done,
60+
.mat-step-icon-state-edit {
61+
background-color: mat-color($accent);
62+
color: mat-color($accent, default-contrast);
63+
}
64+
}
65+
66+
&.mat-warn {
67+
.mat-step-icon {
68+
color: mat-color($warn, default-contrast);
69+
}
70+
71+
.mat-step-icon-selected,
72+
.mat-step-icon-state-done,
73+
.mat-step-icon-state-edit {
74+
background-color: mat-color($warn);
75+
color: mat-color($warn, default-contrast);
76+
}
77+
}
78+
5279
.mat-step-icon-state-error {
5380
background-color: transparent;
5481
color: mat-color($warn, text);
@@ -118,7 +145,7 @@
118145
}
119146

120147
.mat-stepper-label-position-bottom .mat-horizontal-stepper-header,
121-
.mat-vertical-stepper-header, {
148+
.mat-vertical-stepper-header {
122149
padding: $vertical-padding $mat-stepper-side-gap;
123150
}
124151

src/material/stepper/step-header.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,34 @@ import {MatStepLabel} from './step-label';
2323
import {MatStepperIntl} from './stepper-intl';
2424
import {MatStepperIconContext} from './stepper-icon';
2525
import {CdkStepHeader, StepState} from '@angular/cdk/stepper';
26+
import {CanColorCtor, mixinColor, CanColor} from '@angular/material/core';
2627

2728

29+
// Boilerplate for applying mixins to MatStepHeader.
30+
/** @docs-private */
31+
class MatStepHeaderBase extends CdkStepHeader {
32+
constructor(elementRef: ElementRef) {
33+
super(elementRef);
34+
}
35+
}
36+
37+
const _MatStepHeaderMixinBase: CanColorCtor & typeof MatStepHeaderBase =
38+
mixinColor(MatStepHeaderBase, 'primary');
39+
2840
@Component({
2941
selector: 'mat-step-header',
3042
templateUrl: 'step-header.html',
3143
styleUrls: ['step-header.css'],
44+
inputs: ['color'],
3245
host: {
3346
'class': 'mat-step-header mat-focus-indicator',
3447
'role': 'tab',
3548
},
3649
encapsulation: ViewEncapsulation.None,
3750
changeDetection: ChangeDetectionStrategy.OnPush,
3851
})
39-
export class MatStepHeader extends CdkStepHeader implements AfterViewInit, OnDestroy {
52+
export class MatStepHeader extends _MatStepHeaderMixinBase implements AfterViewInit, OnDestroy,
53+
CanColor {
4054
private _intlSubscription: Subscription;
4155

4256
/** State of the given step. */

src/material/stepper/stepper-horizontal.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
[optional]="step.optional"
2020
[errorMessage]="step.errorMessage"
2121
[iconOverrides]="_iconOverrides"
22-
[disableRipple]="disableRipple">
22+
[disableRipple]="disableRipple"
23+
[color]="step.color || color">
2324
</mat-step-header>
2425
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
2526
</ng-container>

src/material/stepper/stepper-vertical.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
[optional]="step.optional"
1919
[errorMessage]="step.errorMessage"
2020
[iconOverrides]="_iconOverrides"
21-
[disableRipple]="disableRipple">
21+
[disableRipple]="disableRipple"
22+
[color]="step.color || color">
2223
</mat-step-header>
2324

2425
<div class="mat-vertical-content-container" [class.mat-stepper-vertical-line]="!isLast">

src/material/stepper/stepper.spec.ts

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
Validators,
4242
FormBuilder
4343
} from '@angular/forms';
44-
import {MatRipple} from '@angular/material/core';
44+
import {MatRipple, ThemePalette} from '@angular/material/core';
4545
import {By} from '@angular/platform-browser';
4646
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
4747
import {Observable, Subject} from 'rxjs';
@@ -880,6 +880,43 @@ describe('MatStepper', () => {
880880

881881
expect(headerRipples.every(ripple => ripple.disabled)).toBe(true);
882882
});
883+
884+
it('should be able to set the theme for all steps', () => {
885+
const fixture = createComponent(SimpleMatVerticalStepperApp);
886+
fixture.detectChanges();
887+
888+
const headers =
889+
Array.from<HTMLElement>(fixture.nativeElement.querySelectorAll('.mat-step-header'));
890+
891+
expect(headers.every(element => element.classList.contains('mat-primary'))).toBe(true);
892+
expect(headers.some(element => element.classList.contains('mat-accent'))).toBe(false);
893+
expect(headers.some(element => element.classList.contains('mat-warn'))).toBe(false);
894+
895+
fixture.componentInstance.stepperTheme = 'accent';
896+
fixture.detectChanges();
897+
898+
expect(headers.some(element => element.classList.contains('mat-accent'))).toBe(true);
899+
expect(headers.some(element => element.classList.contains('mat-primary'))).toBe(false);
900+
expect(headers.some(element => element.classList.contains('mat-warn'))).toBe(false);
901+
});
902+
903+
it('should be able to set the theme for a specific step', () => {
904+
const fixture = createComponent(SimpleMatVerticalStepperApp);
905+
fixture.detectChanges();
906+
907+
const headers =
908+
Array.from<HTMLElement>(fixture.nativeElement.querySelectorAll('.mat-step-header'));
909+
910+
expect(headers.every(element => element.classList.contains('mat-primary'))).toBe(true);
911+
912+
fixture.componentInstance.secondStepTheme = 'accent';
913+
fixture.detectChanges();
914+
915+
expect(headers[0].classList.contains('mat-primary')).toBe(true);
916+
expect(headers[1].classList.contains('mat-primary')).toBe(false);
917+
expect(headers[2].classList.contains('mat-primary')).toBe(true);
918+
expect(headers[1].classList.contains('mat-accent')).toBe(true);
919+
});
883920
});
884921

885922
describe('horizontal stepper', () => {
@@ -937,6 +974,43 @@ describe('MatStepper', () => {
937974

938975
expect(headerRipples.every(ripple => ripple.disabled)).toBe(true);
939976
});
977+
978+
it('should be able to set the theme for all steps', () => {
979+
const fixture = createComponent(SimpleMatHorizontalStepperApp);
980+
fixture.detectChanges();
981+
982+
const headers =
983+
Array.from<HTMLElement>(fixture.nativeElement.querySelectorAll('.mat-step-header'));
984+
985+
expect(headers.every(element => element.classList.contains('mat-primary'))).toBe(true);
986+
expect(headers.some(element => element.classList.contains('mat-accent'))).toBe(false);
987+
expect(headers.some(element => element.classList.contains('mat-warn'))).toBe(false);
988+
989+
fixture.componentInstance.stepperTheme = 'accent';
990+
fixture.detectChanges();
991+
992+
expect(headers.some(element => element.classList.contains('mat-accent'))).toBe(true);
993+
expect(headers.some(element => element.classList.contains('mat-primary'))).toBe(false);
994+
expect(headers.some(element => element.classList.contains('mat-warn'))).toBe(false);
995+
});
996+
997+
it('should be able to set the theme for a specific step', () => {
998+
const fixture = createComponent(SimpleMatHorizontalStepperApp);
999+
fixture.detectChanges();
1000+
1001+
const headers =
1002+
Array.from<HTMLElement>(fixture.nativeElement.querySelectorAll('.mat-step-header'));
1003+
1004+
expect(headers.every(element => element.classList.contains('mat-primary'))).toBe(true);
1005+
1006+
fixture.componentInstance.secondStepTheme = 'accent';
1007+
fixture.detectChanges();
1008+
1009+
expect(headers[0].classList.contains('mat-primary')).toBe(true);
1010+
expect(headers[1].classList.contains('mat-primary')).toBe(false);
1011+
expect(headers[2].classList.contains('mat-primary')).toBe(true);
1012+
expect(headers[1].classList.contains('mat-accent')).toBe(true);
1013+
});
9401014
});
9411015

9421016
describe('linear stepper with valid step', () => {
@@ -1395,7 +1469,7 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
13951469

13961470
@Component({
13971471
template: `
1398-
<mat-horizontal-stepper [disableRipple]="disableRipple">
1472+
<mat-horizontal-stepper [disableRipple]="disableRipple" [color]="stepperTheme">
13991473
<mat-step>
14001474
<ng-template matStepLabel>Step 1</ng-template>
14011475
Content 1
@@ -1404,7 +1478,7 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
14041478
<button mat-button matStepperNext>Next</button>
14051479
</div>
14061480
</mat-step>
1407-
<mat-step>
1481+
<mat-step [color]="secondStepTheme">
14081482
<ng-template matStepLabel>Step 2</ng-template>
14091483
Content 2
14101484
<div>
@@ -1425,11 +1499,13 @@ class MatHorizontalStepperWithErrorsApp implements OnInit {
14251499
class SimpleMatHorizontalStepperApp {
14261500
inputLabel = 'Step 3';
14271501
disableRipple = false;
1502+
stepperTheme: ThemePalette;
1503+
secondStepTheme: ThemePalette;
14281504
}
14291505

14301506
@Component({
14311507
template: `
1432-
<mat-vertical-stepper [disableRipple]="disableRipple">
1508+
<mat-vertical-stepper [disableRipple]="disableRipple" [color]="stepperTheme">
14331509
<mat-step>
14341510
<ng-template matStepLabel>Step 1</ng-template>
14351511
Content 1
@@ -1438,7 +1514,7 @@ class SimpleMatHorizontalStepperApp {
14381514
<button mat-button matStepperNext>Next</button>
14391515
</div>
14401516
</mat-step>
1441-
<mat-step *ngIf="showStepTwo">
1517+
<mat-step *ngIf="showStepTwo" [color]="secondStepTheme">
14421518
<ng-template matStepLabel>Step 2</ng-template>
14431519
Content 2
14441520
<div>
@@ -1460,6 +1536,8 @@ class SimpleMatVerticalStepperApp {
14601536
inputLabel = 'Step 3';
14611537
showStepTwo = true;
14621538
disableRipple = false;
1539+
stepperTheme: ThemePalette;
1540+
secondStepTheme: ThemePalette;
14631541
}
14641542

14651543
@Component({

0 commit comments

Comments
 (0)