Skip to content

Commit adc251c

Browse files
crisbetotinayuangao
authored andcommitted
feat(stepper): allow for header icons to be customized (#7482)
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 5d453b9 commit adc251c

File tree

10 files changed

+159
-16
lines changed

10 files changed

+159
-16
lines changed

src/lib/stepper/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './stepper-button';
1313
export * from './step-header';
1414
export * from './stepper-intl';
1515
export * from './stepper-animations';
16+
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(): number { 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
@@ -121,6 +121,25 @@ By default, the `completed` attribute of a step returns `true` if the step is va
121121
linear stepper) and the user has interacted with the step. The user, however, can also override
122122
this default `completed` behavior by setting the `completed` attribute as needed.
123123

124+
#### Overriding icons
125+
By default, the step headers will use the `create` and `done` icons from the Material design icon
126+
set via `<mat-icon>` elements. If you want to provide a different set of icons, you can do so
127+
by placing a `matStepperIcon` for each of the icons that you want to override:
128+
129+
```html
130+
<mat-vertical-stepper>
131+
<ng-template matStepperIcon="edit">
132+
<custom-icon>edit</custom-icon>
133+
</ng-template>
134+
135+
<ng-template matStepperIcon="done">
136+
<custom-icon>done</custom-icon>
137+
</ng-template>
138+
139+
<!-- Stepper steps go here -->
140+
</mat-vertical-stepper>
141+
```
142+
124143
### Keyboard interaction
125144
- <kbd>LEFT_ARROW</kbd>: Focuses the previous step header
126145
- <kbd>RIGHT_ARROW</kbd>: Focuses the next step header

src/lib/stepper/stepper.spec.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ describe('MatHorizontalStepper', () => {
3030
SimplePreselectedMatHorizontalStepperApp,
3131
LinearMatHorizontalStepperApp,
3232
SimpleStepperWithoutStepControl,
33-
SimpleStepperWithStepControlAndCompletedBinding
33+
SimpleStepperWithStepControlAndCompletedBinding,
34+
IconOverridesStepper,
3435
],
3536
providers: [
3637
{provide: Directionality, useFactory: () => ({value: dir})}
@@ -174,6 +175,41 @@ describe('MatHorizontalStepper', () => {
174175
});
175176
});
176177

178+
describe('icon overrides', () => {
179+
let fixture: ComponentFixture<IconOverridesStepper>;
180+
181+
beforeEach(() => {
182+
fixture = TestBed.createComponent(IconOverridesStepper);
183+
fixture.detectChanges();
184+
});
185+
186+
it('should allow for the `edit` icon to be overridden', () => {
187+
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
188+
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;
189+
190+
stepperComponent._steps.toArray()[0].editable = true;
191+
stepperComponent.next();
192+
fixture.detectChanges();
193+
194+
const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');
195+
196+
expect(header.textContent).toContain('Custom edit');
197+
});
198+
199+
it('should allow for the `done` icon to be overridden', () => {
200+
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
201+
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;
202+
203+
stepperComponent._steps.toArray()[0].editable = false;
204+
stepperComponent.next();
205+
fixture.detectChanges();
206+
207+
const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');
208+
209+
expect(header.textContent).toContain('Custom done');
210+
});
211+
});
212+
177213
describe('linear horizontal stepper', () => {
178214
let fixture: ComponentFixture<LinearMatHorizontalStepperApp>;
179215
let testComponent: LinearMatHorizontalStepperApp;
@@ -1168,3 +1204,17 @@ class SimpleStepperWithStepControlAndCompletedBinding {
11681204
{label: 'Three', completed: false, control: new FormControl()}
11691205
];
11701206
}
1207+
1208+
@Component({
1209+
template: `
1210+
<mat-horizontal-stepper>
1211+
<ng-template matStepperIcon="edit">Custom edit</ng-template>
1212+
<ng-template matStepperIcon="done">Custom done</ng-template>
1213+
1214+
<mat-step>Content 1</mat-step>
1215+
<mat-step>Content 2</mat-step>
1216+
<mat-step>Content 3</mat-step>
1217+
</mat-horizontal-stepper>
1218+
`
1219+
})
1220+
class IconOverridesStepper {}

src/lib/stepper/stepper.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@ import {
2424
ChangeDetectorRef,
2525
ChangeDetectionStrategy,
2626
Optional,
27+
TemplateRef,
2728
} from '@angular/core';
2829
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
2930
import {ErrorStateMatcher} from '@angular/material/core';
3031
import {MatStepHeader} from './step-header';
3132
import {MatStepLabel} from './step-label';
3233
import {takeUntil} from 'rxjs/operators/takeUntil';
3334
import {matStepperAnimations} from './stepper-animations';
35+
import {MatStepperIcon} from './stepper-icon';
36+
37+
/** Workaround for https://github.com/angular/angular/issues/17849 */
38+
export const _MatStep = CdkStep;
39+
export const _MatStepper = CdkStepper;
3440

3541
@Component({
3642
moduleId: module.id,
@@ -64,6 +70,7 @@ export class MatStep extends CdkStep implements ErrorStateMatcher {
6470
}
6571
}
6672

73+
6774
@Directive({
6875
selector: '[matStepper]'
6976
})
@@ -74,7 +81,25 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
7481
/** Steps that the stepper holds. */
7582
@ContentChildren(MatStep) _steps: QueryList<MatStep>;
7683

84+
/** Custom icon overrides passed in by the consumer. */
85+
@ContentChildren(MatStepperIcon) _icons: QueryList<MatStepperIcon>;
86+
87+
/** Consumer-specified template-refs to be used to override the header icons. */
88+
_iconOverrides: {[key: string]: TemplateRef<any>} = {};
89+
7790
ngAfterContentInit() {
91+
const icons = this._icons.toArray();
92+
const editOverride = icons.find(icon => icon.name === 'edit');
93+
const doneOverride = icons.find(icon => icon.name === 'done');
94+
95+
if (editOverride) {
96+
this._iconOverrides.edit = editOverride.templateRef;
97+
}
98+
99+
if (doneOverride) {
100+
this._iconOverrides.done = doneOverride.templateRef;
101+
}
102+
78103
// Mark the component for change detection whenever the content children query changes
79104
this._steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => this._stateChanged());
80105
}

0 commit comments

Comments
 (0)