mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(stepper): initial version of stepper (#6594)
This commit is contained in:
committed by
Jeremy Elbourn
parent
70bd5fc20e
commit
87318bc7c8
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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
|
||||
*/
|
||||
|
||||
export * from './public_api';
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {NgModule} from '@angular/core';
|
||||
import {CdkStepper, CdkStep} from './stepper';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {CdkStepLabel} from './step-label';
|
||||
import {CdkStepperNext, CdkStepperPrevious} from './stepper-button';
|
||||
import {BidiModule} from '@angular/cdk/bidi';
|
||||
|
||||
@NgModule({
|
||||
imports: [BidiModule, CommonModule],
|
||||
exports: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious],
|
||||
declarations: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious]
|
||||
})
|
||||
export class CdkStepperModule {}
|
||||
|
||||
export * from './stepper';
|
||||
export * from './step-label';
|
||||
export * from './stepper-button';
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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, TemplateRef} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[cdkStepLabel]',
|
||||
})
|
||||
export class CdkStepLabel {
|
||||
constructor(public template: TemplateRef<any>) { }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ng-template><ng-content></ng-content></ng-template>
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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} from '@angular/core';
|
||||
import {CdkStepper} from './stepper';
|
||||
|
||||
/** Button that moves to the next step in a stepper workflow. */
|
||||
@Directive({
|
||||
selector: 'button[cdkStepperNext]',
|
||||
host: {'(click)': '_stepper.next()'}
|
||||
})
|
||||
export class CdkStepperNext {
|
||||
constructor(public _stepper: CdkStepper) { }
|
||||
}
|
||||
|
||||
/** Button that moves to the previous step in a stepper workflow. */
|
||||
@Directive({
|
||||
selector: 'button[cdkStepperPrevious]',
|
||||
host: {'(click)': '_stepper.previous()'}
|
||||
})
|
||||
export class CdkStepperPrevious {
|
||||
constructor(public _stepper: CdkStepper) { }
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {
|
||||
ContentChildren,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
QueryList,
|
||||
Directive,
|
||||
// This import is only used to define a generic type. The current TypeScript version incorrectly
|
||||
// considers such imports as unused (https://github.com/Microsoft/TypeScript/issues/14953)
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
ElementRef,
|
||||
Component,
|
||||
ContentChild,
|
||||
ViewChild,
|
||||
TemplateRef,
|
||||
ViewEncapsulation,
|
||||
Optional,
|
||||
Inject,
|
||||
forwardRef
|
||||
} from '@angular/core';
|
||||
import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keycodes';
|
||||
import {CdkStepLabel} from './step-label';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
import {AbstractControl} from '@angular/forms';
|
||||
import {Directionality} from '@angular/cdk/bidi';
|
||||
|
||||
/** Used to generate unique ID for each stepper component. */
|
||||
let nextId = 0;
|
||||
|
||||
/**
|
||||
* Position state of the content of each step in stepper that is used for transitioning
|
||||
* the content into correct position upon step selection change.
|
||||
*/
|
||||
export type StepContentPositionState = 'previous' | 'current' | 'next';
|
||||
|
||||
/** Change event emitted on selection changes. */
|
||||
export class StepperSelectionEvent {
|
||||
/** Index of the step now selected. */
|
||||
selectedIndex: number;
|
||||
|
||||
/** Index of the step previously selected. */
|
||||
previouslySelectedIndex: number;
|
||||
|
||||
/** The step instance now selected. */
|
||||
selectedStep: CdkStep;
|
||||
|
||||
/** The step instance previously selected. */
|
||||
previouslySelectedStep: CdkStep;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cdk-step',
|
||||
templateUrl: 'step.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class CdkStep {
|
||||
/** Template for step label if it exists. */
|
||||
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel;
|
||||
|
||||
/** Template for step content. */
|
||||
@ViewChild(TemplateRef) content: TemplateRef<any>;
|
||||
|
||||
/** The top level abstract control of the step. */
|
||||
@Input() stepControl: AbstractControl;
|
||||
|
||||
/** Whether user has seen the expanded step content or not . */
|
||||
interacted = false;
|
||||
|
||||
/** Label of the step. */
|
||||
@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(@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper) { }
|
||||
|
||||
/** Selects this step component. */
|
||||
select(): void {
|
||||
this._stepper.selected = this;
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[cdkStepper]',
|
||||
})
|
||||
export class CdkStepper {
|
||||
/** The list of step components that the stepper is holding. */
|
||||
@ContentChildren(CdkStep) _steps: QueryList<CdkStep>;
|
||||
|
||||
/** The list of step headers of the steps in the stepper. */
|
||||
_stepHeader: QueryList<ElementRef>;
|
||||
|
||||
/** Whether the validity of previous steps should be checked or not. */
|
||||
@Input()
|
||||
get linear() { return this._linear; }
|
||||
set linear(value: any) { this._linear = coerceBooleanProperty(value); }
|
||||
private _linear = false;
|
||||
|
||||
/** The index of the selected step. */
|
||||
@Input()
|
||||
get selectedIndex() { return this._selectedIndex; }
|
||||
set selectedIndex(index: number) {
|
||||
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) {
|
||||
this._emitStepperSelectionEvent(index);
|
||||
this._focusIndex = this._selectedIndex;
|
||||
}
|
||||
}
|
||||
private _selectedIndex: number = 0;
|
||||
|
||||
/** The step that is selected. */
|
||||
@Input()
|
||||
get selected() { return this._steps[this.selectedIndex]; }
|
||||
set selected(step: CdkStep) {
|
||||
let index = this._steps.toArray().indexOf(step);
|
||||
this.selectedIndex = index;
|
||||
}
|
||||
|
||||
/** Event emitted when the selected step has changed. */
|
||||
@Output() selectionChange = new EventEmitter<StepperSelectionEvent>();
|
||||
|
||||
/** The index of the step that the focus can be set. */
|
||||
_focusIndex: number = 0;
|
||||
|
||||
/** Used to track unique ID for each stepper component. */
|
||||
_groupId: number;
|
||||
|
||||
constructor(@Optional() private _dir: Directionality) {
|
||||
this._groupId = nextId++;
|
||||
}
|
||||
|
||||
/** Selects and focuses the next step in list. */
|
||||
next(): void {
|
||||
this.selectedIndex = Math.min(this._selectedIndex + 1, this._steps.length - 1);
|
||||
}
|
||||
|
||||
/** Selects and focuses the previous step in list. */
|
||||
previous(): void {
|
||||
this.selectedIndex = Math.max(this._selectedIndex - 1, 0);
|
||||
}
|
||||
|
||||
/** Returns a unique id for each step label element. */
|
||||
_getStepLabelId(i: number): string {
|
||||
return `mat-step-label-${this._groupId}-${i}`;
|
||||
}
|
||||
|
||||
/** Returns unique id for each step content element. */
|
||||
_getStepContentId(i: number): string {
|
||||
return `mat-step-content-${this._groupId}-${i}`;
|
||||
}
|
||||
|
||||
/** Returns position state of the step with the given index. */
|
||||
_getAnimationDirection(index: number): StepContentPositionState {
|
||||
const position = index - this._selectedIndex;
|
||||
if (position < 0) {
|
||||
return 'previous';
|
||||
} else if (position > 0) {
|
||||
return 'next';
|
||||
} else {
|
||||
return 'current';
|
||||
}
|
||||
}
|
||||
|
||||
/** 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({
|
||||
selectedIndex: newIndex,
|
||||
previouslySelectedIndex: this._selectedIndex,
|
||||
selectedStep: stepsArray[newIndex],
|
||||
previouslySelectedStep: stepsArray[this._selectedIndex],
|
||||
});
|
||||
this._selectedIndex = newIndex;
|
||||
}
|
||||
|
||||
_onKeydown(event: KeyboardEvent) {
|
||||
switch (event.keyCode) {
|
||||
case RIGHT_ARROW:
|
||||
if (this._dir && this._dir.value === 'rtl') {
|
||||
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
|
||||
} else {
|
||||
this._focusStep((this._focusIndex + 1) % this._steps.length);
|
||||
}
|
||||
break;
|
||||
case LEFT_ARROW:
|
||||
if (this._dir && this._dir.value === 'rtl') {
|
||||
this._focusStep((this._focusIndex + 1) % this._steps.length);
|
||||
} else {
|
||||
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
|
||||
}
|
||||
break;
|
||||
case SPACE:
|
||||
case ENTER:
|
||||
this.selectedIndex = this._focusIndex;
|
||||
break;
|
||||
default:
|
||||
// Return to avoid calling preventDefault on keys that are not explicitly handled.
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private _focusStep(index: number) {
|
||||
this._focusIndex = index;
|
||||
this._stepHeader.toArray()[this._focusIndex].nativeElement.focus();
|
||||
}
|
||||
|
||||
private _anyControlsInvalid(index: number): boolean {
|
||||
const stepsArray = this._steps.toArray();
|
||||
stepsArray[this._selectedIndex].interacted = true;
|
||||
if (this._linear) {
|
||||
return stepsArray.slice(0, index).some(step => step.stepControl.invalid);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig-build",
|
||||
"files": [
|
||||
"public_api.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true,
|
||||
"flatModuleOutFile": "index.js",
|
||||
"flatModuleId": "@angular/cdk/stepper",
|
||||
"skipTemplateCodegen": true
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ export class DemoApp {
|
||||
{name: 'Slider', route: '/slider'},
|
||||
{name: 'Slide Toggle', route: '/slide-toggle'},
|
||||
{name: 'Snack Bar', route: '/snack-bar'},
|
||||
{name: 'Stepper', route: 'stepper'},
|
||||
{name: 'Table', route: '/table'},
|
||||
{name: 'Tabs', route: '/tabs'},
|
||||
{name: 'Toolbar', route: '/toolbar'},
|
||||
|
||||
@@ -40,6 +40,7 @@ import {PeopleDatabase} from '../table/people-database';
|
||||
import {DatepickerDemo} from '../datepicker/datepicker-demo';
|
||||
import {TypographyDemo} from '../typography/typography-demo';
|
||||
import {ExpansionDemo} from '../expansion/expansion-demo';
|
||||
import {StepperDemo} from '../stepper/stepper-demo';
|
||||
import {DemoMaterialModule} from '../demo-material-module';
|
||||
import {
|
||||
FullscreenOverlayContainer,
|
||||
@@ -92,6 +93,7 @@ import {TableHeaderDemo} from '../table/table-header-demo';
|
||||
SliderDemo,
|
||||
SlideToggleDemo,
|
||||
SpagettiPanel,
|
||||
StepperDemo,
|
||||
StyleDemo,
|
||||
TableHeaderDemo,
|
||||
ToolbarDemo,
|
||||
|
||||
@@ -36,6 +36,7 @@ import {DatepickerDemo} from '../datepicker/datepicker-demo';
|
||||
import {TableDemo} from '../table/table-demo';
|
||||
import {TypographyDemo} from '../typography/typography-demo';
|
||||
import {ExpansionDemo} from '../expansion/expansion-demo';
|
||||
import {StepperDemo} from '../stepper/stepper-demo';
|
||||
import {DemoApp} from './demo-app';
|
||||
import {AccessibilityDemo} from '../a11y/a11y';
|
||||
import {ACCESSIBILITY_DEMO_ROUTES} from '../a11y/routes';
|
||||
@@ -78,6 +79,7 @@ export const DEMO_APP_ROUTES: Routes = [
|
||||
{path: 'style', component: StyleDemo},
|
||||
{path: 'typography', component: TypographyDemo},
|
||||
{path: 'expansion', component: ExpansionDemo},
|
||||
{path: 'stepper', component: StepperDemo}
|
||||
]}
|
||||
];
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ import {
|
||||
MdTabsModule,
|
||||
MdToolbarModule,
|
||||
MdTooltipModule,
|
||||
StyleModule
|
||||
StyleModule,
|
||||
MdStepperModule,
|
||||
} from '@angular/material';
|
||||
import {CdkTableModule} from '@angular/cdk/table';
|
||||
import {A11yModule} from '@angular/cdk/a11y';
|
||||
@@ -73,6 +74,7 @@ import {PortalModule} from '@angular/cdk/portal';
|
||||
MdSliderModule,
|
||||
MdSnackBarModule,
|
||||
MdSortModule,
|
||||
MdStepperModule,
|
||||
MdTabsModule,
|
||||
MdToolbarModule,
|
||||
MdTooltipModule,
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<md-checkbox [(ngModel)]="isNonLinear">Disable linear mode</md-checkbox>
|
||||
|
||||
<h3>Linear Vertical Stepper Demo using a single form</h3>
|
||||
<form [formGroup]="formGroup">
|
||||
<md-vertical-stepper formArrayName="formArray" [linear]="!isNonLinear">
|
||||
<md-step formGroupName="0" [stepControl]="formArray?.get([0])">
|
||||
<ng-template mdStepLabel>Fill out your name</ng-template>
|
||||
<md-input-container>
|
||||
<input mdInput placeholder="First Name" formControlName="firstNameFormCtrl" required>
|
||||
<md-error>This field is required</md-error>
|
||||
</md-input-container>
|
||||
|
||||
<md-input-container>
|
||||
<input mdInput placeholder="Last Name" formControlName="lastNameFormCtrl" required>
|
||||
<md-error>This field is required</md-error>
|
||||
</md-input-container>
|
||||
<div>
|
||||
<button md-button mdStepperNext type="button">Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
|
||||
<md-step formGroupName="1" [stepControl]="formArray?.get([1])" optional>
|
||||
<ng-template mdStepLabel>
|
||||
<div>Fill out your email address</div>
|
||||
</ng-template>
|
||||
<md-input-container>
|
||||
<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>
|
||||
<button md-button mdStepperNext type="button">Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
|
||||
<md-step>
|
||||
<ng-template mdStepLabel>Confirm your information</ng-template>
|
||||
Everything seems correct.
|
||||
<div>
|
||||
<button md-button>Done</button>
|
||||
</div>
|
||||
</md-step>
|
||||
</md-vertical-stepper>
|
||||
</form>
|
||||
|
||||
<h3>Linear Horizontal Stepper Demo using a different form for each step</h3>
|
||||
<md-horizontal-stepper [linear]="!isNonLinear">
|
||||
<md-step [stepControl]="nameFormGroup">
|
||||
<form [formGroup]="nameFormGroup">
|
||||
<ng-template mdStepLabel>Fill out your name</ng-template>
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="First Name" formControlName="firstNameCtrl" required>
|
||||
<md-error>This field is required</md-error>
|
||||
</md-form-field>
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="Last Name" formControlName="lastNameCtrl" required>
|
||||
<md-error>This field is required</md-error>
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</md-step>
|
||||
|
||||
<md-step [stepControl]="emailFormGroup" optional>
|
||||
<form [formGroup]="emailFormGroup">
|
||||
<ng-template mdStepLabel>Fill out your email address</ng-template>
|
||||
<md-form-field>
|
||||
<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>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</md-step>
|
||||
|
||||
<md-step>
|
||||
<form>
|
||||
<ng-template mdStepLabel>Confirm your information</ng-template>
|
||||
Everything seems correct.
|
||||
<div>
|
||||
<button md-button>Done</button>
|
||||
</div>
|
||||
</form>
|
||||
</md-step>
|
||||
</md-horizontal-stepper>
|
||||
|
||||
<h3>Vertical Stepper Demo</h3>
|
||||
<md-checkbox [(ngModel)]="isNonEditable">Make steps non-editable</md-checkbox>
|
||||
<md-vertical-stepper>
|
||||
<md-step [editable]="!isNonEditable">
|
||||
<ng-template mdStepLabel>Fill out your name</ng-template>
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="First Name">
|
||||
</md-form-field>
|
||||
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="Last Name">
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperNext type="button">Next</button>
|
||||
</div>
|
||||
</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-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperPrevious type="button">Back</button>
|
||||
<button md-button mdStepperNext type="button">Next</button>
|
||||
</div>
|
||||
</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-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperPrevious type="button">Back</button>
|
||||
<button md-button mdStepperNext type="button">Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
|
||||
<md-step>
|
||||
<ng-template mdStepLabel>Confirm your information</ng-template>
|
||||
Everything seems correct.
|
||||
<div>
|
||||
<button md-button>Done</button>
|
||||
</div>
|
||||
</md-step>
|
||||
</md-vertical-stepper>
|
||||
|
||||
<h3>Horizontal Stepper Demo with Text Label</h3>
|
||||
<md-horizontal-stepper>
|
||||
<md-step label="Fill out your name">
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="First Name">
|
||||
</md-form-field>
|
||||
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="Last Name">
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperNext type="button">Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
|
||||
<md-step label="Fill out your phone number">
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="Phone number">
|
||||
</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 label="Fill out your address">
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="Address">
|
||||
</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 label="Confirm your information">
|
||||
Everything seems correct.
|
||||
<div>
|
||||
<button md-button>Done</button>
|
||||
</div>
|
||||
</md-step>
|
||||
</md-horizontal-stepper>
|
||||
|
||||
<h3>Horizontal Stepper Demo with Templated Label</h3>
|
||||
<md-horizontal-stepper>
|
||||
<md-step *ngFor="let step of steps">
|
||||
<ng-template mdStepLabel>{{step.label}}</ng-template>
|
||||
<md-form-field>
|
||||
<input mdInput placeholder="Answer" [(ngModel)]="step.content">
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
</md-horizontal-stepper>
|
||||
@@ -0,0 +1,53 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {AbstractControl, 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',
|
||||
templateUrl: 'stepper-demo.html'
|
||||
})
|
||||
export class StepperDemo {
|
||||
formGroup: FormGroup;
|
||||
isNonLinear = false;
|
||||
isNonEditable = false;
|
||||
|
||||
nameFormGroup: FormGroup;
|
||||
emailFormGroup: FormGroup;
|
||||
|
||||
steps = [
|
||||
{label: 'Confirm your name', content: 'Last name, First name.'},
|
||||
{label: 'Confirm your contact information', content: '123-456-7890'},
|
||||
{label: 'Confirm your address', content: '1600 Amphitheater Pkwy MTV'},
|
||||
{label: 'You are now done', content: 'Finished!'}
|
||||
];
|
||||
|
||||
/** Returns a FormArray with the name 'formArray'. */
|
||||
get formArray(): AbstractControl | null { return this.formGroup.get('formArray'); }
|
||||
|
||||
constructor(private _formBuilder: FormBuilder) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.formGroup = this._formBuilder.group({
|
||||
formArray: this._formBuilder.array([
|
||||
this._formBuilder.group({
|
||||
firstNameFormCtrl: ['', Validators.required],
|
||||
lastNameFormCtrl: ['', Validators.required],
|
||||
}),
|
||||
this._formBuilder.group({
|
||||
emailFormCtrl: ['', Validators.pattern(EMAIL_REGEX)]
|
||||
}),
|
||||
])
|
||||
});
|
||||
|
||||
this.nameFormGroup = this._formBuilder.group({
|
||||
firstNameCtrl: ['', Validators.required],
|
||||
lastNameCtrl: ['', Validators.required],
|
||||
});
|
||||
|
||||
this.emailFormGroup = this._formBuilder.group({
|
||||
emailCtrl: ['', Validators.pattern(EMAIL_REGEX)]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ System.config({
|
||||
'@angular/cdk/portal': 'dist/bundles/cdk-portal.umd.js',
|
||||
'@angular/cdk/rxjs': 'dist/bundles/cdk-rxjs.umd.js',
|
||||
'@angular/cdk/scrolling': 'dist/bundles/cdk-scrolling.umd.js',
|
||||
'@angular/cdk/stepper': 'dist/bundles/cdk-stepper.umd.js',
|
||||
'@angular/cdk/table': 'dist/bundles/cdk-table.umd.js',
|
||||
'@angular/cdk/testing': 'dist/bundles/cdk-testing.umd.js',
|
||||
},
|
||||
|
||||
@@ -38,6 +38,7 @@ System.config({
|
||||
'@angular/cdk/portal': 'dist/bundles/cdk-portal.umd.js',
|
||||
'@angular/cdk/rxjs': 'dist/bundles/cdk-rxjs.umd.js',
|
||||
'@angular/cdk/scrolling': 'dist/bundles/cdk-scrolling.umd.js',
|
||||
'@angular/cdk/stepper': 'dist/bundles/cdk-stepper.umd.js',
|
||||
'@angular/cdk/table': 'dist/bundles/cdk-table.umd.js',
|
||||
'@angular/cdk/testing': 'dist/bundles/cdk-testing.umd.js',
|
||||
'@angular/material-examples': 'dist/bundles/material-examples.umd.js',
|
||||
|
||||
@@ -39,6 +39,9 @@ export const MAT_ELEMENTS_SELECTOR = `
|
||||
[matHeaderRowDef],
|
||||
[matLine],
|
||||
[matRowDef],
|
||||
[matStepLabel],
|
||||
[matStepperNext],
|
||||
[matStepperPrevious],
|
||||
[matTabLabel],
|
||||
[matTabLink],
|
||||
[matTabNav],
|
||||
@@ -73,6 +76,7 @@ export const MAT_ELEMENTS_SELECTOR = `
|
||||
mat-header-cell,
|
||||
mat-header-row,
|
||||
mat-hint,
|
||||
mat-horizontal-stepper,
|
||||
mat-icon,
|
||||
mat-input-container,
|
||||
mat-form-field,
|
||||
@@ -92,10 +96,12 @@ export const MAT_ELEMENTS_SELECTOR = `
|
||||
mat-sidenav-container,
|
||||
mat-slider,
|
||||
mat-spinner,
|
||||
mat-step,
|
||||
mat-tab,
|
||||
mat-table,
|
||||
mat-tab-group,
|
||||
mat-toolbar`;
|
||||
mat-toolbar,
|
||||
mat-vertical-stepper`;
|
||||
|
||||
/** Selector that matches all elements that may have style collisions with AngularJS Material. */
|
||||
export const MD_ELEMENTS_SELECTOR = `
|
||||
@@ -116,6 +122,9 @@ export const MD_ELEMENTS_SELECTOR = `
|
||||
[mdHeaderRowDef],
|
||||
[mdLine],
|
||||
[mdRowDef],
|
||||
[mdStepLabel],
|
||||
[mdStepperNext],
|
||||
[mdStepperPrevious],
|
||||
[mdTabLabel],
|
||||
[mdTabLink],
|
||||
[mdTabNav],
|
||||
@@ -150,6 +159,7 @@ export const MD_ELEMENTS_SELECTOR = `
|
||||
md-header-cell,
|
||||
md-header-row,
|
||||
md-hint,
|
||||
md-horizontal-stepper,
|
||||
md-icon,
|
||||
md-input-container,
|
||||
md-form-field,
|
||||
@@ -169,10 +179,12 @@ export const MD_ELEMENTS_SELECTOR = `
|
||||
md-sidenav-container,
|
||||
md-slider,
|
||||
md-spinner,
|
||||
md-step,
|
||||
md-tab,
|
||||
md-table,
|
||||
md-tab-group,
|
||||
md-toolbar`;
|
||||
md-toolbar,
|
||||
md-vertical-stepper`;
|
||||
|
||||
/** Directive that enforces that the `mat-` prefix cannot be used. */
|
||||
@Directive({selector: MAT_ELEMENTS_SELECTOR})
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
@import '../../sidenav/sidenav-theme';
|
||||
@import '../../slide-toggle/slide-toggle-theme';
|
||||
@import '../../slider/slider-theme';
|
||||
@import '../../stepper/stepper-theme';
|
||||
@import '../../tabs/tabs-theme';
|
||||
@import '../../toolbar/toolbar-theme';
|
||||
@import '../../tooltip/tooltip-theme';
|
||||
@@ -57,6 +58,7 @@
|
||||
@include mat-sidenav-theme($theme);
|
||||
@include mat-slide-toggle-theme($theme);
|
||||
@include mat-slider-theme($theme);
|
||||
@include mat-stepper-theme($theme);
|
||||
@include mat-tabs-theme($theme);
|
||||
@include mat-toolbar-theme($theme);
|
||||
@include mat-tooltip-theme($theme);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
@import '../../sidenav/sidenav-theme';
|
||||
@import '../../slide-toggle/slide-toggle-theme';
|
||||
@import '../../slider/slider-theme';
|
||||
@import '../../stepper/stepper-theme';
|
||||
@import '../../tabs/tabs-theme';
|
||||
@import '../../toolbar/toolbar-theme';
|
||||
@import '../../tooltip/tooltip-theme';
|
||||
@@ -61,6 +62,7 @@
|
||||
@include mat-sidenav-typography($config);
|
||||
@include mat-slide-toggle-typography($config);
|
||||
@include mat-slider-typography($config);
|
||||
@include mat-stepper-typography($config);
|
||||
@include mat-tabs-typography($config);
|
||||
@include mat-toolbar-typography($config);
|
||||
@include mat-tooltip-typography($config);
|
||||
|
||||
+3
-1
@@ -46,6 +46,7 @@ import {MdTableModule} from './table/index';
|
||||
import {MdSortModule} from './sort/index';
|
||||
import {MdPaginatorModule} from './paginator/index';
|
||||
import {MdFormFieldModule} from './form-field/index';
|
||||
import {MdStepperModule} from './stepper/index';
|
||||
|
||||
const MATERIAL_MODULES = [
|
||||
MdAutocompleteModule,
|
||||
@@ -75,6 +76,7 @@ const MATERIAL_MODULES = [
|
||||
MdSlideToggleModule,
|
||||
MdSnackBarModule,
|
||||
MdSortModule,
|
||||
MdStepperModule,
|
||||
MdTabsModule,
|
||||
MdToolbarModule,
|
||||
MdTooltipModule,
|
||||
@@ -85,7 +87,7 @@ const MATERIAL_MODULES = [
|
||||
A11yModule,
|
||||
PlatformModule,
|
||||
MdCommonModule,
|
||||
ObserversModule
|
||||
ObserversModule,
|
||||
];
|
||||
|
||||
/** @deprecated */
|
||||
|
||||
@@ -46,3 +46,4 @@ export * from './tabs/index';
|
||||
export * from './tabs/tab-nav-bar/index';
|
||||
export * from './toolbar/index';
|
||||
export * from './tooltip/index';
|
||||
export * from './stepper/index';
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
@import '../core/theming/palette';
|
||||
@import '../core/theming/theming';
|
||||
@import '../core/typography/typography-utils';
|
||||
|
||||
@mixin mat-stepper-theme($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
$primary: map-get($theme, primary);
|
||||
|
||||
.mat-step-header {
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: mat-color($background, hover);
|
||||
}
|
||||
|
||||
.mat-step-label-active {
|
||||
color: mat-color($foreground, text);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.mat-step-icon-not-touched {
|
||||
background-color: mat-color($foreground, disabled-text);
|
||||
color: mat-color($primary, default-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-stepper-horizontal, .mat-stepper-vertical {
|
||||
background-color: mat-color($background, card);
|
||||
}
|
||||
|
||||
.mat-stepper-vertical-line::before {
|
||||
border-left-color: mat-color($foreground, divider);
|
||||
}
|
||||
|
||||
.mat-stepper-horizontal-line {
|
||||
border-top-color: mat-color($foreground, divider);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mat-stepper-typography($config) {
|
||||
.mat-stepper-vertical, .mat-stepper-horizontal {
|
||||
font-family: mat-font-family($config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {PortalModule} from '@angular/cdk/portal';
|
||||
import {MdButtonModule} from '../button/index';
|
||||
import {MdStep, MdStepper, MdHorizontalStepper, MdVerticalStepper} from './stepper';
|
||||
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,
|
||||
MdIconModule
|
||||
],
|
||||
exports: [MdCommonModule, MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper,
|
||||
MdStepperNext, MdStepperPrevious, MdStepHeader],
|
||||
declarations: [MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper,
|
||||
MdStepperNext, MdStepperPrevious, MdStepHeader],
|
||||
})
|
||||
export class MdStepperModule {}
|
||||
|
||||
export * from './step-label';
|
||||
export * from './stepper';
|
||||
export * from './stepper-button';
|
||||
export * from './step-header';
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
$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;
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {Component, Input, ViewEncapsulation} from '@angular/core';
|
||||
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
|
||||
import {MdStepLabel} from './step-label';
|
||||
|
||||
@Component({
|
||||
selector: 'md-step-header, mat-step-header',
|
||||
templateUrl: 'step-header.html',
|
||||
styleUrls: ['step-header.css'],
|
||||
host: {
|
||||
'class': 'mat-step-header',
|
||||
'role': 'tab',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MdStepHeader {
|
||||
/** Icon for the given step. */
|
||||
@Input()
|
||||
icon: string;
|
||||
|
||||
/** Label of the given step. */
|
||||
@Input()
|
||||
label: MdStepLabel | string;
|
||||
|
||||
/** Index of the given step. */
|
||||
@Input()
|
||||
get index() { return this._index; }
|
||||
set index(value: any) {
|
||||
this._index = coerceNumberProperty(value);
|
||||
}
|
||||
private _index: number;
|
||||
|
||||
/** Whether the given step is selected. */
|
||||
@Input()
|
||||
get selected() { return this._selected; }
|
||||
set selected(value: any) {
|
||||
this._selected = coerceBooleanProperty(value);
|
||||
}
|
||||
private _selected: boolean;
|
||||
|
||||
/** Whether the given step label is active. */
|
||||
@Input()
|
||||
get active() { return this._active; }
|
||||
set active(value: any) {
|
||||
this._active = coerceBooleanProperty(value);
|
||||
}
|
||||
private _active: boolean;
|
||||
|
||||
/** Whether the given step is optional. */
|
||||
@Input()
|
||||
get optional() { return this._optional; }
|
||||
set optional(value: any) {
|
||||
this._optional = coerceBooleanProperty(value);
|
||||
}
|
||||
private _optional: boolean;
|
||||
|
||||
/** Returns string label of given step if it is a text label. */
|
||||
get _stringLabel(): string | null {
|
||||
return this.label instanceof MdStepLabel ? null : this.label;
|
||||
}
|
||||
|
||||
/** Returns MdStepLabel if the label of given step is a template label. */
|
||||
get _templateLabel(): MdStepLabel | null {
|
||||
return this.label instanceof MdStepLabel ? this.label : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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, TemplateRef} from '@angular/core';
|
||||
import {CdkStepLabel} from '@angular/cdk/stepper';
|
||||
|
||||
/** Workaround for https://github.com/angular/angular/issues/17849 */
|
||||
export const _MdStepLabel = CdkStepLabel;
|
||||
|
||||
@Directive({
|
||||
selector: '[mdStepLabel], [matStepLabel]',
|
||||
})
|
||||
export class MdStepLabel extends _MdStepLabel {
|
||||
constructor(template: TemplateRef<any>) {
|
||||
super(template);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ng-template><ng-content></ng-content></ng-template>
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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} from '@angular/core';
|
||||
import {CdkStepper, CdkStepperNext, CdkStepperPrevious} from '@angular/cdk/stepper';
|
||||
import {MdStepper} from './stepper';
|
||||
|
||||
/** Workaround for https://github.com/angular/angular/issues/17849 */
|
||||
export const _MdStepperNext = CdkStepperNext;
|
||||
export const _MdStepperPrevious = CdkStepperPrevious;
|
||||
|
||||
/** Button that moves to the next step in a stepper workflow. */
|
||||
@Directive({
|
||||
selector: 'button[mdStepperNext], button[matStepperNext]',
|
||||
host: {'(click)': '_stepper.next()'},
|
||||
providers: [{provide: CdkStepper, useExisting: MdStepper}]
|
||||
})
|
||||
export class MdStepperNext extends _MdStepperNext { }
|
||||
|
||||
/** Button that moves to the previous step in a stepper workflow. */
|
||||
@Directive({
|
||||
selector: 'button[mdStepperPrevious], button[matStepperPrevious]',
|
||||
host: {'(click)': '_stepper.previous()'},
|
||||
providers: [{provide: CdkStepper, useExisting: MdStepper}]
|
||||
})
|
||||
export class MdStepperPrevious extends _MdStepperPrevious { }
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="mat-horizontal-stepper-header-container">
|
||||
<ng-container *ngFor="let step of _steps; let i = index; let isLast = last">
|
||||
<md-step-header class="mat-horizontal-stepper-header"
|
||||
(click)="step.select()"
|
||||
(keydown)="_onKeydown($event)"
|
||||
[tabIndex]="_focusIndex == i ? 0 : -1"
|
||||
[id]="_getStepLabelId(i)"
|
||||
[attr.aria-controls]="_getStepContentId(i)"
|
||||
[attr.aria-selected]="selectedIndex == i"
|
||||
[index]="i"
|
||||
[icon]="_getIndicatorType(i)"
|
||||
[label]="step.stepLabel || step.label"
|
||||
[selected]="selectedIndex == i"
|
||||
[active]="step.completed || selectedIndex == i"
|
||||
[optional]="step.optional">
|
||||
</md-step-header>
|
||||
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="mat-horizontal-content-container">
|
||||
<div *ngFor="let step of _steps; let i = index"
|
||||
class="mat-horizontal-stepper-content" role="tabpanel"
|
||||
[@stepTransition]="_getAnimationDirection(i)"
|
||||
[id]="_getStepContentId(i)"
|
||||
[attr.aria-labelledby]="_getStepLabelId(i)"
|
||||
[attr.aria-expanded]="selectedIndex == i">
|
||||
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="mat-step" *ngFor="let step of _steps; let i = index; let isLast = last">
|
||||
<md-step-header class="mat-vertical-stepper-header"
|
||||
(click)="step.select()"
|
||||
(keydown)="_onKeydown($event)"
|
||||
[tabIndex]="_focusIndex == i ? 0 : -1"
|
||||
[id]="_getStepLabelId(i)"
|
||||
[attr.aria-controls]="_getStepContentId(i)"
|
||||
[attr.aria-selected]="selectedIndex == i"
|
||||
[index]="i"
|
||||
[icon]="_getIndicatorType(i)"
|
||||
[label]="step.stepLabel || step.label"
|
||||
[selected]="selectedIndex == i"
|
||||
[active]="step.completed || selectedIndex == i"
|
||||
[optional]="step.optional">
|
||||
</md-step-header>
|
||||
|
||||
<div class="mat-vertical-content-container" [class.mat-stepper-vertical-line]="!isLast">
|
||||
<div class="mat-vertical-stepper-content" role="tabpanel"
|
||||
[@stepTransition]="_getAnimationDirection(i)"
|
||||
[id]="_getStepContentId(i)"
|
||||
[attr.aria-labelledby]="_getStepLabelId(i)"
|
||||
[attr.aria-expanded]="selectedIndex == i">
|
||||
<div class="mat-vertical-content">
|
||||
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,132 @@
|
||||
Angular Material's stepper provides a wizard-like workflow by dividing content into logical steps.
|
||||
|
||||
Material stepper builds on the foundation of the CDK stepper that is responsible for the logic
|
||||
that drives a stepped workflow. Material stepper extends the CDK stepper and has Material Design
|
||||
styling.
|
||||
|
||||
### Stepper variants
|
||||
There are two stepper components: `md-horizontal-stepper` and `md-vertical-stepper`. They
|
||||
can be used the same way. The only difference is the orientation of stepper.
|
||||
`md-horizontal-stepper` selector can be used to create a horizontal stepper, and
|
||||
`md-vertical-stepper` can be used to create a vertical stepper. `md-step` components need to be
|
||||
placed inside either one of the two stepper components.
|
||||
|
||||
### Labels
|
||||
If a step's label is only text, then the `label` attribute can be used.
|
||||
```html
|
||||
<md-vertical-stepper>
|
||||
<md-step label="Step 1">
|
||||
Content 1
|
||||
</md-step>
|
||||
<md-step label="Step 1">
|
||||
Content 2
|
||||
</md-step>
|
||||
</md-vertical-stepper>
|
||||
```
|
||||
|
||||
For more complex labels, add a template with the `mdStepLabel` directive inside the
|
||||
`md-step`.
|
||||
```html
|
||||
<md-vertical-stepper>
|
||||
<md-step>
|
||||
<ng-template mdStepLabel>...</ng-template>
|
||||
...
|
||||
</md-step>
|
||||
</md-vertical-stepper>
|
||||
```
|
||||
|
||||
### Stepper buttons
|
||||
There are two button directives to support navigation between different steps:
|
||||
`mdStepperPrevious` and `mdStepperNext`.
|
||||
```html
|
||||
<md-horizontal-stepper>
|
||||
<md-step>
|
||||
...
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
</md-horizontal-stepper>
|
||||
```
|
||||
|
||||
### Linear stepper
|
||||
The `linear` attribute can be set on `md-horizontal-stepper` and `md-vertical-stepper` to create
|
||||
a linear stepper that requires the user to complete previous steps before proceeding
|
||||
to following steps. For each `md-step`, the `stepControl` attribute can be set to the top level
|
||||
`AbstractControl` that is used to check the validity of the step.
|
||||
|
||||
There are two possible approaches. One is using a single form for stepper, and the other is
|
||||
using a different form for each step.
|
||||
|
||||
#### Using a single form
|
||||
When using a single form for the stepper, `mdStepperPrevious` and `mdStepperNext` have to be
|
||||
set to `type="button"` in order to prevent submission of the form before all steps
|
||||
are completed.
|
||||
|
||||
```html
|
||||
<form [formGroup]="formGroup">
|
||||
<md-horizontal-stepper formArrayName="formArray" linear>
|
||||
<md-step formGroupName="0" [stepControl]="formArray.get([0])">
|
||||
...
|
||||
<div>
|
||||
<button md-button mdStepperNext type="button">Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
<md-step formGroupName="1" [stepControl]="formArray.get([1])">
|
||||
...
|
||||
<div>
|
||||
<button md-button mdStepperPrevious type="button">Back</button>
|
||||
<button md-button mdStepperNext type="button">Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
...
|
||||
</md-horizontal-stepper>
|
||||
</form>
|
||||
```
|
||||
|
||||
#### Using a different form for each step
|
||||
```html
|
||||
<md-vertical-stepper linear>
|
||||
<md-step [stepControl]="formGroup1">
|
||||
<form [formGroup]="formGroup1">
|
||||
...
|
||||
</form>
|
||||
</md-step>
|
||||
<md-step [stepControl]="formGroup2">
|
||||
<form [formGroup]="formGroup2">
|
||||
...
|
||||
</form>
|
||||
</md-step>
|
||||
</md-vertical-stepper>
|
||||
```
|
||||
### Types of steps
|
||||
|
||||
#### Optional step
|
||||
If completion of a step in linear stepper is not required, then the `optional` attribute can be set
|
||||
on `md-step`.
|
||||
|
||||
#### Editable step
|
||||
By default, steps are editable, which means users can return to previously completed steps and
|
||||
edit their responses. `editable="true"` can be set on `md-step` to change the default.
|
||||
|
||||
#### Completed step
|
||||
By default, the `completed` attribute of a step returns `true` if the step is valid (in case of
|
||||
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.
|
||||
|
||||
### Keyboard interaction
|
||||
- <kbd>LEFT_ARROW</kbd>: Focuses the previous step header
|
||||
- <kbd>RIGHT_ARROW</kbd>: Focuses the next step header
|
||||
- <kbd>ENTER</kbd>, <kbd>SPACE</kbd>: Selects the step that the focus is currently on
|
||||
- <kbd>TAB</kbd>: Focuses the next tabbable element
|
||||
- <kbd>TAB</kbd>+<kbd>SHIFT</kbd>: Focuses the previous tabbable element
|
||||
|
||||
### Accessibility
|
||||
The stepper is treated as a tabbed view for accessibility purposes, so it is given
|
||||
`role="tablist"` by default. The header of step that can be clicked to select the step
|
||||
is given `role="tab"`, and the content that can be expanded upon selection is given
|
||||
`role="tabpanel"`. `aria-selected` attribute of step header and `aria-expanded` attribute of
|
||||
step content is automatically set based on step selection change.
|
||||
|
||||
The stepper and each step should be given a meaningful label via `aria-label` or `aria-labelledby`.
|
||||
@@ -0,0 +1,122 @@
|
||||
@import '../core/style/variables';
|
||||
|
||||
$mat-horizontal-stepper-header-height: 72px !default;
|
||||
$mat-stepper-label-header-height: 24px !default;
|
||||
$mat-stepper-side-gap: 24px !default;
|
||||
$mat-vertical-stepper-content-margin: 36px !default;
|
||||
$mat-stepper-line-width: 1px !default;
|
||||
$mat-stepper-line-gap: 8px !default;
|
||||
|
||||
.mat-stepper-vertical,
|
||||
.mat-stepper-horizontal {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mat-step-header {
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mat-horizontal-stepper-header-container {
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mat-stepper-horizontal-line {
|
||||
border-top-width: $mat-stepper-line-width;
|
||||
border-top-style: solid;
|
||||
flex: auto;
|
||||
height: 0;
|
||||
margin: 0 $mat-stepper-line-gap - $mat-stepper-side-gap;
|
||||
min-width: $mat-stepper-line-gap + $mat-stepper-side-gap;
|
||||
}
|
||||
|
||||
.mat-horizontal-stepper-header {
|
||||
display: flex;
|
||||
height: $mat-horizontal-stepper-header-height;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
padding: 0 $mat-stepper-side-gap;
|
||||
|
||||
.mat-step-icon,
|
||||
.mat-step-icon-not-touched {
|
||||
margin-right: $mat-stepper-line-gap;
|
||||
flex: none;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin-right: 0;
|
||||
margin-left: $mat-stepper-line-gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-vertical-stepper-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $mat-stepper-side-gap;
|
||||
max-height: $mat-stepper-label-header-height;
|
||||
|
||||
.mat-step-icon,
|
||||
.mat-step-icon-not-touched {
|
||||
margin-right: $mat-vertical-stepper-content-margin - $mat-stepper-side-gap;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin-right: 0;
|
||||
margin-left: $mat-vertical-stepper-content-margin - $mat-stepper-side-gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-horizontal-stepper-content {
|
||||
overflow: hidden;
|
||||
|
||||
&[aria-expanded='false'] {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-horizontal-content-container {
|
||||
overflow: hidden;
|
||||
padding: 0 $mat-stepper-side-gap $mat-stepper-side-gap $mat-stepper-side-gap;
|
||||
}
|
||||
|
||||
.mat-vertical-content-container {
|
||||
margin-left: $mat-vertical-stepper-content-margin;
|
||||
border: 0;
|
||||
position: relative;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin-left: 0;
|
||||
margin-right: $mat-vertical-stepper-content-margin;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-stepper-vertical-line::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: $mat-stepper-line-gap - $mat-stepper-side-gap;
|
||||
bottom: $mat-stepper-line-gap - $mat-stepper-side-gap;
|
||||
left: 0;
|
||||
border-left-width: $mat-stepper-line-width;
|
||||
border-left-style: solid;
|
||||
|
||||
[dir='rtl'] & {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-vertical-stepper-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mat-vertical-content {
|
||||
padding: 0 $mat-stepper-side-gap $mat-stepper-side-gap $mat-stepper-side-gap;
|
||||
}
|
||||
|
||||
.mat-step:last-child {
|
||||
.mat-vertical-content-container {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,765 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component, DebugElement} from '@angular/core';
|
||||
import {MdStepperModule} from './index';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {MdStepperNext, MdStepperPrevious} from './stepper-button';
|
||||
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
|
||||
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
|
||||
import {MdStepper, MdHorizontalStepper, MdVerticalStepper} from './stepper';
|
||||
|
||||
const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
|
||||
describe('MdHorizontalStepper', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MdStepperModule, NoopAnimationsModule, ReactiveFormsModule],
|
||||
declarations: [
|
||||
SimpleMdHorizontalStepperApp,
|
||||
LinearMdHorizontalStepperApp
|
||||
]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
describe('basic horizontal stepper', () => {
|
||||
let fixture: ComponentFixture<SimpleMdHorizontalStepperApp>;
|
||||
let stepperComponent: MdHorizontalStepper;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SimpleMdHorizontalStepperApp);
|
||||
fixture.detectChanges();
|
||||
|
||||
stepperComponent = fixture.debugElement
|
||||
.query(By.css('md-horizontal-stepper')).componentInstance;
|
||||
});
|
||||
|
||||
it('should default to the first step', () => {
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should change selected index on header click', () => {
|
||||
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
|
||||
assertSelectionChangeOnHeaderClick(stepperComponent, fixture, stepHeaders);
|
||||
});
|
||||
|
||||
it('should set the "tablist" role on stepper', () => {
|
||||
let stepperEl = fixture.debugElement.query(By.css('md-horizontal-stepper')).nativeElement;
|
||||
expect(stepperEl.getAttribute('role')).toBe('tablist');
|
||||
});
|
||||
|
||||
it('should set aria-expanded of content correctly', () => {
|
||||
let stepContents = fixture.debugElement.queryAll(By.css(`.mat-horizontal-stepper-content`));
|
||||
assertCorrectAriaExpandedAttribute(stepperComponent, fixture, stepContents);
|
||||
});
|
||||
|
||||
it('should display the correct label', () => {
|
||||
assertCorrectStepLabel(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should go to next available step when the next button is clicked', () => {
|
||||
assertNextStepperButtonClick(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should go to previous available step when the previous button is clicked', () => {
|
||||
assertPreviousStepperButtonClick(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should set the correct step position for animation', () => {
|
||||
assertCorrectStepPosition(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should support keyboard events to move and select focus', () => {
|
||||
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
|
||||
assertCorrectKeyboardInteraction(stepperComponent, fixture, stepHeaders);
|
||||
});
|
||||
|
||||
it('should not set focus on header of selected step if header is not clicked', () => {
|
||||
assertStepHeaderFocusNotCalled(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should only be able to return to a previous step if it is editable', () => {
|
||||
assertEditableStepChange(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should set create icon if step is editable and completed', () => {
|
||||
assertCorrectStepIcon(stepperComponent, fixture, true, 'edit');
|
||||
});
|
||||
|
||||
it('should set done icon if step is not editable and is completed', () => {
|
||||
assertCorrectStepIcon(stepperComponent, fixture, false, 'done');
|
||||
});
|
||||
});
|
||||
|
||||
describe('linear horizontal stepper', () => {
|
||||
let fixture: ComponentFixture<LinearMdHorizontalStepperApp>;
|
||||
let testComponent: LinearMdHorizontalStepperApp;
|
||||
let stepperComponent: MdHorizontalStepper;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LinearMdHorizontalStepperApp);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
stepperComponent = fixture.debugElement
|
||||
.query(By.css('md-horizontal-stepper')).componentInstance;
|
||||
});
|
||||
|
||||
it('should have true linear attribute', () => {
|
||||
expect(stepperComponent.linear).toBe(true);
|
||||
});
|
||||
|
||||
it('should not move to next step if current step is not valid', () => {
|
||||
expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe('');
|
||||
expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false);
|
||||
expect(testComponent.oneGroup.valid).toBe(false);
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
|
||||
let stepHeaderEl = fixture.debugElement
|
||||
.queryAll(By.css('.mat-horizontal-stepper-header'))[1].nativeElement;
|
||||
assertLinearStepperValidity(stepHeaderEl, stepperComponent, testComponent, fixture);
|
||||
});
|
||||
|
||||
it('should not focus step header upon click if it is not able to be selected', () => {
|
||||
assertStepHeaderBlurred(fixture);
|
||||
});
|
||||
|
||||
it('should be able to move to next step even when invalid if current step is optional', () => {
|
||||
assertOptionalStepValidity(stepperComponent, testComponent, fixture);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MdVerticalStepper', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MdStepperModule, NoopAnimationsModule, ReactiveFormsModule],
|
||||
declarations: [
|
||||
SimpleMdVerticalStepperApp,
|
||||
LinearMdVerticalStepperApp
|
||||
]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
describe('basic vertical stepper', () => {
|
||||
let fixture: ComponentFixture<SimpleMdVerticalStepperApp>;
|
||||
let stepperComponent: MdVerticalStepper;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SimpleMdVerticalStepperApp);
|
||||
fixture.detectChanges();
|
||||
|
||||
stepperComponent = fixture.debugElement
|
||||
.query(By.css('md-vertical-stepper')).componentInstance;
|
||||
});
|
||||
|
||||
it('should default to the first step', () => {
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should change selected index on header click', () => {
|
||||
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
|
||||
assertSelectionChangeOnHeaderClick(stepperComponent, fixture, stepHeaders);
|
||||
|
||||
});
|
||||
|
||||
it('should set the "tablist" role on stepper', () => {
|
||||
let stepperEl = fixture.debugElement.query(By.css('md-vertical-stepper')).nativeElement;
|
||||
expect(stepperEl.getAttribute('role')).toBe('tablist');
|
||||
});
|
||||
|
||||
it('should set aria-expanded of content correctly', () => {
|
||||
let stepContents = fixture.debugElement.queryAll(By.css(`.mat-vertical-stepper-content`));
|
||||
assertCorrectAriaExpandedAttribute(stepperComponent, fixture, stepContents);
|
||||
});
|
||||
|
||||
it('should display the correct label', () => {
|
||||
assertCorrectStepLabel(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should go to next available step when the next button is clicked', () => {
|
||||
assertNextStepperButtonClick(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should go to previous available step when the previous button is clicked', () => {
|
||||
assertPreviousStepperButtonClick(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should set the correct step position for animation', () => {
|
||||
assertCorrectStepPosition(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should support keyboard events to move and select focus', () => {
|
||||
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
|
||||
assertCorrectKeyboardInteraction(stepperComponent, fixture, stepHeaders);
|
||||
});
|
||||
|
||||
it('should not set focus on header of selected step if header is not clicked', () => {
|
||||
assertStepHeaderFocusNotCalled(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should only be able to return to a previous step if it is editable', () => {
|
||||
assertEditableStepChange(stepperComponent, fixture);
|
||||
});
|
||||
|
||||
it('should set create icon if step is editable and completed', () => {
|
||||
assertCorrectStepIcon(stepperComponent, fixture, true, 'edit');
|
||||
});
|
||||
|
||||
it('should set done icon if step is not editable and is completed', () => {
|
||||
assertCorrectStepIcon(stepperComponent, fixture, false, 'done');
|
||||
});
|
||||
});
|
||||
|
||||
describe('linear vertical stepper', () => {
|
||||
let fixture: ComponentFixture<LinearMdVerticalStepperApp>;
|
||||
let testComponent: LinearMdVerticalStepperApp;
|
||||
let stepperComponent: MdVerticalStepper;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LinearMdVerticalStepperApp);
|
||||
fixture.detectChanges();
|
||||
|
||||
testComponent = fixture.componentInstance;
|
||||
stepperComponent = fixture.debugElement
|
||||
.query(By.css('md-vertical-stepper')).componentInstance;
|
||||
});
|
||||
|
||||
it('should have true linear attribute', () => {
|
||||
expect(stepperComponent.linear).toBe(true);
|
||||
});
|
||||
|
||||
it('should not move to next step if current step is not valid', () => {
|
||||
expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe('');
|
||||
expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false);
|
||||
expect(testComponent.oneGroup.valid).toBe(false);
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
|
||||
let stepHeaderEl = fixture.debugElement
|
||||
.queryAll(By.css('.mat-vertical-stepper-header'))[1].nativeElement;
|
||||
|
||||
assertLinearStepperValidity(stepHeaderEl, stepperComponent, testComponent, fixture);
|
||||
});
|
||||
|
||||
it('should not focus step header upon click if it is not able to be selected', () => {
|
||||
assertStepHeaderBlurred(fixture);
|
||||
});
|
||||
|
||||
it('should be able to move to next step even when invalid if current step is optional', () => {
|
||||
assertOptionalStepValidity(stepperComponent, testComponent, fixture);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** Asserts that `selectedIndex` updates correctly when header of another step is clicked. */
|
||||
function assertSelectionChangeOnHeaderClick(stepperComponent: MdStepper,
|
||||
fixture: ComponentFixture<any>,
|
||||
stepHeaders: DebugElement[]) {
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
|
||||
// select the second step
|
||||
let stepHeaderEl = stepHeaders[1].nativeElement;
|
||||
stepHeaderEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(1);
|
||||
|
||||
// select the third step
|
||||
stepHeaderEl = stepHeaders[2].nativeElement;
|
||||
stepHeaderEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(2);
|
||||
}
|
||||
|
||||
/** Asserts that 'aria-expanded' attribute is correct for expanded content of step. */
|
||||
function assertCorrectAriaExpandedAttribute(stepperComponent: MdStepper,
|
||||
fixture: ComponentFixture<any>,
|
||||
stepContents: DebugElement[]) {
|
||||
let firstStepContentEl = stepContents[0].nativeElement;
|
||||
expect(firstStepContentEl.getAttribute('aria-expanded')).toBe('true');
|
||||
|
||||
stepperComponent.selectedIndex = 1;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(firstStepContentEl.getAttribute('aria-expanded')).toBe('false');
|
||||
let secondStepContentEl = stepContents[1].nativeElement;
|
||||
expect(secondStepContentEl.getAttribute('aria-expanded')).toBe('true');
|
||||
}
|
||||
|
||||
/** Asserts that step has correct label. */
|
||||
function assertCorrectStepLabel(stepperComponent: MdStepper, fixture: ComponentFixture<any>) {
|
||||
let selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
|
||||
expect(selectedLabel.textContent).toMatch('Step 1');
|
||||
|
||||
stepperComponent.selectedIndex = 2;
|
||||
fixture.detectChanges();
|
||||
|
||||
selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
|
||||
expect(selectedLabel.textContent).toMatch('Step 3');
|
||||
|
||||
fixture.componentInstance.inputLabel = 'New Label';
|
||||
fixture.detectChanges();
|
||||
|
||||
selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
|
||||
expect(selectedLabel.textContent).toMatch('New Label');
|
||||
}
|
||||
|
||||
/** Asserts that clicking on MdStepperNext button updates `selectedIndex` correctly. */
|
||||
function assertNextStepperButtonClick(stepperComponent: MdStepper, fixture: ComponentFixture<any>) {
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
|
||||
let nextButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperNext))[0].nativeElement;
|
||||
nextButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(1);
|
||||
|
||||
nextButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperNext))[1].nativeElement;
|
||||
nextButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(2);
|
||||
|
||||
nextButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperNext))[2].nativeElement;
|
||||
nextButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(2);
|
||||
}
|
||||
|
||||
/** Asserts that clicking on MdStepperPrevious button updates `selectedIndex` correctly. */
|
||||
function assertPreviousStepperButtonClick(stepperComponent: MdStepper,
|
||||
fixture: ComponentFixture<any>) {
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
|
||||
stepperComponent.selectedIndex = 2;
|
||||
let previousButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperPrevious))[2].nativeElement;
|
||||
previousButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(1);
|
||||
|
||||
previousButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperPrevious))[1].nativeElement;
|
||||
previousButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
|
||||
previousButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperPrevious))[0].nativeElement;
|
||||
previousButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
}
|
||||
|
||||
/** Asserts that step position is correct for animation. */
|
||||
function assertCorrectStepPosition(stepperComponent: MdStepper, fixture: ComponentFixture<any>) {
|
||||
expect(stepperComponent._getAnimationDirection(0)).toBe('current');
|
||||
expect(stepperComponent._getAnimationDirection(1)).toBe('next');
|
||||
expect(stepperComponent._getAnimationDirection(2)).toBe('next');
|
||||
|
||||
stepperComponent.selectedIndex = 1;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent._getAnimationDirection(0)).toBe('previous');
|
||||
expect(stepperComponent._getAnimationDirection(1)).toBe('current');
|
||||
expect(stepperComponent._getAnimationDirection(2)).toBe('next');
|
||||
|
||||
stepperComponent.selectedIndex = 2;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent._getAnimationDirection(0)).toBe('previous');
|
||||
expect(stepperComponent._getAnimationDirection(1)).toBe('previous');
|
||||
expect(stepperComponent._getAnimationDirection(2)).toBe('current');
|
||||
}
|
||||
|
||||
/** Asserts that keyboard interaction works correctly. */
|
||||
function assertCorrectKeyboardInteraction(stepperComponent: MdStepper,
|
||||
fixture: ComponentFixture<any>,
|
||||
stepHeaders: DebugElement[]) {
|
||||
expect(stepperComponent._focusIndex).toBe(0);
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
|
||||
let stepHeaderEl = stepHeaders[0].nativeElement;
|
||||
dispatchKeyboardEvent(stepHeaderEl, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent._focusIndex)
|
||||
.toBe(1, 'Expected index of focused step to be increased by 1 after RIGHT_ARROW event.');
|
||||
expect(stepperComponent.selectedIndex)
|
||||
.toBe(0, 'Expected index of selected step to remain unchanged after RIGHT_ARROW event.');
|
||||
|
||||
stepHeaderEl = stepHeaders[1].nativeElement;
|
||||
dispatchKeyboardEvent(stepHeaderEl, 'keydown', ENTER);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent._focusIndex)
|
||||
.toBe(1, 'Expected index of focused step to remain unchanged after ENTER event.');
|
||||
expect(stepperComponent.selectedIndex)
|
||||
.toBe(1,
|
||||
'Expected index of selected step to change to index of focused step after EVENT event.');
|
||||
|
||||
stepHeaderEl = stepHeaders[1].nativeElement;
|
||||
dispatchKeyboardEvent(stepHeaderEl, 'keydown', LEFT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent._focusIndex)
|
||||
.toBe(0, 'Expected index of focused step to decrease by 1 after LEFT_ARROW event.');
|
||||
expect(stepperComponent.selectedIndex)
|
||||
.toBe(1, 'Expected index of selected step to remain unchanged after LEFT_ARROW event.');
|
||||
|
||||
// When the focus is on the last step and right arrow key is pressed, the focus should cycle
|
||||
// through to the first step.
|
||||
stepperComponent._focusIndex = 2;
|
||||
stepHeaderEl = stepHeaders[2].nativeElement;
|
||||
dispatchKeyboardEvent(stepHeaderEl, 'keydown', RIGHT_ARROW);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent._focusIndex)
|
||||
.toBe(0,
|
||||
'Expected index of focused step to cycle through to index 0 after RIGHT_ARROW event.');
|
||||
expect(stepperComponent.selectedIndex)
|
||||
.toBe(1, 'Expected index of selected step to remain unchanged after RIGHT_ARROW event.');
|
||||
|
||||
stepHeaderEl = stepHeaders[0].nativeElement;
|
||||
dispatchKeyboardEvent(stepHeaderEl, 'keydown', SPACE);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent._focusIndex)
|
||||
.toBe(0, 'Expected index of focused to remain unchanged after SPACE event.');
|
||||
expect(stepperComponent.selectedIndex)
|
||||
.toBe(0,
|
||||
'Expected index of selected step to change to index of focused step after SPACE event.');
|
||||
}
|
||||
|
||||
/** Asserts that step selection change using stepper buttons does not focus step header. */
|
||||
function assertStepHeaderFocusNotCalled(stepperComponent: MdStepper,
|
||||
fixture: ComponentFixture<any>) {
|
||||
let stepHeaderEl = fixture.debugElement
|
||||
.queryAll(By.css('md-step-header'))[1].nativeElement;
|
||||
let nextButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperNext))[0].nativeElement;
|
||||
spyOn(stepHeaderEl, 'focus');
|
||||
nextButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(1);
|
||||
expect(stepHeaderEl.focus).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that linear stepper does not allow step selection change if current step is not valid.
|
||||
*/
|
||||
function assertLinearStepperValidity(stepHeaderEl: HTMLElement,
|
||||
stepperComponent: MdStepper,
|
||||
testComponent:
|
||||
LinearMdHorizontalStepperApp | LinearMdVerticalStepperApp,
|
||||
fixture: ComponentFixture<any>) {
|
||||
stepHeaderEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
|
||||
let nextButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperNext))[0].nativeElement;
|
||||
nextButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
|
||||
testComponent.oneGroup.get('oneCtrl')!.setValue('answer');
|
||||
stepHeaderEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.oneGroup.valid).toBe(true);
|
||||
expect(stepperComponent.selectedIndex).toBe(1);
|
||||
}
|
||||
|
||||
/** Asserts that step header focus is blurred if the step cannot be selected upon header click. */
|
||||
function assertStepHeaderBlurred(fixture: ComponentFixture<any>) {
|
||||
let stepHeaderEl = fixture.debugElement
|
||||
.queryAll(By.css('md-step-header'))[1].nativeElement;
|
||||
spyOn(stepHeaderEl, 'blur');
|
||||
stepHeaderEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepHeaderEl.blur).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** Asserts that it is only possible to go back to a previous step if the step is editable. */
|
||||
function assertEditableStepChange(stepperComponent: MdStepper,
|
||||
fixture: ComponentFixture<any>) {
|
||||
stepperComponent.selectedIndex = 1;
|
||||
stepperComponent._steps.toArray()[0].editable = false;
|
||||
let previousButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperPrevious))[1].nativeElement;
|
||||
previousButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(1);
|
||||
|
||||
stepperComponent._steps.toArray()[0].editable = true;
|
||||
previousButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that it is possible to skip an optional step in linear stepper if there is no input
|
||||
* or the input is valid.
|
||||
*/
|
||||
function assertOptionalStepValidity(stepperComponent: MdStepper,
|
||||
testComponent: LinearMdHorizontalStepperApp | LinearMdVerticalStepperApp,
|
||||
fixture: ComponentFixture<any>) {
|
||||
testComponent.oneGroup.get('oneCtrl')!.setValue('input');
|
||||
testComponent.twoGroup.get('twoCtrl')!.setValue('input');
|
||||
stepperComponent.selectedIndex = 2;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex).toBe(2);
|
||||
expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(true);
|
||||
|
||||
let nextButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperNext))[2].nativeElement;
|
||||
nextButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent.selectedIndex)
|
||||
.toBe(3, 'Expected selectedIndex to change when optional step input is empty.');
|
||||
|
||||
stepperComponent.selectedIndex = 2;
|
||||
testComponent.threeGroup.get('threeCtrl')!.setValue('input');
|
||||
nextButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(false);
|
||||
expect(stepperComponent.selectedIndex)
|
||||
.toBe(2, 'Expected selectedIndex to remain unchanged when optional step input is invalid.');
|
||||
|
||||
testComponent.threeGroup.get('threeCtrl')!.setValue('123@gmail.com');
|
||||
nextButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(true);
|
||||
expect(stepperComponent.selectedIndex)
|
||||
.toBe(3, 'Expected selectedIndex to change when optional step input is valid.');
|
||||
}
|
||||
|
||||
/** Asserts that step header set the correct icon depending on the state of step. */
|
||||
function assertCorrectStepIcon(stepperComponent: MdStepper,
|
||||
fixture: ComponentFixture<any>,
|
||||
isEditable: boolean,
|
||||
icon: String) {
|
||||
let nextButtonNativeEl = fixture.debugElement
|
||||
.queryAll(By.directive(MdStepperNext))[0].nativeElement;
|
||||
expect(stepperComponent._getIndicatorType(0)).toBe('number');
|
||||
stepperComponent._steps.toArray()[0].editable = isEditable;
|
||||
nextButtonNativeEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(stepperComponent._getIndicatorType(0)).toBe(icon);
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-horizontal-stepper>
|
||||
<md-step>
|
||||
<ng-template mdStepLabel>Step 1</ng-template>
|
||||
Content 1
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
<md-step>
|
||||
<ng-template mdStepLabel>Step 2</ng-template>
|
||||
Content 2
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
<md-step [label]="inputLabel">
|
||||
Content 3
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
</md-horizontal-stepper>
|
||||
`
|
||||
})
|
||||
class SimpleMdHorizontalStepperApp {
|
||||
inputLabel = 'Step 3';
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-horizontal-stepper linear>
|
||||
<md-step [stepControl]="oneGroup">
|
||||
<form [formGroup]="oneGroup">
|
||||
<ng-template mdStepLabel>Step one</ng-template>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="oneCtrl" required>
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</md-step>
|
||||
<md-step [stepControl]="twoGroup">
|
||||
<form [formGroup]="twoGroup">
|
||||
<ng-template mdStepLabel>Step two</ng-template>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="twoCtrl" required>
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</md-step>
|
||||
<md-step [stepControl]="threeGroup" optional>
|
||||
<form [formGroup]="threeGroup">
|
||||
<ng-template mdStepLabel>Step two</ng-template>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="threeCtrl">
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</md-step>
|
||||
<md-step>
|
||||
Done
|
||||
</md-step>
|
||||
</md-horizontal-stepper>
|
||||
`
|
||||
})
|
||||
class LinearMdHorizontalStepperApp {
|
||||
oneGroup: FormGroup;
|
||||
twoGroup: FormGroup;
|
||||
threeGroup: FormGroup;
|
||||
|
||||
ngOnInit() {
|
||||
this.oneGroup = new FormGroup({
|
||||
oneCtrl: new FormControl('', Validators.required)
|
||||
});
|
||||
this.twoGroup = new FormGroup({
|
||||
twoCtrl: new FormControl('', Validators.required)
|
||||
});
|
||||
this.threeGroup = new FormGroup({
|
||||
threeCtrl: new FormControl('', Validators.pattern(EMAIL_REGEX))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-vertical-stepper>
|
||||
<md-step>
|
||||
<ng-template mdStepLabel>Step 1</ng-template>
|
||||
Content 1
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
<md-step>
|
||||
<ng-template mdStepLabel>Step 2</ng-template>
|
||||
Content 2
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
<md-step [label]="inputLabel">
|
||||
Content 3
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</md-step>
|
||||
</md-vertical-stepper>
|
||||
`
|
||||
})
|
||||
class SimpleMdVerticalStepperApp {
|
||||
inputLabel = 'Step 3';
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-vertical-stepper linear>
|
||||
<md-step [stepControl]="oneGroup">
|
||||
<form [formGroup]="oneGroup">
|
||||
<ng-template mdStepLabel>Step one</ng-template>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="oneCtrl" required>
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</md-step>
|
||||
<md-step [stepControl]="twoGroup">
|
||||
<form [formGroup]="twoGroup">
|
||||
<ng-template mdStepLabel>Step two</ng-template>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="twoCtrl" required>
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</md-step>
|
||||
<md-step [stepControl]="threeGroup" optional>
|
||||
<form [formGroup]="threeGroup">
|
||||
<ng-template mdStepLabel>Step two</ng-template>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="threeCtrl">
|
||||
</md-form-field>
|
||||
<div>
|
||||
<button md-button mdStepperPrevious>Back</button>
|
||||
<button md-button mdStepperNext>Next</button>
|
||||
</div>
|
||||
</form>
|
||||
</md-step>
|
||||
<md-step>
|
||||
Done
|
||||
</md-step>
|
||||
</md-vertical-stepper>
|
||||
`
|
||||
})
|
||||
class LinearMdVerticalStepperApp {
|
||||
oneGroup: FormGroup;
|
||||
twoGroup: FormGroup;
|
||||
threeGroup: FormGroup;
|
||||
|
||||
ngOnInit() {
|
||||
this.oneGroup = new FormGroup({
|
||||
oneCtrl: new FormControl('', Validators.required)
|
||||
});
|
||||
this.twoGroup = new FormGroup({
|
||||
twoCtrl: new FormControl('', Validators.required)
|
||||
});
|
||||
this.threeGroup = new FormGroup({
|
||||
threeCtrl: new FormControl('', Validators.pattern(EMAIL_REGEX))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {CdkStep, CdkStepper} from '@angular/cdk/stepper';
|
||||
import {
|
||||
Component,
|
||||
ContentChild,
|
||||
ContentChildren, Directive,
|
||||
// This import is only used to define a generic type. The current TypeScript version incorrectly
|
||||
// considers such imports as unused (https://github.com/Microsoft/TypeScript/issues/14953)
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Optional,
|
||||
QueryList,
|
||||
SkipSelf,
|
||||
ViewChildren,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {MdStepLabel} from './step-label';
|
||||
import {
|
||||
defaultErrorStateMatcher,
|
||||
ErrorOptions,
|
||||
MD_ERROR_GLOBAL_OPTIONS,
|
||||
ErrorStateMatcher
|
||||
} from '../core/error/error-options';
|
||||
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
|
||||
import {MdStepHeader} from './step-header';
|
||||
import {state, style, transition, trigger, animate} from '@angular/animations';
|
||||
|
||||
/** Workaround for https://github.com/angular/angular/issues/17849 */
|
||||
export const _MdStep = CdkStep;
|
||||
export const _MdStepper = CdkStepper;
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-step, mat-step',
|
||||
templateUrl: 'step.html',
|
||||
providers: [{provide: MD_ERROR_GLOBAL_OPTIONS, useExisting: MdStep}],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MdStep extends _MdStep implements ErrorOptions {
|
||||
/** Content for step label given by <ng-template matStepLabel> or <ng-template mdStepLabel>. */
|
||||
@ContentChild(MdStepLabel) stepLabel: MdStepLabel;
|
||||
|
||||
/** Original ErrorStateMatcher that checks the validity of form control. */
|
||||
private _originalErrorStateMatcher: ErrorStateMatcher;
|
||||
|
||||
constructor(@Inject(forwardRef(() => MdStepper)) mdStepper: MdStepper,
|
||||
@Optional() @SkipSelf() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
|
||||
super(mdStepper);
|
||||
this._originalErrorStateMatcher =
|
||||
errorOptions ? errorOptions.errorStateMatcher || defaultErrorStateMatcher
|
||||
: defaultErrorStateMatcher;
|
||||
}
|
||||
|
||||
/** Custom error state matcher that additionally checks for validity of interacted form. */
|
||||
errorStateMatcher = (control: FormControl, form: FormGroupDirective | NgForm) => {
|
||||
let originalErrorState = this._originalErrorStateMatcher(control, form);
|
||||
|
||||
// Custom error state checks for the validity of form that is not submitted or touched
|
||||
// since user can trigger a form change by calling for another step without directly
|
||||
// interacting with the current form.
|
||||
let customErrorState = control.invalid && this.interacted;
|
||||
|
||||
return originalErrorState || customErrorState;
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[mdStepper]'
|
||||
})
|
||||
export class MdStepper extends _MdStepper {
|
||||
/** The list of step headers of the steps in the stepper. */
|
||||
@ViewChildren(MdStepHeader, {read: ElementRef}) _stepHeader: QueryList<ElementRef>;
|
||||
|
||||
/** Steps that the stepper holds. */
|
||||
@ContentChildren(MdStep) _steps: QueryList<MdStep>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-horizontal-stepper, mat-horizontal-stepper',
|
||||
templateUrl: 'stepper-horizontal.html',
|
||||
styleUrls: ['stepper.css'],
|
||||
inputs: ['selectedIndex'],
|
||||
host: {
|
||||
'class': 'mat-stepper-horizontal',
|
||||
'role': 'tablist',
|
||||
},
|
||||
animations: [
|
||||
trigger('stepTransition', [
|
||||
state('previous', style({transform: 'translate3d(-100%, 0, 0)', visibility: 'hidden'})),
|
||||
state('current', style({transform: 'translate3d(0%, 0, 0)', visibility: 'visible'})),
|
||||
state('next', style({transform: 'translate3d(100%, 0, 0)', visibility: 'hidden'})),
|
||||
transition('* => *',
|
||||
animate('500ms cubic-bezier(0.35, 0, 0.25, 1)'))
|
||||
])
|
||||
],
|
||||
providers: [{provide: MdStepper, useExisting: MdHorizontalStepper}],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MdHorizontalStepper extends MdStepper { }
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-vertical-stepper, mat-vertical-stepper',
|
||||
templateUrl: 'stepper-vertical.html',
|
||||
styleUrls: ['stepper.css'],
|
||||
inputs: ['selectedIndex'],
|
||||
host: {
|
||||
'class': 'mat-stepper-vertical',
|
||||
'role': 'tablist',
|
||||
},
|
||||
animations: [
|
||||
trigger('stepTransition', [
|
||||
state('previous', style({height: '0px', visibility: 'hidden'})),
|
||||
state('next', style({height: '0px', visibility: 'hidden'})),
|
||||
state('current', style({height: '*', visibility: 'visible'})),
|
||||
transition('* <=> current', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
|
||||
])
|
||||
],
|
||||
providers: [{provide: MdStepper, useExisting: MdVerticalStepper}],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MdVerticalStepper extends MdStepper { }
|
||||
@@ -60,6 +60,7 @@ System.config({
|
||||
'@angular/cdk/portal': 'dist/packages/cdk/portal/index.js',
|
||||
'@angular/cdk/rxjs': 'dist/packages/cdk/rxjs/index.js',
|
||||
'@angular/cdk/scrolling': 'dist/packages/cdk/scrolling/index.js',
|
||||
'@angular/cdk/stepper': 'dist/packages/cdk/stepper/index.js',
|
||||
'@angular/cdk/table': 'dist/packages/cdk/table/index.js',
|
||||
'@angular/cdk/testing': 'dist/packages/cdk/testing/index.js',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user