Skip to content

Commit 585023a

Browse files
committed
feat(stepper): allow for header icons to be customized
Currently users are locked into using the Material `create` and `done` icon for the step headers. These changes add the ability to customize the icons by providing an `ng-template` with an override. Fixes #7384.
1 parent ac70420 commit 585023a

File tree

10 files changed

+167
-28
lines changed

10 files changed

+167
-28
lines changed

src/lib/stepper/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ export * from './step-label';
1111
export * from './stepper';
1212
export * from './stepper-button';
1313
export * from './step-header';
14-
14+
export * from './stepper-icon';

src/lib/stepper/step-header.html

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
<div class="mat-step-header-ripple" mat-ripple [matRippleTrigger]="_getHostElement()"></div>
2-
<div [class.mat-step-icon]="icon !== 'number' || selected"
3-
[class.mat-step-icon-not-touched]="icon == 'number' && !selected"
4-
[ngSwitch]="icon">
2+
<div [class.mat-step-icon]="state !== 'number' || selected"
3+
[class.mat-step-icon-not-touched]="state == 'number' && !selected"
4+
[ngSwitch]="state">
5+
56
<span *ngSwitchCase="'number'">{{index + 1}}</span>
6-
<mat-icon *ngSwitchCase="'edit'">create</mat-icon>
7-
<mat-icon *ngSwitchCase="'done'">done</mat-icon>
7+
8+
<ng-container *ngSwitchCase="'edit'" [ngSwitch]="!!(iconOverrides && iconOverrides.edit)">
9+
<ng-container *ngSwitchCase="true" [ngTemplateOutlet]="iconOverrides.edit"></ng-container>
10+
<mat-icon *ngSwitchDefault>create</mat-icon>
11+
</ng-container>
12+
13+
<ng-container *ngSwitchCase="'done'" [ngSwitch]="!!(iconOverrides && iconOverrides.done)">
14+
<ng-container *ngSwitchCase="true" [ngTemplateOutlet]="iconOverrides.done"></ng-container>
15+
<mat-icon *ngSwitchDefault>done</mat-icon>
16+
</ng-container>
817
</div>
918
<div class="mat-step-label"
1019
[class.mat-step-label-active]="active"

src/lib/stepper/step-header.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@
88

99
import {FocusMonitor} from '@angular/cdk/a11y';
1010
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
11-
import {Component, Input, ViewEncapsulation, ElementRef, OnDestroy, Renderer2} from '@angular/core';
11+
import {
12+
Component,
13+
Input,
14+
ViewEncapsulation,
15+
ElementRef,
16+
OnDestroy,
17+
Renderer2,
18+
TemplateRef,
19+
} from '@angular/core';
1220
import {MatStepLabel} from './step-label';
1321

1422

@@ -25,12 +33,15 @@ import {MatStepLabel} from './step-label';
2533
preserveWhitespaces: false,
2634
})
2735
export class MatStepHeader implements OnDestroy {
28-
/** Icon for the given step. */
29-
@Input() icon: string;
36+
/** State of the given step. */
37+
@Input() state: string;
3038

3139
/** Label of the given step. */
3240
@Input() label: MatStepLabel | string;
3341

42+
/** Overrides for the header icons, passed in via the stepper. */
43+
@Input() iconOverrides: {[key: string]: TemplateRef<any>};
44+
3445
/** Index of the given step. */
3546
@Input()
3647
get index() { return this._index; }

src/lib/stepper/stepper-horizontal.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
[attr.aria-controls]="_getStepContentId(i)"
99
[attr.aria-selected]="selectedIndex == i"
1010
[index]="i"
11-
[icon]="_getIndicatorType(i)"
11+
[state]="_getIndicatorType(i)"
1212
[label]="step.stepLabel || step.label"
1313
[selected]="selectedIndex === i"
1414
[active]="step.completed || selectedIndex === i"
15-
[optional]="step.optional">
15+
[optional]="step.optional"
16+
[iconOverrides]="_iconOverrides">
1617
</mat-step-header>
1718
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
1819
</ng-container>

src/lib/stepper/stepper-icon.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Directive, Input, TemplateRef} from '@angular/core';
10+
11+
/**
12+
* Template to be used to override the icons inside the step header.
13+
*/
14+
@Directive({
15+
selector: 'ng-template[matStepperIcon]',
16+
})
17+
export class MatStepperIcon {
18+
/** Name of the icon to be overridden. */
19+
@Input('matStepperIcon') name: 'edit' | 'done';
20+
21+
constructor(public templateRef: TemplateRef<any>) { }
22+
}

src/lib/stepper/stepper-module.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import {MatCommonModule, MatRippleModule} from '@angular/material/core';
1616
import {MatIconModule} from '@angular/material/icon';
1717
import {MatStepHeader} from './step-header';
1818
import {MatStepLabel} from './step-label';
19-
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
2019
import {MatStepperNext, MatStepperPrevious} from './stepper-button';
20+
import {MatStepperIcon} from './stepper-icon';
21+
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
2122

2223

