Skip to content

feat(stepper): allow for header icons to be customized #7482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib/stepper/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './stepper-button';
export * from './step-header';
export * from './stepper-intl';
export * from './stepper-animations';
export * from './stepper-icon';
19 changes: 14 additions & 5 deletions src/lib/stepper/step-header.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
<div class="mat-step-header-ripple" mat-ripple [matRippleTrigger]="_getHostElement()"></div>
<div [class.mat-step-icon]="icon !== 'number' || selected"
[class.mat-step-icon-not-touched]="icon == 'number' && !selected"
[ngSwitch]="icon">
<div [class.mat-step-icon]="state !== 'number' || selected"
[class.mat-step-icon-not-touched]="state == 'number' && !selected"
[ngSwitch]="state">

<span *ngSwitchCase="'number'">{{index + 1}}</span>
<mat-icon *ngSwitchCase="'edit'">create</mat-icon>
<mat-icon *ngSwitchCase="'done'">done</mat-icon>

<ng-container *ngSwitchCase="'edit'" [ngSwitch]="!!(iconOverrides && iconOverrides.edit)">
<ng-container *ngSwitchCase="true" [ngTemplateOutlet]="iconOverrides.edit"></ng-container>
<mat-icon *ngSwitchDefault>create</mat-icon>
</ng-container>

<ng-container *ngSwitchCase="'done'" [ngSwitch]="!!(iconOverrides && iconOverrides.done)">
<ng-container *ngSwitchCase="true" [ngTemplateOutlet]="iconOverrides.done"></ng-container>
<mat-icon *ngSwitchDefault>done</mat-icon>
</ng-container>
</div>
<div class="mat-step-label"
[class.mat-step-label-active]="active"
Expand Down
8 changes: 6 additions & 2 deletions src/lib/stepper/step-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Input,
OnDestroy,
ViewEncapsulation,
TemplateRef,
} from '@angular/core';
import {Subscription} from 'rxjs/Subscription';
import {MatStepLabel} from './step-label';
Expand All @@ -38,12 +39,15 @@ import {MatStepperIntl} from './stepper-intl';
export class MatStepHeader implements OnDestroy {
private _intlSubscription: Subscription;

/** Icon for the given step. */
@Input() icon: string;
/** State of the given step. */
@Input() state: string;

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

/** Overrides for the header icons, passed in via the stepper. */
@Input() iconOverrides: {[key: string]: TemplateRef<any>};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not a map?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't need to be. It's just a string->TemplateRef map.


/** Index of the given step. */
@Input()
get index() { return this._index; }
Expand Down
5 changes: 3 additions & 2 deletions src/lib/stepper/stepper-horizontal.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
[attr.aria-controls]="_getStepContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[index]="i"
[icon]="_getIndicatorType(i)"
[state]="_getIndicatorType(i)"
[label]="step.stepLabel || step.label"
[selected]="selectedIndex === i"
[active]="step.completed || selectedIndex === i || !linear"
[optional]="step.optional">
[optional]="step.optional"
[iconOverrides]="_iconOverrides">
</mat-step-header>
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
</ng-container>
Expand Down
22 changes: 22 additions & 0 deletions src/lib/stepper/stepper-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, Input, TemplateRef} from '@angular/core';

/**
* Template to be used to override the icons inside the step header.
*/
@Directive({
selector: 'ng-template[matStepperIcon]',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with

<mat-icon *matStepperIcon>

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I haven't tried it but I suppose.

})
export class MatStepperIcon {
/** Name of the icon to be overridden. */
@Input('matStepperIcon') name: 'edit' | 'done';

constructor(public templateRef: TemplateRef<any>) { }
}
19 changes: 15 additions & 4 deletions src/lib/stepper/stepper-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import {MatCommonModule, MatRippleModule, ErrorStateMatcher} from '@angular/mate
import {MatIconModule} from '@angular/material/icon';
import {MatStepHeader} from './step-header';
import {MatStepLabel} from './step-label';
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
import {MatStepperNext, MatStepperPrevious} from './stepper-button';
import {MatStepperIntl} from './stepper-intl';
import {MatStepperIcon} from './stepper-icon';
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';


@NgModule({
Expand All @@ -41,10 +42,20 @@ import {MatStepperIntl} from './stepper-intl';
MatStepper,
MatStepperNext,
MatStepperPrevious,
MatStepHeader
MatStepHeader,
MatStepperIcon,
],
declarations: [
MatHorizontalStepper,
MatVerticalStepper,
MatStep,
MatStepLabel,
MatStepper,
MatStepperNext,
MatStepperPrevious,
MatStepHeader,
MatStepperIcon,
],
declarations: [MatHorizontalStepper, MatVerticalStepper, MatStep, MatStepLabel, MatStepper,
MatStepperNext, MatStepperPrevious, MatStepHeader],
providers: [MatStepperIntl, ErrorStateMatcher],
})
export class MatStepperModule {}
5 changes: 3 additions & 2 deletions src/lib/stepper/stepper-vertical.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
[attr.aria-controls]="_getStepContentId(i)"
[attr.aria-selected]="selectedIndex === i"
[index]="i"
[icon]="_getIndicatorType(i)"
[state]="_getIndicatorType(i)"
[label]="step.stepLabel || step.label"
[selected]="selectedIndex === i"
[active]="step.completed || selectedIndex === i || !linear"
[optional]="step.optional">
[optional]="step.optional"
[iconOverrides]="_iconOverrides">
</mat-step-header>

