Skip to content

Commit 8fd1dd0

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 541a95e commit 8fd1dd0

File tree

10 files changed

+160
-17
lines changed

10 files changed

+160
-17
lines changed

src/lib/stepper/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from './stepper';
1212
export * from './stepper-button';
1313
export * from './step-header';
1414
export * from './stepper-intl';
15+
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: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Input,
1717
OnDestroy,
1818
ViewEncapsulation,
19+
TemplateRef,
1920
} from '@angular/core';
2021
import {Subscription} from 'rxjs/Subscription';
2122
import {MatStepLabel} from './step-label';
@@ -38,12 +39,15 @@ import {MatStepperIntl} from './stepper-intl';
3839
export class MatStepHeader implements OnDestroy {
3940
private _intlSubscription: Subscription;
4041

41-
/** Icon for the given step. */
42-
@Input() icon: string;
42+
/** State of the given step. */
43+
@Input() state: string;
4344

4445
/** Label of the given step. */
4546
@Input() label: MatStepLabel | string;
4647

48+
/** Overrides for the header icons, passed in via the stepper. */
49+
@Input() iconOverrides: {[key: string]: TemplateRef<any>};
50+
4751
/** Index of the given step. */
4852
@Input()
4953
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 || !linear"
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 LLC 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: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import {MatCommonModule, MatRippleModule, ErrorStateMatcher} from '@angular/mate
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';
2120
import {MatStepperIntl} from './stepper-intl';
21+
import {MatStepperIcon} from './stepper-icon';
22+
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
2223

2324

2425
@NgModule({
@@ -41,10 +42,20 @@ import {MatStepperIntl} from './stepper-intl';
4142
MatStepper,
4243
MatStepperNext,
4344
MatStepperPrevious,
44-
MatStepHeader
45+
MatStepHeader,
46+
MatStepperIcon,
47+
],
48+
declarations: [
49+
MatHorizontalStepper,
50+
MatVerticalStepper,
51+
MatStep,
52+
MatStepLabel,
53+
MatStepper,
54+
MatStepperNext,
55+
MatStepperPrevious,
56+
MatStepHeader,
57+
MatStepperIcon,
4558
],
46-
declarations: [MatHorizontalStepper, MatVerticalStepper, MatStep, MatStepLabel, MatStepper,
47-
MatStepperNext, MatStepperPrevious, MatStepHeader],
4859
providers: [MatStepperIntl, ErrorStateMatcher],
4960
})
5061
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 || !linear"
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,25 @@ By default, the `completed` attribute of a step returns `true` if the step is va
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 `<mat-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

src/lib/stepper/stepper.spec.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ describe('MatHorizontalStepper', () => {
2222
declarations: [
2323
SimpleMatHorizontalStepperApp,
2424
SimplePreselectedMatHorizontalStepperApp,
25-
LinearMatHorizontalStepperApp
25+
LinearMatHorizontalStepperApp,
26+
IconOverridesStepper,
2627
],
2728
providers: [
2829
{provide: Directionality, useFactory: () => ({value: dir})}
@@ -133,6 +134,41 @@ describe('MatHorizontalStepper', () => {
133134
});
134135
});
135136

137+
describe('icon overrides', () => {
138+
let fixture: ComponentFixture<IconOverridesStepper>;
139+
140+
beforeEach(() => {
141+
fixture = TestBed.createComponent(IconOverridesStepper);
142+
fixture.detectChanges();
143+
});
144+
145+
it('should allow for the `edit` icon to be overridden', () => {
146+
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
147+
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;
148+
149+
stepperComponent._steps.toArray()[0].editable = true;
150+
stepperComponent.next();
151+
fixture.detectChanges();
152+
153+
const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');
154+
155+
expect(header.textContent).toContain('Custom edit');
156+
});
157+
158+
it('should allow for the `done` icon to be overridden', () => {
159+
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
160+
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;
161+
162+
stepperComponent._steps.toArray()[0].editable = false;
163+
stepperComponent.next();
164+
fixture.detectChanges();
165+
166+
const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');
167+
168+
expect(header.textContent).toContain('Custom done');
169+
});
170+
});
171+
136172
describe('linear horizontal stepper', () => {
137173
let fixture: ComponentFixture<LinearMatHorizontalStepperApp>;
138174
let testComponent: LinearMatHorizontalStepperApp;
@@ -888,3 +924,18 @@ class LinearMatVerticalStepperApp {
888924
class SimplePreselectedMatHorizontalStepperApp {
889925
index = 0;
890926
}
927+
928+
@Component({
929+
template: `
930+
<mat-horizontal-stepper>
931+
<ng-template matStepperIcon="edit">Custom edit</ng-template>
932+
<ng-template matStepperIcon="done">Custom done</ng-template>
933+
934+
<mat-step>Content 1</mat-step>
935+
<mat-step>Content 2</mat-step>
936+
<mat-step>Content 3</mat-step>
937+
</mat-horizontal-stepper>
938+
`
939+
})
940+
class IconOverridesStepper {}
941+

src/lib/stepper/stepper.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ import {
2121
ViewChildren,
2222
ViewEncapsulation,
2323
ChangeDetectionStrategy,
24+
TemplateRef,
25+
AfterContentInit,
2426
} from '@angular/core';
2527
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
2628
import {ErrorStateMatcher} from '@angular/material/core';
2729
import {MatStepHeader} from './step-header';
2830
import {MatStepLabel} from './step-label';
31+
import {MatStepperIcon} from './stepper-icon';
2932

3033
/** Workaround for https://github.com/angular/angular/issues/17849 */
3134
export const _MatStep = CdkStep;
@@ -63,15 +66,36 @@ export class MatStep extends _MatStep implements ErrorStateMatcher {
6366
}
6467
}
6568

69+
6670
@Directive({
6771
selector: '[matStepper]'
6872
})
69-
export class MatStepper extends _MatStepper {
73+
export class MatStepper extends _MatStepper implements AfterContentInit {
7074
/** The list of step headers of the steps in the stepper. */
7175
@ViewChildren(MatStepHeader, {read: ElementRef}) _stepHeader: QueryList<ElementRef>;
7276

7377
/** Steps that the stepper holds. */
7478
@ContentChildren(MatStep) _steps: QueryList<MatStep>;
79+
80+
/** Custom icon overrides passed in by the consumer. */
81+
@ContentChildren(MatStepperIcon) _icons: QueryList<MatStepperIcon>;
82+
83+
/** Consumer-specified template-refs to be used to override the header icons. */
84+
_iconOverrides: {[key: string]: TemplateRef<any>} = {};
85+
86+
ngAfterContentInit() {
87+
const icons = this._icons.toArray();
88+
const editOverride = icons.find(icon => icon.name === 'edit');
89+
const doneOverride = icons.find(icon => icon.name === 'done');
90+
91+
if (editOverride) {
92+
this._iconOverrides.edit = editOverride.templateRef;
93+
}
94+
95+
if (doneOverride) {
96+
this._iconOverrides.done = doneOverride.templateRef;
97+
}
98+
}
7599
}
76100

77101
@Component({

0 commit comments

Comments
 (0)