2324
@NgModule({
@@ -40,9 +41,10 @@ import {MatStepperNext, MatStepperPrevious} from './stepper-button';
4041
MatStepper,
4142
MatStepperNext,
4243
MatStepperPrevious,
43-
MatStepHeader
44+
MatStepHeader,
45+
MatStepperIcon,
4446
],
4547
declarations: [MatHorizontalStepper, MatVerticalStepper, MatStep, MatStepLabel, MatStepper,
46-
MatStepperNext, MatStepperPrevious, MatStepHeader],
48+
MatStepperNext, MatStepperPrevious, MatStepHeader, MatStepperIcon],
4749
})
4850
export class MatStepperModule {}

src/lib/stepper/stepper-vertical.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
[attr.aria-controls]="_getStepContentId(i)"
88
[attr.aria-selected]="selectedIndex === i"
99
[index]="i"
10-
[icon]="_getIndicatorType(i)"
10+
[state]="_getIndicatorType(i)"
1111
[label]="step.stepLabel || step.label"
1212
[selected]="selectedIndex === i"
1313
[active]="step.completed || selectedIndex === i"
14-
[optional]="step.optional">
14+
[optional]="step.optional"
15+
[iconOverrides]="_iconOverrides">
1516
</mat-step-header>
1617

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

src/lib/stepper/stepper.md

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ that drives a stepped workflow. Material stepper extends the CDK stepper and has
77
styling.
88

