Skip to content

Commit 9ab2c90

Browse files
arodr967jelbourn
authored andcommitted
feat(stepper): Create MAT_STEPPER_GLOBAL_OPTIONS InjectionToken (#11457)
1 parent 9d3d95f commit 9ab2c90

18 files changed

+665
-180
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
TemplateRef,
3232
ViewChild,
3333
ViewEncapsulation,
34+
InjectionToken,
3435
} from '@angular/core';
3536
import {DOCUMENT} from '@angular/common';
3637
import {AbstractControl} from '@angular/forms';
@@ -65,6 +66,37 @@ export class StepperSelectionEvent {
6566
previouslySelectedStep: CdkStep;
6667
}
6768

69+
/** The state of each step. */
70+
export type StepState = 'number' | 'edit' | 'done' | 'error' | string;
71+
72+
/** Enum to represent the different states of the steps. */
73+
export const STEP_STATE = {
74+
NUMBER: 'number',
75+
EDIT: 'edit',
76+
DONE: 'done',
77+
ERROR: 'error'
78+
};
79+
80+
/** InjectionToken that can be used to specify the global stepper options. */
81+
export const MAT_STEPPER_GLOBAL_OPTIONS =
82+
new InjectionToken<StepperOptions>('mat-stepper-global-options');
83+
84+
/** Configurable options for stepper. */
85+
export interface StepperOptions {
86+
/**
87+
* Whether the stepper should display an error state or not.
88+
* Default behavior is assumed to be false.
89+
*/
90+
showError?: boolean;
91+
92+
/**
93+
* Whether the stepper should display the default indicator type
94+
* or not.
95+
* Default behavior is assumed to be true.
96+
*/
97+
displayDefaultIndicatorType?: boolean;
98+
}
99+
68100
@Component({
69101
moduleId: module.id,
70102
selector: 'cdk-step',
@@ -74,6 +106,10 @@ export class StepperSelectionEvent {
74106
changeDetection: ChangeDetectionStrategy.OnPush,
75107
})
76108
export class CdkStep implements OnChanges {
109+
private _stepperOptions: StepperOptions;
110+
_showError: boolean;
111+
_displayDefaultIndicatorType: boolean;
112+
77113
/** Template for step label if it exists. */
78114
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel;
79115

@@ -89,6 +125,9 @@ export class CdkStep implements OnChanges {
89125
/** Plain text label of the step. */
90126
@Input() label: string;
91127

128+
/** Error message to display when there's an error. */
129+
@Input() errorMessage: string;
130+
92131
/** Aria label for the tab. */
93132
@Input('aria-label') ariaLabel: string;
94133

@@ -98,6 +137,9 @@ export class CdkStep implements OnChanges {
98137
*/
99138
@Input('aria-labelledby') ariaLabelledby: string;
100139

140+
/** State of the step. */
141+
@Input() state: StepState;
142+
101143
/** Whether the user can return to this step once it has been marked as complted. */
102144
@Input()
103145
get editable(): boolean { return this._editable; }
@@ -117,18 +159,39 @@ export class CdkStep implements OnChanges {
117159
/** Whether step is marked as completed. */
118160
@Input()
119161
get completed(): boolean {
120-
return this._customCompleted == null ? this._defaultCompleted() : this._customCompleted;
162+
return this._customCompleted == null ? this._getDefaultCompleted() : this._customCompleted;
121163
}
122164
set completed(value: boolean) {
123165
this._customCompleted = coerceBooleanProperty(value);
124166
}
125167
private _customCompleted: boolean | null = null;
126168

127-
private _defaultCompleted() {
169+
private _getDefaultCompleted() {
128170
return this.stepControl ? this.stepControl.valid && this.interacted : this.interacted;
129171
}
130172

131-
constructor(@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper) { }
173+
/** Whether step has an error. */
174+
@Input()
175+
get hasError(): boolean {
176+
return this._customError || this._getDefaultError();
177+
}
178+
set hasError(value: boolean) {
179+
this._customError = coerceBooleanProperty(value);
180+
}
181+
private _customError: boolean | null = null;
182+
183+
private _getDefaultError() {
184+
return this.stepControl && this.stepControl.invalid && this.interacted;
185+
}
186+
187+
/** @breaking-change 8.0.0 remove the `?` after `stepperOptions` */
188+
constructor(
189+
@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper,
190+
@Optional() @Inject(MAT_STEPPER_GLOBAL_OPTIONS) stepperOptions?: StepperOptions) {
191+
this._stepperOptions = stepperOptions ? stepperOptions : {};
192+
this._displayDefaultIndicatorType = this._stepperOptions.displayDefaultIndicatorType !== false;
193+
this._showError = !!this._stepperOptions.showError;
194+
}
132195

133196
/** Selects this step component. */
134197
select(): void {
@@ -143,6 +206,10 @@ export class CdkStep implements OnChanges {
143206
this._customCompleted = false;
144207
}
145208

209+
if (this._customError != null) {
210+
this._customError = false;
211+
}
212+
146213
if (this.stepControl) {
147214
this.stepControl.reset();
148215
}
@@ -301,15 +368,46 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
301368
}
302369

303370
/** Returns the type of icon to be displayed. */
304-
_getIndicatorType(index: number): 'number' | 'edit' | 'done' {
371+
_getIndicatorType(index: number, state: StepState = STEP_STATE.NUMBER): StepState {
305372
const step = this._steps.toArray()[index];
306-
if (!step.completed || this._selectedIndex == index) {
307-
return 'number';
373+
const isCurrentStep = this._isCurrentStep(index);
374+
375+
return step._displayDefaultIndicatorType
376+
? this._getDefaultIndicatorLogic(step, isCurrentStep)
377+
: this._getGuidelineLogic(step, isCurrentStep, state);
378+
}
379+
380+
private _getDefaultIndicatorLogic(step: CdkStep, isCurrentStep: boolean): StepState {
381+
if (step._showError && step.hasError && !isCurrentStep) {
382+
return STEP_STATE.ERROR;
383+
} else if (!step.completed || isCurrentStep) {
384+
return STEP_STATE.NUMBER;
385+
} else {
386+
return step.editable ? STEP_STATE.EDIT : STEP_STATE.DONE;
387+
}
388+
}
389+
390+
private _getGuidelineLogic(
391+
step: CdkStep,
392+
isCurrentStep: boolean,
393+
state: StepState = STEP_STATE.NUMBER): StepState {
394+
if (step._showError && step.hasError && !isCurrentStep) {
395+
return STEP_STATE.ERROR;
396+
} else if (step.completed && !isCurrentStep) {
397+
return STEP_STATE.DONE;
398+
} else if (step.completed && isCurrentStep) {
399+
return state;
400+
} else if (step.editable && isCurrentStep) {
401+
return STEP_STATE.EDIT;
308402
} else {
309-
return step.editable ? 'edit' : 'done';
403+
return state;
310404
}
311405
}
312406

407+
private _isCurrentStep(index: number) {
408+
return this._selectedIndex === index;
409+
}
410+
313411
/** Returns the index of the currently-focused step header. */
314412
_getFocusIndex() {
315413
return this._keyManager ? this._keyManager.activeItemIndex : this._selectedIndex;

src/lib/stepper/_stepper-theme.scss

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
$foreground: map-get($theme, foreground);
77
$background: map-get($theme, background);
88
$primary: map-get($theme, primary);
9+
$warn: map-get($theme, warn);
910

1011
.mat-step-header {
1112
&.cdk-keyboard-focused,
@@ -20,18 +21,29 @@
2021
}
2122

2223
.mat-step-icon {
23-
background-color: mat-color($primary);
24+
background-color: mat-color($foreground, disabled-text);
2425
color: mat-color($primary, default-contrast);
2526
}
2627

27-
.mat-step-icon-not-touched {
28-
background-color: mat-color($foreground, disabled-text);
28+
.mat-step-icon-selected,
29+
.mat-step-icon-state-done,
30+
.mat-step-icon-state-edit {
31+
background-color: mat-color($primary);
2932
color: mat-color($primary, default-contrast);
3033
}
3134

35+
.mat-step-icon-state-error {
36+
background-color: transparent;
37+
color: mat-color($warn);
38+
}
39+
3240
.mat-step-label.mat-step-label-active {
3341
color: mat-color($foreground, text);
3442
}
43+
44+
.mat-step-label.mat-step-label-error {
45+
color: mat-color($warn);
46+
}
3547
}
3648

3749
.mat-stepper-horizontal, .mat-stepper-vertical {
@@ -59,6 +71,14 @@
5971
};
6072
}
6173

74+
.mat-step-sub-label-error {
75+
font-weight: normal;
76+
}
77+
78+
.mat-step-label-error {
79+
font-size: mat-font-size($config, body-2);
80+
}
81+
6282
.mat-step-label-selected {
6383
font: {
6484
size: mat-font-size($config, body-2);

src/lib/stepper/step-header.html

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
<div class="mat-step-header-ripple" mat-ripple [matRippleTrigger]="_getHostElement()"></div>
2-
<div [class.mat-step-icon]="state !== 'number' || selected"
3-
[class.mat-step-icon-not-touched]="state == 'number' && !selected"
2+
<div class="mat-step-icon-state-{{state}} mat-step-icon" [class.mat-step-icon-selected]="selected"
43
[ngSwitch]="state">
5-
64
<ng-container *ngSwitchCase="'number'" [ngSwitch]="!!(iconOverrides && iconOverrides.number)">
75
<ng-container
86
*ngSwitchCase="true"
@@ -26,16 +24,35 @@
2624
[ngTemplateOutletContext]="_getIconContext()"></ng-container>
2725
<mat-icon class="mat-step-icon-content" *ngSwitchDefault>done</mat-icon>
2826
</ng-container>
27+
28+
<ng-container *ngSwitchCase="'error'" [ngSwitch]="!!(iconOverrides && iconOverrides.error)">
29+
<ng-container
30+
*ngSwitchCase="true"
31+
[ngTemplateOutlet]="iconOverrides.error"
32+
[ngTemplateOutletContext]="_getIconContext()"></ng-container>
33+
<mat-icon *ngSwitchDefault>warning</mat-icon>
34+
</ng-container>
35+
36+
<!-- Custom state. -->
37+
<ng-container *ngSwitchDefault [ngSwitch]="!!(iconOverrides && iconOverrides[state])">
38+
<ng-container
39+
*ngSwitchCase="true"
40+
[ngTemplateOutlet]="iconOverrides[state]"
41+
[ngTemplateOutletContext]="_getIconContext()"></ng-container>
42+
<mat-icon *ngSwitchDefault>{{state}}</mat-icon>
43+
</ng-container>
2944
</div>
3045
<div class="mat-step-label"
3146
[class.mat-step-label-active]="active"
32-
[class.mat-step-label-selected]="selected">
47+
[class.mat-step-label-selected]="selected"
48+
[class.mat-step-label-error]="state == 'error'">
3349
<!-- If there is a label template, use it. -->
3450
<ng-container *ngIf="_templateLabel()" [ngTemplateOutlet]="_templateLabel()!.template">
3551
</ng-container>
36-
<!-- It there is no label template, fall back to the text label. -->
52+
<!-- If there is no label template, fall back to the text label. -->
3753
<div class="mat-step-text-label" *ngIf="_stringLabel()">{{label}}</div>
3854

39-
<div class="mat-step-optional" *ngIf="optional">{{_intl.optionalLabel}}</div>
55+
<div class="mat-step-optional" *ngIf="optional && state != 'error'">{{_intl.optionalLabel}}</div>
56+
<div class="mat-step-sub-label-error" *ngIf="state == 'error'">{{errorMessage}}</div>
4057
</div>
4158

src/lib/stepper/step-header.scss

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ $mat-stepper-label-min-width: 50px !default;
55
$mat-stepper-side-gap: 24px !default;
66
$mat-vertical-stepper-content-margin: 36px !default;
77
$mat-stepper-line-gap: 8px !default;
8-
$mat-step-optional-font-size: 12px;
8+
$mat-step-sub-label-font-size: 12px;
99
$mat-step-header-icon-size: 16px !default;
1010

1111
.mat-step-header {
@@ -17,12 +17,12 @@ $mat-step-header-icon-size: 16px !default;
1717
-webkit-tap-highlight-color: transparent;
1818
}
1919

20-
.mat-step-optional {
21-
font-size: $mat-step-optional-font-size;
20+
.mat-step-optional,
21+
.mat-step-sub-label-error {
22+
font-size: $mat-step-sub-label-font-size;
2223
}
2324

24-
.mat-step-icon,
25-
.mat-step-icon-not-touched {
25+
.mat-step-icon {
2626
border-radius: 50%;
2727
height: $mat-stepper-label-header-height;
2828
width: $mat-stepper-label-header-height;
@@ -44,6 +44,12 @@ $mat-step-header-icon-size: 16px !default;
4444
width: $mat-step-header-icon-size;
4545
}
4646

47+
.mat-step-icon-state-error .mat-icon {
48+
font-size: $mat-step-header-icon-size + 8;
49+
height: $mat-step-header-icon-size + 8;
50+
width: $mat-step-header-icon-size + 8;
51+
}
52+
4753
.mat-step-label {
4854
display: inline-block;
4955
white-space: nowrap;

src/lib/stepper/step-header.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {Subscription} from 'rxjs';
2121
import {MatStepLabel} from './step-label';
2222
import {MatStepperIntl} from './stepper-intl';
2323
import {MatStepperIconContext} from './stepper-icon';
24-
24+
import {StepState} from '@angular/cdk/stepper';
2525

2626
@Component({
2727
moduleId: module.id,
@@ -39,11 +39,14 @@ export class MatStepHeader implements OnDestroy {
3939
private _intlSubscription: Subscription;
4040

4141
/** State of the given step. */
42-
@Input() state: string;
42+
@Input() state: StepState;
4343

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

47+
/** Error message to display when there's an error. */
48+
@Input() errorMessage: string;
49+
4750
/** Overrides for the header icons, passed in via the stepper. */
4851
@Input() iconOverrides: {[key: string]: TemplateRef<MatStepperIconContext>};
4952

src/lib/stepper/stepper-horizontal.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
[attr.aria-label]="step.ariaLabel || null"
1313
[attr.aria-labelledby]="(!step.ariaLabel && step.ariaLabelledby) ? step.ariaLabelledby : null"
1414
[index]="i"
15-
[state]="_getIndicatorType(i)"
15+
[state]="_getIndicatorType(i, step.state)"
1616
[label]="step.stepLabel || step.label"
1717
[selected]="selectedIndex === i"
1818
[active]="step.completed || selectedIndex === i || !linear"
1919
[optional]="step.optional"
20+
[errorMessage]="step.errorMessage"
2021
[iconOverrides]="_iconOverrides">
2122
</mat-step-header>
2223
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>

src/lib/stepper/stepper-icon.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {Directive, Input, TemplateRef} from '@angular/core';
10+
import {StepState} from '@angular/cdk/stepper';
1011

1112
/** Template context available to an attached `matStepperIcon`. */
1213
export interface MatStepperIconContext {
@@ -26,7 +27,7 @@ export interface MatStepperIconContext {
2627
})
2728
export class MatStepperIcon {
2829
/** Name of the icon to be overridden. */
29-
@Input('matStepperIcon') name: 'edit' | 'done' | 'number';
30+
@Input('matStepperIcon') name: StepState;
3031

3132
constructor(public templateRef: TemplateRef<MatStepperIconContext>) {}
3233
}

src/lib/stepper/stepper-vertical.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
[attr.aria-label]="step.ariaLabel || null"
1212
[attr.aria-labelledby]="(!step.ariaLabel && step.ariaLabelledby) ? step.ariaLabelledby : null"
1313
[index]="i"
14-
[state]="_getIndicatorType(i)"
14+
[state]="_getIndicatorType(i, step.state)"
1515
[label]="step.stepLabel || step.label"
1616
[selected]="selectedIndex === i"
1717
[active]="step.completed || selectedIndex === i || !linear"
1818
[optional]="step.optional"
19+
[errorMessage]="step.errorMessage"
1920
[iconOverrides]="_iconOverrides">
2021
</mat-step-header>
2122

0 commit comments

Comments
 (0)