feat(stepper): initial version of stepper (#6594)

This commit is contained in:
Ji Won Shin
2017-08-31 15:26:05 -07:00
committed by Jeremy Elbourn
parent 70bd5fc20e
commit 87318bc7c8
35 changed files with 2126 additions and 4 deletions
+9
View File
@@ -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';
+25
View File
@@ -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';
+16
View File
@@ -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>) { }
}
+1
View File
@@ -0,0 +1 @@
<ng-template><ng-content></ng-content></ng-template>
+28
View File
@@ -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) { }
}
+263
View File
@@ -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;
}
}
+13
View File
@@ -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
}
}
+1
View File
@@ -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'},
+2
View File
@@ -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,
+2
View File
@@ -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}
]}
];
+3 -1
View File
@@ -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,
+197
View File
@@ -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>
+53
View File
@@ -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)]
});
}
}
+1
View File
@@ -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',
},
+1
View File
@@ -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',
+14 -2
View File
@@ -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})
+2
View File
@@ -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
View File
@@ -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 */
+1
View File
@@ -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';
+53
View File
@@ -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);
}
}
+40
View File
@@ -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';
+17
View File
@@ -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>
+42
View File
@@ -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;
}
+73
View File
@@ -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;
}
}
+22
View File
@@ -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);
}
}
+1
View File
@@ -0,0 +1 @@
<ng-template><ng-content></ng-content></ng-template>
+31
View File
@@ -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 { }
+30
View File
@@ -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>
+28
View File
@@ -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>
+132
View File
@@ -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`.
+122
View File
@@ -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;
}
}
+765
View File
@@ -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))
});
}
}
+132
View File
@@ -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 { }
+1
View File
@@ -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',
},