Skip to content
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

feat(stepper): Support additional properties for step #6509

Merged
merged 9 commits into from
Aug 22, 2017
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() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm I wonder if this is something we want to give the user control of by making it an @Input()?

@jelbourn WDYT of something like this:

@Input()
get completed() {
  return this._customCompleted == null ? this._defaultCompleted : this._customCompleted;
}
set completed(value) {
  this._customCompleted = value == null ? null : coerceBooleanProperty(value);
}
private _customCompleted: boolean | null = null;

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

This way most users could use the default completeness behavior, but they could still override if they want

Copy link
Member

Choose a reason for hiding this comment

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

Seems reasonable, though _defaultCompleted would be a function

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;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

$mat-step-header-icon-size: 16px !default;

.mat-step-icon {
    display: flex;
    align-items: center;
    justify-content: center;
}

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

Loading