Skip to content

Commit 4b39e01

Browse files
committed
feat(stepper): Support additional properties for step (#6509)
* Additional properties for step * Unit tests * Code changes based on review + test name changes * Refactor code for shared functionality between vertical and horizontal stepper * Refactor md-step-header and md-step-content + optional step change * Simplify code based on review * Changes to step-header based on review * Minor changes
1 parent 9c439e1 commit 4b39e01

15 files changed

+483
-168
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
Component,
2121
ContentChild,
2222
ViewChild,
23-
TemplateRef
23+
TemplateRef,
24+
ViewEncapsulation
2425
} from '@angular/core';
2526
import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keycodes';
2627
import {CdkStepLabel} from './step-label';
@@ -53,7 +54,8 @@ export class StepperSelectionEvent {
5354

5455
@Component({
5556
selector: 'cdk-step',
56-
templateUrl: 'step.html'
57+
templateUrl: 'step.html',
58+
encapsulation: ViewEncapsulation.None
5759
})
5860
export class CdkStep {
5961
/** Template for step label if it exists. */
@@ -77,6 +79,35 @@ export class CdkStep {
7779
@Input()
7880
label: string;
7981

82+
@Input()
83+
get editable() { return this._editable; }
84+
set editable(value: any) {
85+
this._editable = coerceBooleanProperty(value);
86+
}
87+
private _editable = true;
88+
89+
/** Whether the completion of step is optional or not. */
90+
@Input()
91+
get optional() { return this._optional; }
92+
set optional(value: any) {
93+
this._optional = coerceBooleanProperty(value);
94+
}
95+
private _optional = false;
96+
97+
/** Return whether step is completed or not. */
98+
@Input()
99+
get completed() {
100+
return this._customCompleted == null ? this._defaultCompleted : this._customCompleted;
101+
}
102+
set completed(value: any) {
103+
this._customCompleted = coerceBooleanProperty(value);
104+
}
105+
private _customCompleted: boolean | null = null;
106+
107+
private get _defaultCompleted() {
108+
return this._stepControl ? this._stepControl.valid && this.interacted : this.interacted;
109+
}
110+
80111
constructor(private _stepper: CdkStepper) { }
81112

82113
/** Selects this step component. */
@@ -109,7 +140,8 @@ export class CdkStepper {
109140
@Input()
110141
get selectedIndex() { return this._selectedIndex; }
111142
set selectedIndex(index: number) {
112-
if (this._anyControlsInvalid(index)) {
143+
if (this._anyControlsInvalid(index)
144+
|| index < this._selectedIndex && !this._steps.toArray()[index].editable) {
113145
// remove focus from clicked step header if the step is not able to be selected
114146
this._stepHeader.toArray()[index].nativeElement.blur();
115147
} else if (this._selectedIndex != index) {
@@ -134,7 +166,7 @@ export class CdkStepper {
134166
_focusIndex: number = 0;
135167

136168
/** Used to track unique ID for each stepper component. */
137-
private _groupId: number;
169+
_groupId: number;
138170

139171
constructor() {
140172
this._groupId = nextId++;
@@ -172,6 +204,16 @@ export class CdkStepper {
172204
}
173205
}
174206

207+
/** Returns the type of icon to be displayed. */
208+
_getIndicatorType(index: number): 'number' | 'edit' | 'done' {
209+
const step = this._steps.toArray()[index];
210+
if (!step.completed || this._selectedIndex == index) {
211+
return 'number';
212+
} else {
213+
return step.editable ? 'edit' : 'done';
214+
}
215+
}
216+
175217
private _emitStepperSelectionEvent(newIndex: number): void {
176218
const stepsArray = this._steps.toArray();
177219
this.selectionChange.emit({

src/demo-app/stepper/stepper-demo.html

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ <h3>Linear Vertical Stepper Demo using a single form</h3>
1919
</div>
2020
</md-step>
2121

22-
<md-step formGroupName="1" [stepControl]="formArray.get([1])">
22+
<md-step formGroupName="1" [stepControl]="formArray.get([1])" optional>
2323
<ng-template mdStepLabel>
24-
<div>Fill out your phone number</div>
24+
<div>Fill out your email address</div>
2525
</ng-template>
2626
<md-input-container>
27-
<input mdInput placeholder="Phone number" formControlName="phoneFormCtrl">
28-
<md-error>This field is required</md-error>
27+
<input mdInput placeholder="Email address" formControlName="emailFormCtrl">
28+
<md-error>The input is invalid.</md-error>
2929
</md-input-container>
3030
<div>
3131
<button md-button mdStepperPrevious type="button">Back</button>
@@ -62,12 +62,12 @@ <h3>Linear Horizontal Stepper Demo using a different form for each step</h3>
6262
</form>
6363
</md-step>
6464

65-
<md-step [stepControl]="phoneFormGroup">
66-
<form [formGroup]="phoneFormGroup">
65+
<md-step [stepControl]="emailFormGroup" optional>
66+
<form [formGroup]="emailFormGroup">
6767
<ng-template mdStepLabel>Fill out your phone number</ng-template>
6868
<md-form-field>
69-
<input mdInput placeholder="Phone number" formControlName="phoneCtrl" required>
70-
<md-error>This field is required</md-error>
69+
<input mdInput placeholder="Email address" formControlName="emailCtrl">
70+
<md-error>The input is invalid</md-error>
7171
</md-form-field>
7272
<div>
7373
<button md-button mdStepperPrevious>Back</button>
@@ -88,44 +88,41 @@ <h3>Linear Horizontal Stepper Demo using a different form for each step</h3>
8888
</md-horizontal-stepper>
8989

9090
<h3>Vertical Stepper Demo</h3>
91+
<md-checkbox [(ngModel)]="isNonEditable">Make steps non-editable</md-checkbox>
9192
<md-vertical-stepper>
92-
<md-step>
93+
<md-step [editable]="!isNonEditable">
9394
<ng-template mdStepLabel>Fill out your name</ng-template>
9495
<md-form-field>
9596
<input mdInput placeholder="First Name">
96-
<md-error>This field is required</md-error>
9797
</md-form-field>
9898

9999
<md-form-field>
100100
<input mdInput placeholder="Last Name">
101-
<md-error>This field is required</md-error>
102101
</md-form-field>
103102
<div>
104103
<button md-button mdStepperNext type="button">Next</button>
105104
</div>
106105
</md-step>
107106

108-
<md-step>
107+
<md-step [editable]="!isNonEditable">
109108
<ng-template mdStepLabel>
110109
<div>Fill out your phone number</div>
111110
</ng-template>
112111
<md-form-field>
113112
<input mdInput placeholder="Phone number">
114-
<md-error>This field is required</md-error>
115113
</md-form-field>
116114
<div>
117115
<button md-button mdStepperPrevious type="button">Back</button>
118116
<button md-button mdStepperNext type="button">Next</button>
119117
</div>
120118
</md-step>
121119

122-
<md-step>
120+
<md-step [editable]="!isNonEditable">
123121
<ng-template mdStepLabel>
124122
<div>Fill out your address</div>
125123
</ng-template>
126124
<md-form-field>
127125
<input mdInput placeholder="Address">
128-
<md-error>This field is required</md-error>
129126
</md-form-field>
130127
<div>
131128
<button md-button mdStepperPrevious type="button">Back</button>
@@ -148,25 +145,20 @@ <h3>Horizontal Stepper Demo</h3>
148145
<ng-template mdStepLabel>Fill out your name</ng-template>
149146
<md-form-field>
150147
<input mdInput placeholder="First Name">
151-
<md-error>This field is required</md-error>
152148
</md-form-field>
153149

154150
<md-form-field>
155151
<input mdInput placeholder="Last Name">
156-
<md-error>This field is required</md-error>
157152
</md-form-field>
158153
<div>
159154
<button md-button mdStepperNext type="button">Next</button>
160155
</div>
161156
</md-step>
162157

163158
<md-step>
164-
<ng-template mdStepLabel>
165-
<div>Fill out your phone number</div>
166-
</ng-template>
159+
<ng-template mdStepLabel>Fill out your phone number</ng-template>
167160
<md-form-field>
168161
<input mdInput placeholder="Phone number">
169-
<md-error>This field is required</md-error>
170162
</md-form-field>
171163
<div>
172164
<button md-button mdStepperPrevious type="button">Back</button>
@@ -175,12 +167,9 @@ <h3>Horizontal Stepper Demo</h3>
175167
</md-step>
176168

177169
<md-step>
178-
<ng-template mdStepLabel>
179-
<div>Fill out your address</div>
180-
</ng-template>
170+
<ng-template mdStepLabel>Fill out your address</ng-template>
181171
<md-form-field>
182172
<input mdInput placeholder="Address">
183-
<md-error>This field is required</md-error>
184173
</md-form-field>
185174
<div>
186175
<button md-button mdStepperPrevious type="button">Back</button>

src/demo-app/stepper/stepper-demo.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {Component} from '@angular/core';
22
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
33

4+
const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
5+
46
@Component({
57
moduleId: module.id,
68
selector: 'stepper-demo',
@@ -10,9 +12,10 @@ import {FormBuilder, FormGroup, Validators} from '@angular/forms';
1012
export class StepperDemo {
1113
formGroup: FormGroup;
1214
isNonLinear = false;
15+
isNonEditable = false;
1316

1417
nameFormGroup: FormGroup;
15-
phoneFormGroup: FormGroup;
18+
emailFormGroup: FormGroup;
1619

1720
steps = [
1821
{label: 'Confirm your name', content: 'Last name, First name.'},
@@ -34,8 +37,8 @@ export class StepperDemo {
3437
lastNameFormCtrl: ['', Validators.required],
3538
}),
3639
this._formBuilder.group({
37-
phoneFormCtrl: [''],
38-
})
40+
emailFormCtrl: ['', Validators.pattern(EMAIL_REGEX)]
41+
}),
3942
])
4043
});
4144

@@ -44,8 +47,8 @@ export class StepperDemo {
4447
lastNameCtrl: ['', Validators.required],
4548
});
4649

47-
this.phoneFormGroup = this._formBuilder.group({
48-
phoneCtrl: ['', Validators.required]
50+
this.emailFormGroup = this._formBuilder.group({
51+
emailCtrl: ['', Validators.pattern(EMAIL_REGEX)]
4952
});
5053
}
5154
}

src/lib/stepper/_stepper-theme.scss

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,29 @@
77
$background: map-get($theme, background);
88
$primary: map-get($theme, primary);
99

10-
.mat-horizontal-stepper-header, .mat-vertical-stepper-header {
11-
10+
.mat-step-header {
1211
&:focus,
1312
&:hover {
1413
background-color: mat-color($background, hover);
1514
}
1615

17-
.mat-stepper-label {
16+
.mat-step-label-active {
1817
color: mat-color($foreground, text);
1918
}
2019

21-
.mat-stepper-index {
20+
.mat-step-label-inactive,
21+
.mat-step-optional {
22+
color: mat-color($foreground, disabled-text);
23+
}
24+
25+
.mat-step-icon {
2226
background-color: mat-color($primary);
2327
color: mat-color($primary, default-contrast);
2428
}
2529

26-
&[aria-selected='false'] {
27-
.mat-stepper-label {
28-
color: mat-color($foreground, disabled-text);
29-
}
30-
31-
.mat-stepper-index {
32-
background-color: mat-color($foreground, disabled-text);
33-
}
30+
.mat-step-icon-not-touched {
31+
background-color: mat-color($foreground, disabled-text);
32+
color: mat-color($primary, default-contrast);
3433
}
3534
}
3635

src/lib/stepper/index.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,22 @@ import {CdkStepperModule} from '@angular/cdk/stepper';
1717
import {MdCommonModule} from '../core';
1818
import {MdStepLabel} from './step-label';
1919
import {MdStepperNext, MdStepperPrevious} from './stepper-button';
20+
import {MdIconModule} from '../icon/index';
21+
import {MdStepHeader} from './step-header';
2022

2123
@NgModule({
22-
imports: [MdCommonModule, CommonModule, PortalModule, MdButtonModule, CdkStepperModule],
24+
imports: [
25+
MdCommonModule,
26+
CommonModule,
27+
PortalModule,
28+
MdButtonModule,
29+
CdkStepperModule,
30+
MdIconModule
31+
],
2332
exports: [MdCommonModule, MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper,
24-
MdStepperNext, MdStepperPrevious],
33+
MdStepperNext, MdStepperPrevious, MdStepHeader],
2534
declarations: [MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper,
26-
MdStepperNext, MdStepperPrevious],
35+
MdStepperNext, MdStepperPrevious, MdStepHeader],
2736
})
2837
export class MdStepperModule {}
2938

@@ -32,3 +41,4 @@ export * from './stepper-vertical';
3241
export * from './step-label';
3342
export * from './stepper';
3443
export * from './stepper-button';
44+
export * from './step-header';

src/lib/stepper/step-header.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<div [class.mat-step-icon]="icon != 'number' || selected"
2+
[class.mat-step-icon-not-touched]="icon == 'number' && !selected">
3+
<span *ngIf="icon == 'number'">{{index + 1}}</span>
4+
<md-icon *ngIf="icon == 'edit'">create</md-icon>
5+
<md-icon *ngIf="icon == 'done'">done</md-icon>
6+
</div>
7+
<div [class.mat-step-label-active]="active"
8+
[class.mat-step-label-inactive]="!active">
9+
<!-- If there is a label template, use it. -->
10+
<ng-container *ngIf="_templateLabel" [ngTemplateOutlet]="label.template">
11+
</ng-container>
12+
<!-- It there is no label template, fall back to the text label. -->
13+
<div class="mat-step-text-label" *ngIf="_stringLabel">{{label}}</div>
14+
15+
<div class="mat-step-optional" *ngIf="optional">Optional</div>
16+
</div>
17+

src/lib/stepper/step-header.scss

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
$mat-stepper-label-header-height: 24px !default;
2+
$mat-stepper-label-min-width: 50px !default;
3+
$mat-stepper-side-gap: 24px !default;
4+
$mat-vertical-stepper-content-margin: 36px !default;
5+
$mat-stepper-line-gap: 8px !default;
6+
$mat-step-optional-font-size: 12px;
7+
$mat-step-header-icon-size: 16px !default;
8+
9+
:host {
10+
display: flex;
11+
}
12+
13+
.mat-step-optional {
14+
font-size: $mat-step-optional-font-size;
15+
}
16+
17+
.mat-step-icon,
18+
.mat-step-icon-not-touched {
19+
border-radius: 50%;
20+
height: $mat-stepper-label-header-height;
21+
width: $mat-stepper-label-header-height;
22+
align-items: center;
23+
justify-content: center;
24+
display: flex;
25+
}
26+
27+
.mat-step-icon .mat-icon {
28+
font-size: $mat-step-header-icon-size;
29+
height: $mat-step-header-icon-size;
30+
width: $mat-step-header-icon-size;
31+
}
32+
33+
.mat-step-label-active,
34+
.mat-step-label-inactive {
35+
display: inline-block;
36+
white-space: nowrap;
37+
overflow: hidden;
38+
text-overflow: ellipsis;
39+
min-width: $mat-stepper-label-min-width;
40+
vertical-align: middle;
41+
}
42+
43+
.mat-step-text-label {
44+
text-overflow: ellipsis;
45+
overflow: hidden;
46+
}

0 commit comments

Comments
 (0)