Skip to content

Commit

Permalink
feat(stepper): allow for header icons to be customized (#7482)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto authored and tinayuangao committed Jan 30, 2018
1 parent 5d453b9 commit adc251c
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 16 deletions.
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>};

/** Index of the given step. */
@Input()
get index(): number { 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]',
})
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 @@ -174,6 +175,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 @@ -1168,3 +1204,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

0 comments on commit adc251c

Please sign in to comment.