<div class="mat-vertical-content-container" [class.mat-stepper-vertical-line]="!isLast">
Expand Down
19 changes: 19 additions & 0 deletions src/lib/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,25 @@ By default, the `completed` attribute of a step returns `true` if the step is va
linear stepper) and the user has interacted with the step. The user, however, can also override
this default `completed` behavior by setting the `completed` attribute as needed.

#### Overriding icons
By default, the step headers will use the `create` and `done` icons from the Material design icon
set via `<mat-icon>` elements. If you want to provide a different set of icons, you can do so
by placing a `matStepperIcon` for each of the icons that you want to override:

```html
<mat-vertical-stepper>
<ng-template matStepperIcon="edit">
<custom-icon>edit</custom-icon>
</ng-template>

<ng-template matStepperIcon="done">
<custom-icon>done</custom-icon>
</ng-template>

<!-- Stepper steps go here -->
</mat-vertical-stepper>
```

### Keyboard interaction
- <kbd>LEFT_ARROW</kbd>: Focuses the previous step header
- <kbd>RIGHT_ARROW</kbd>: Focuses the next step header
Expand Down
52 changes: 51 additions & 1 deletion src/lib/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ describe('MatHorizontalStepper', () => {
SimplePreselectedMatHorizontalStepperApp,
LinearMatHorizontalStepperApp,
SimpleStepperWithoutStepControl,
SimpleStepperWithStepControlAndCompletedBinding
SimpleStepperWithStepControlAndCompletedBinding,
IconOverridesStepper,
],
providers: [
{provide: Directionality, useFactory: () => ({value: dir})}
Expand Down Expand Up @@ -146,6 +147,41 @@ describe('MatHorizontalStepper', () => {
});
});

describe('icon overrides', () => {
let fixture: ComponentFixture<IconOverridesStepper>;

beforeEach(() => {
fixture = TestBed.createComponent(IconOverridesStepper);
fixture.detectChanges();
});

it('should allow for the `edit` icon to be overridden', () => {
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;

stepperComponent._steps.toArray()[0].editable = true;
stepperComponent.next();
fixture.detectChanges();

const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');

expect(header.textContent).toContain('Custom edit');
});

it('should allow for the `done` icon to be overridden', () => {
const stepperDebugElement = fixture.debugElement.query(By.directive(MatStepper));
const stepperComponent: MatStepper = stepperDebugElement.componentInstance;

stepperComponent._steps.toArray()[0].editable = false;
stepperComponent.next();
fixture.detectChanges();

const header = stepperDebugElement.nativeElement.querySelector('mat-step-header');

expect(header.textContent).toContain('Custom done');
});
});

describe('linear horizontal stepper', () => {
let fixture: ComponentFixture<LinearMatHorizontalStepperApp>;
let testComponent: LinearMatHorizontalStepperApp;
Expand Down Expand Up @@ -1083,3 +1119,17 @@ class SimpleStepperWithStepControlAndCompletedBinding {
{label: 'Three', completed: false, control: new FormControl()}
];
}

@Component({
template: `
<mat-horizontal-stepper>
<ng-template matStepperIcon="edit">Custom edit</ng-template>
<ng-template matStepperIcon="done">Custom done</ng-template>

<mat-step>Content 1</mat-step>
<mat-step>Content 2</mat-step>
<mat-step>Content 3</mat-step>
</mat-horizontal-stepper>
`
})
class IconOverridesStepper {}
25 changes: 25 additions & 0 deletions src/lib/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@ import {
ChangeDetectorRef,
ChangeDetectionStrategy,
Optional,
TemplateRef,
} from '@angular/core';
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
import {ErrorStateMatcher} from '@angular/material/core';
import {MatStepHeader} from './step-header';
import {MatStepLabel} from './step-label';
import {takeUntil} from 'rxjs/operators/takeUntil';
import {matStepperAnimations} from './stepper-animations';
import {MatStepperIcon} from './stepper-icon';

/** Workaround for https://github.com/angular/angular/issues/17849 */
export const _MatStep = CdkStep;
export const _MatStepper = CdkStepper;

@Component({
moduleId: module.id,
Expand Down Expand Up @@ -64,6 +70,7 @@ export class MatStep extends CdkStep implements ErrorStateMatcher {
}
}


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

/** Custom icon overrides passed in by the consumer. */
@ContentChildren(MatStepperIcon) _icons: QueryList<MatStepperIcon>;

/** Consumer-specified template-refs to be used to override the header icons. */
_iconOverrides: {[key: string]: TemplateRef<any>} = {};

ngAfterContentInit() {
const icons = this._icons.toArray();
const editOverride = icons.find(icon => icon.name === 'edit');
const doneOverride = icons.find(icon => icon.name === 'done');

if (editOverride) {
this._iconOverrides.edit = editOverride.templateRef;
}

if (doneOverride) {
this._iconOverrides.done = doneOverride.templateRef;
}

// Mark the component for change detection whenever the content children query changes
this._steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => this._stateChanged());
}
Expand Down