99
### Stepper variants
10-
There are two stepper components: `mat-horizontal-stepper` and `mat-vertical-stepper`. They
11-
can be used the same way. The only difference is the orientation of stepper.
10+
There are two stepper components: `mat-horizontal-stepper` and `mat-vertical-stepper`. They
11+
can be used the same way. The only difference is the orientation of stepper.
1212
`mat-horizontal-stepper` selector can be used to create a horizontal stepper, and
1313
`mat-vertical-stepper` can be used to create a vertical stepper. `mat-step` components need to be
1414
placed inside either one of the two stepper components.
@@ -26,7 +26,7 @@ If a step's label is only text, then the `label` attribute can be used.
2626
</mat-vertical-stepper>
2727
```
2828

29-
For more complex labels, add a template with the `matStepLabel` directive inside the
29+
For more complex labels, add a template with the `matStepLabel` directive inside the
3030
`mat-step`.
3131
```html
3232
<mat-vertical-stepper>
@@ -49,22 +49,22 @@ There are two button directives to support navigation between different steps:
4949
<button mat-button matStepperNext>Next</button>
5050
</div>
5151
</mat-step>
52-
</mat-horizontal-stepper>
52+
</mat-horizontal-stepper>
5353
```
5454

5555
### Linear stepper
5656
The `linear` attribute can be set on `mat-horizontal-stepper` and `mat-vertical-stepper` to create
5757
a linear stepper that requires the user to complete previous steps before proceeding
5858
to following steps. For each `mat-step`, the `stepControl` attribute can be set to the top level
59-
`AbstractControl` that is used to check the validity of the step.
59+
`AbstractControl` that is used to check the validity of the step.
6060

6161
There are two possible approaches. One is using a single form for stepper, and the other is
6262
using a different form for each step.
6363

6464
#### Using a single form
6565
When using a single form for the stepper, `matStepperPrevious` and `matStepperNext` have to be
6666
set to `type="button"` in order to prevent submission of the form before all steps
67-
are completed.
67+
are completed.
6868

6969
```html
7070
<form [formGroup]="formGroup">
@@ -83,7 +83,7 @@ are completed.
8383
</div>
8484
</mat-step>
8585
...
86-
</mat-horizontal-stepper>
86+
</mat-horizontal-stepper>
8787
</form>
8888
```
8989

@@ -106,17 +106,36 @@ are completed.
106106

107107
#### Optional step
108108
If completion of a step in linear stepper is not required, then the `optional` attribute can be set
109-
on `mat-step`.
109+
on `mat-step`.
110110

111111
#### Editable step
112112
By default, steps are editable, which means users can return to previously completed steps and
113-
edit their responses. `editable="true"` can be set on `mat-step` to change the default.
113+
edit their responses. `editable="true"` can be set on `mat-step` to change the default
114114

115115
#### Completed step
116116
By default, the `completed` attribute of a step returns `true` if the step is valid (in case of
117117
linear stepper) and the user has interacted with the step. The user, however, can also override
118118
this default `completed` behavior by setting the `completed` attribute as needed.
119119

120+
#### Overriding icons
121+
By default, the step headers will use the `create` and `done` icons from the Material design icon
122+
set via `<md-icon>` elements. If you want to provide a different set of icons, you can do so
123+
by placing a `matStepperIcon` for each of the icons that you want to override:
124+
125+
```html
126+
<mat-vertical-stepper>
127+
<ng-template matStepperIcon="edit">
128+
<custom-icon>edit</custom-icon>
129+
</ng-template>
130+
131+
<ng-template matStepperIcon="done">
132+
<custom-icon>done</custom-icon>
133+
</ng-template>
134+
135+
<!-- Stepper steps go here -->
136+
</mat-vertical-stepper>
137+
```
138+
120139
### Keyboard interaction
121140
- <kbd>LEFT_ARROW</kbd>: Focuses the previous step header
122141
- <kbd>RIGHT_ARROW</kbd>: Focuses the next step header
@@ -131,4 +150,4 @@ is given `role="tab"`, and the content that can be expanded upon selection is gi
131150
`role="tabpanel"`. `aria-selected` attribute of step header and `aria-expanded` attribute of
132151
step content is automatically set based on step selection change.
133152

134-
The stepper and each step should be given a meaningful label via `aria-label` or `aria-labelledby`.
153+
The stepper and each step should be given a meaningful label via `aria-label` or `aria-labelledby`.

src/lib/stepper/stepper.spec.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ describe('MatHorizontalStepper', () => {
2020
imports: [MatStepperModule, NoopAnimationsModule, ReactiveFormsModule],
2121
declarations: [
2222
SimpleMatHorizontalStepperApp,
23-
LinearMatHorizontalStepperApp
23+
LinearMatHorizontalStepperApp,
24+
IconOverridesStepper,
2425
],
2526
providers: [
2627
{provide: Directionality, useFactory: () => ({value: dir})}
@@ -116,6 +117,41 @@ describe('MatHorizontalStepper', () => {
116117
});
117118
});
118119

120+
describe('icon overrides', () => {
121+
let fixture: ComponentFixture<IconOverridesStepper>;
122+
123+
beforeEach(() => {
124+
fixture = TestBed.createComponent(IconOverridesStepper);
125+
fixture.detectChanges();
126+
});
127+
128+
it('should allow for the `edit` icon to be overridden', () => {
129+
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
130+
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;
131+
132+
stepperComponent._steps.toArray()[0].editable = true;
133+
stepperComponent.next();
134+
fixture.detectChanges();
135+
136+
const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');
137+
138+
expect(header.textContent).toContain('Custom edit');
139+
});
140+
141+
it('should allow for the `done` icon to be overridden', () => {
142+
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
143+
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;
144+
145+
stepperComponent._steps.toArray()[0].editable = false;
146+
stepperComponent.next();
147+
fixture.detectChanges();
148+
149+
const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');
150+
151+
expect(header.textContent).toContain('Custom done');
152+
});
153+
});
154+
119155
describe('linear horizontal stepper', () => {
120156
let fixture: ComponentFixture<LinearMatHorizontalStepperApp>;
121157
let testComponent: LinearMatHorizontalStepperApp;
@@ -858,3 +894,17 @@ class LinearMatVerticalStepperApp {
858894
});
859895
}
860896
}
897+
898+
@Component({
899+
template: `
900+
<mat-horizontal-stepper>
901+
<ng-template matStepperIcon="edit">Custom edit</ng-template>
902+
<ng-template matStepperIcon="done">Custom done</ng-template>
903+
904+
<mat-step>Content 1</mat-step>
905+
<mat-step>Content 2</mat-step>
906+
<mat-step>Content 3</mat-step>
907+
</mat-horizontal-stepper>
908+
`
909+
})
910+
class IconOverridesStepper {}

src/lib/stepper/stepper.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
SkipSelf,
2222
ViewChildren,
2323
ViewEncapsulation,
24+
TemplateRef,
25+
AfterContentInit,
2426
} from '@angular/core';
2527
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
2628
import {
@@ -31,6 +33,7 @@ import {
3133
} from '@angular/material/core';
3234
import {MatStepHeader} from './step-header';
3335
import {MatStepLabel} from './step-label';
36+
import {MatStepperIcon} from './stepper-icon';
3437

3538
/** Workaround for https://github.com/angular/angular/issues/17849 */
3639
export const _MatStep = CdkStep;
@@ -75,15 +78,36 @@ export class MatStep extends _MatStep implements ErrorOptions {
7578
}
7679
}
7780

81+
7882
@Directive({
7983
selector: '[matStepper]'
8084
})
81-
export class MatStepper extends _MatStepper {
85+
export class MatStepper extends _MatStepper implements AfterContentInit {
8286
/** The list of step headers of the steps in the stepper. */
8387
@ViewChildren(MatStepHeader, {read: ElementRef}) _stepHeader: QueryList<ElementRef>;
8488

8589
/** Steps that the stepper holds. */
8690
@ContentChildren(MatStep) _steps: QueryList<MatStep>;
91+
92+
/** Custom icon overrides passed in by the consumer. */
93+
@ContentChildren(MatStepperIcon) _icons: QueryList<MatStepperIcon>;
94+
95+
/** Consumer-specified template-refs to be used to override the header icons. */
96+
_iconOverrides: {[key: string]: TemplateRef<any>} = {};
97+
98+
ngAfterContentInit() {
99+
const icons = this._icons.toArray();
100+
const editOverride = icons.find(icon => icon.name === 'edit');
101+
const doneOverride = icons.find(icon => icon.name === 'done');
102+
103+
if (editOverride) {
104+
this._iconOverrides.edit = editOverride.templateRef;
105+
}
106+
107+
if (doneOverride) {
108+
this._iconOverrides.done = doneOverride.templateRef;
109+
}
110+
}
87111
}
88112

89113
@Component({

0 commit comments

Comments
 (0)