Skip to content

Commit

Permalink
feat(stepper): Support additional properties for step (#6509)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jwshinjwshin committed Aug 23, 2017
1 parent 9c439e1 commit 4b39e01
Show file tree
Hide file tree
Showing 15 changed files with 483 additions and 168 deletions.
50 changes: 46 additions & 4 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
Component,
ContentChild,
ViewChild,
TemplateRef
TemplateRef,
ViewEncapsulation
} from '@angular/core';
import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keycodes';
import {CdkStepLabel} from './step-label';
Expand Down Expand Up @@ -53,7 +54,8 @@ export class StepperSelectionEvent {

@Component({
selector: 'cdk-step',
templateUrl: 'step.html'
templateUrl: 'step.html',
encapsulation: ViewEncapsulation.None
})
export class CdkStep {
/** Template for step label if it exists. */
Expand All @@ -77,6 +79,35 @@ export class CdkStep {
@Input()
label: string;

@Input()
get editable() { return this._editable; }
set editable(value: any) {
this._editable = coerceBooleanProperty(value);
}
private _editable = true;

/** Whether the completion of step is optional or not. */
@Input()
get optional() { return this._optional; }
set optional(value: any) {
this._optional = coerceBooleanProperty(value);
}
private _optional = false;

/** Return whether step is completed or not. */
@Input()
get completed() {
return this._customCompleted == null ? this._defaultCompleted : this._customCompleted;
}
set completed(value: any) {
this._customCompleted = coerceBooleanProperty(value);
}
private _customCompleted: boolean | null = null;

private get _defaultCompleted() {
return this._stepControl ? this._stepControl.valid && this.interacted : this.interacted;
}

constructor(private _stepper: CdkStepper) { }

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

/** Used to track unique ID for each stepper component. */
private _groupId: number;
_groupId: number;

constructor() {
this._groupId = nextId++;
Expand Down Expand Up @@ -172,6 +204,16 @@ export class CdkStepper {
}
}

/** Returns the type of icon to be displayed. */
_getIndicatorType(index: number): 'number' | 'edit' | 'done' {
const step = this._steps.toArray()[index];
if (!step.completed || this._selectedIndex == index) {
return 'number';
} else {
return step.editable ? 'edit' : 'done';
}
}

private _emitStepperSelectionEvent(newIndex: number): void {
const stepsArray = this._steps.toArray();
this.selectionChange.emit({
Expand Down
39 changes: 14 additions & 25 deletions src/demo-app/stepper/stepper-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ <h3>Linear Vertical Stepper Demo using a single form</h3>
</div>
</md-step>

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

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

<h3>Vertical Stepper Demo</h3>
<md-checkbox [(ngModel)]="isNonEditable">Make steps non-editable</md-checkbox>
<md-vertical-stepper>
<md-step>
<md-step [editable]="!isNonEditable">
<ng-template mdStepLabel>Fill out your name</ng-template>
<md-form-field>
<input mdInput placeholder="First Name">
<md-error>This field is required</md-error>
</md-form-field>

<md-form-field>
<input mdInput placeholder="Last Name">
<md-error>This field is required</md-error>
</md-form-field>
<div>
<button md-button mdStepperNext type="button">Next</button>
</div>
</md-step>

<md-step>
<md-step [editable]="!isNonEditable">
<ng-template mdStepLabel>
<div>Fill out your phone number</div>
</ng-template>
<md-form-field>
<input mdInput placeholder="Phone number">
<md-error>This field is required</md-error>
</md-form-field>
<div>
<button md-button mdStepperPrevious type="button">Back</button>
<button md-button mdStepperNext type="button">Next</button>
</div>
</md-step>

<md-step>
<md-step [editable]="!isNonEditable">
<ng-template mdStepLabel>
<div>Fill out your address</div>
</ng-template>
<md-form-field>
<input mdInput placeholder="Address">
<md-error>This field is required</md-error>
</md-form-field>
<div>
<button md-button mdStepperPrevious type="button">Back</button>
Expand All @@ -148,25 +145,20 @@ <h3>Horizontal Stepper Demo</h3>
<ng-template mdStepLabel>Fill out your name</ng-template>
<md-form-field>
<input mdInput placeholder="First Name">
<md-error>This field is required</md-error>
</md-form-field>

<md-form-field>
<input mdInput placeholder="Last Name">
<md-error>This field is required</md-error>
</md-form-field>
<div>
<button md-button mdStepperNext type="button">Next</button>
</div>
</md-step>

<md-step>
<ng-template mdStepLabel>
<div>Fill out your phone number</div>
</ng-template>
<ng-template mdStepLabel>Fill out your phone number</ng-template>
<md-form-field>
<input mdInput placeholder="Phone number">
<md-error>This field is required</md-error>
</md-form-field>
<div>
<button md-button mdStepperPrevious type="button">Back</button>
Expand All @@ -175,12 +167,9 @@ <h3>Horizontal Stepper Demo</h3>
</md-step>

<md-step>
<ng-template mdStepLabel>
<div>Fill out your address</div>
</ng-template>
<ng-template mdStepLabel>Fill out your address</ng-template>
<md-form-field>
<input mdInput placeholder="Address">
<md-error>This field is required</md-error>
</md-form-field>
<div>
<button md-button mdStepperPrevious type="button">Back</button>
Expand Down
13 changes: 8 additions & 5 deletions src/demo-app/stepper/stepper-demo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {Component} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';

const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;

@Component({
moduleId: module.id,
selector: 'stepper-demo',
Expand All @@ -10,9 +12,10 @@ import {FormBuilder, FormGroup, Validators} from '@angular/forms';
export class StepperDemo {
formGroup: FormGroup;
isNonLinear = false;
isNonEditable = false;

nameFormGroup: FormGroup;
phoneFormGroup: FormGroup;
emailFormGroup: FormGroup;

steps = [
{label: 'Confirm your name', content: 'Last name, First name.'},
Expand All @@ -34,8 +37,8 @@ export class StepperDemo {
lastNameFormCtrl: ['', Validators.required],
}),
this._formBuilder.group({
phoneFormCtrl: [''],
})
emailFormCtrl: ['', Validators.pattern(EMAIL_REGEX)]
}),
])
});

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

this.phoneFormGroup = this._formBuilder.group({
phoneCtrl: ['', Validators.required]
this.emailFormGroup = this._formBuilder.group({
emailCtrl: ['', Validators.pattern(EMAIL_REGEX)]
});
}
}
23 changes: 11 additions & 12 deletions src/lib/stepper/_stepper-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,29 @@
$background: map-get($theme, background);
$primary: map-get($theme, primary);

.mat-horizontal-stepper-header, .mat-vertical-stepper-header {

.mat-step-header {
&:focus,
&:hover {
background-color: mat-color($background, hover);
}

.mat-stepper-label {
.mat-step-label-active {
color: mat-color($foreground, text);
}

.mat-stepper-index {
.mat-step-label-inactive,
.mat-step-optional {
color: mat-color($foreground, disabled-text);
}

.mat-step-icon {
background-color: mat-color($primary);
color: mat-color($primary, default-contrast);
}

&[aria-selected='false'] {
.mat-stepper-label {
color: mat-color($foreground, disabled-text);
}

.mat-stepper-index {
background-color: mat-color($foreground, disabled-text);
}
.mat-step-icon-not-touched {
background-color: mat-color($foreground, disabled-text);
color: mat-color($primary, default-contrast);
}
}

Expand Down
16 changes: 13 additions & 3 deletions src/lib/stepper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,22 @@ import {CdkStepperModule} from '@angular/cdk/stepper';
import {MdCommonModule} from '../core';
import {MdStepLabel} from './step-label';
import {MdStepperNext, MdStepperPrevious} from './stepper-button';
import {MdIconModule} from '../icon/index';
import {MdStepHeader} from './step-header';

@NgModule({
imports: [MdCommonModule, CommonModule, PortalModule, MdButtonModule, CdkStepperModule],
imports: [
MdCommonModule,
CommonModule,
PortalModule,
MdButtonModule,
CdkStepperModule,
MdIconModule
],
exports: [MdCommonModule, MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper,
MdStepperNext, MdStepperPrevious],
MdStepperNext, MdStepperPrevious, MdStepHeader],
declarations: [MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper,
MdStepperNext, MdStepperPrevious],
MdStepperNext, MdStepperPrevious, MdStepHeader],
})
export class MdStepperModule {}

Expand All @@ -32,3 +41,4 @@ export * from './stepper-vertical';
export * from './step-label';
export * from './stepper';
export * from './stepper-button';
export * from './step-header';
17 changes: 17 additions & 0 deletions src/lib/stepper/step-header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div [class.mat-step-icon]="icon != 'number' || selected"
[class.mat-step-icon-not-touched]="icon == 'number' && !selected">
<span *ngIf="icon == 'number'">{{index + 1}}</span>
<md-icon *ngIf="icon == 'edit'">create</md-icon>
<md-icon *ngIf="icon == 'done'">done</md-icon>
</div>
<div [class.mat-step-label-active]="active"
[class.mat-step-label-inactive]="!active">
<!-- If there is a label template, use it. -->
<ng-container *ngIf="_templateLabel" [ngTemplateOutlet]="label.template">
</ng-container>
<!-- It there is no label template, fall back to the text label. -->
<div class="mat-step-text-label" *ngIf="_stringLabel">{{label}}</div>

<div class="mat-step-optional" *ngIf="optional">Optional</div>
</div>

46 changes: 46 additions & 0 deletions src/lib/stepper/step-header.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
$mat-stepper-label-header-height: 24px !default;
$mat-stepper-label-min-width: 50px !default;
$mat-stepper-side-gap: 24px !default;
$mat-vertical-stepper-content-margin: 36px !default;
$mat-stepper-line-gap: 8px !default;
$mat-step-optional-font-size: 12px;
$mat-step-header-icon-size: 16px !default;

:host {
display: flex;
}

.mat-step-optional {
font-size: $mat-step-optional-font-size;
}

.mat-step-icon,
.mat-step-icon-not-touched {
border-radius: 50%;
height: $mat-stepper-label-header-height;
width: $mat-stepper-label-header-height;
align-items: center;
justify-content: center;
display: flex;
}

.mat-step-icon .mat-icon {
font-size: $mat-step-header-icon-size;
height: $mat-step-header-icon-size;
width: $mat-step-header-icon-size;
}

.mat-step-label-active,
.mat-step-label-inactive {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: $mat-stepper-label-min-width;
vertical-align: middle;
}

.mat-step-text-label {
text-overflow: ellipsis;
overflow: hidden;
}
Loading

0 comments on commit 4b39e01

Please sign in to comment.