diff --git a/src/cdk/public_api.ts b/src/cdk/public_api.ts index 8c771e4938f4..1cd9d7e1b9df 100644 --- a/src/cdk/public_api.ts +++ b/src/cdk/public_api.ts @@ -15,3 +15,4 @@ export * from './portal/index'; export * from './rxjs/index'; export * from './observe-content/index'; export * from './keyboard/index'; +export * from './stepper/index'; diff --git a/src/cdk/stepper/index.ts b/src/cdk/stepper/index.ts new file mode 100644 index 000000000000..58ccf40404b2 --- /dev/null +++ b/src/cdk/stepper/index.ts @@ -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 {NgModule} from '@angular/core'; +import {CdkStepper, CdkStep} from './stepper'; +import {CommonModule} from '@angular/common'; +import {CdkStepLabel} from './step-label'; + +@NgModule({ + imports: [CommonModule], + exports: [CdkStep, CdkStepper, CdkStepLabel], + declarations: [CdkStep, CdkStepper, CdkStepLabel] +}) +export class CdkStepperModule {} + +export * from './stepper'; +export * from './step-label'; diff --git a/src/cdk/stepper/step-label.ts b/src/cdk/stepper/step-label.ts new file mode 100644 index 000000000000..e253b4bfcab5 --- /dev/null +++ b/src/cdk/stepper/step-label.ts @@ -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) { } +} diff --git a/src/cdk/stepper/step.html b/src/cdk/stepper/step.html new file mode 100644 index 000000000000..cd48c06b9917 --- /dev/null +++ b/src/cdk/stepper/step.html @@ -0,0 +1 @@ + diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts new file mode 100644 index 000000000000..627c2864aac8 --- /dev/null +++ b/src/cdk/stepper/stepper.ts @@ -0,0 +1,169 @@ +/** + * @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 +} from '@angular/core'; +import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '../keyboard/keycodes'; +import {CdkStepLabel} from './step-label'; + +/** Used to generate unique ID for each stepper component. */ +let nextId = 0; + +/** Change event emitted on selection changes. */ +export class CdkStepperSelectionEvent { + /** 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', +}) +export class CdkStep { + /** Template for step label if it exists. */ + @ContentChild(CdkStepLabel) stepLabel: CdkStepLabel; + + /** Template for step content. */ + @ViewChild(TemplateRef) content: TemplateRef; + + /** Label of the step. */ + @Input() + label: string; + + constructor(private _stepper: CdkStepper) { } + + /** Selects this step component. */ + select(): void { + this._stepper.selected = this; + } +} + +@Directive({ + selector: 'cdk-stepper', + host: { + '(focus)': '_focusStep()', + '(keydown)': '_onKeydown($event)', + }, +}) +export class CdkStepper { + /** The list of step components that the stepper is holding. */ + @ContentChildren(CdkStep) _steps: QueryList; + + /** The list of step headers of the steps in the stepper. */ + _stepHeader: QueryList; + + /** The index of the selected step. */ + @Input() + get selectedIndex() { return this._selectedIndex; } + set selectedIndex(index: number) { + if (this._selectedIndex != index) { + this._emitStepperSelectionEvent(index); + this._focusStep(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(); + + /** The index of the step that the focus can be set. */ + _focusIndex: number = 0; + + /** Used to track unique ID for each stepper component. */ + private _groupId: number; + + constructor() { + 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 nique id for each step content element. */ + _getStepContentId(i: number): string { + return `mat-step-content-${this._groupId}-${i}`; + } + + 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: + this._focusStep((this._focusIndex + 1) % this._steps.length); + break; + case LEFT_ARROW: + this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length); + break; + case SPACE: + case ENTER: + this._emitStepperSelectionEvent(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(); + } +} diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts index 98b2c3dc8b1e..7ffb84985c36 100644 --- a/src/demo-app/demo-app-module.ts +++ b/src/demo-app/demo-app-module.ts @@ -76,10 +76,12 @@ import { MdToolbarModule, MdTooltipModule, OverlayContainer, - StyleModule + StyleModule, + MdStepperModule, } from '@angular/material'; import {CdkTableModule} from '@angular/cdk'; import {TableHeaderDemo} from './table/table-header-demo'; +import {StepperDemo} from './stepper/stepper-demo'; /** * NgModule that includes all Material modules that are required to serve the demo-app. @@ -118,7 +120,8 @@ import {TableHeaderDemo} from './table/table-header-demo'; MdTooltipModule, MdNativeDateModule, CdkTableModule, - StyleModule + StyleModule, + MdStepperModule, ] }) export class DemoMaterialModule {} @@ -184,6 +187,7 @@ export class DemoMaterialModule {} PlatformDemo, TypographyDemo, ExpansionDemo, + StepperDemo, ], providers: [ {provide: OverlayContainer, useClass: FullscreenOverlayContainer}, diff --git a/src/demo-app/demo-app/demo-app.ts b/src/demo-app/demo-app/demo-app.ts index 3f422c3ccd5c..ff0268d7a637 100644 --- a/src/demo-app/demo-app/demo-app.ts +++ b/src/demo-app/demo-app/demo-app.ts @@ -61,6 +61,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'}, diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts index 875dc907e604..61a99aef41df 100644 --- a/src/demo-app/demo-app/routes.ts +++ b/src/demo-app/demo-app/routes.ts @@ -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'; export const DEMO_APP_ROUTES: Routes = [ {path: '', component: Home}, @@ -74,4 +75,5 @@ export const DEMO_APP_ROUTES: Routes = [ {path: 'style', component: StyleDemo}, {path: 'typography', component: TypographyDemo}, {path: 'expansion', component: ExpansionDemo}, + {path: 'stepper', component: StepperDemo}, ]; diff --git a/src/demo-app/stepper/stepper-demo.html b/src/demo-app/stepper/stepper-demo.html new file mode 100644 index 000000000000..6b7b9944da9d --- /dev/null +++ b/src/demo-app/stepper/stepper-demo.html @@ -0,0 +1,27 @@ +

Horizontal Stepper Demo

+ + + + + + + + +

Horizontal Stepper Demo with Templated Label

+ + + {{step.label}} + + + + + + +

Vertical Stepper Demo

+ + + + + + + diff --git a/src/demo-app/stepper/stepper-demo.scss b/src/demo-app/stepper/stepper-demo.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/demo-app/stepper/stepper-demo.ts b/src/demo-app/stepper/stepper-demo.ts new file mode 100644 index 000000000000..7df83cde23f5 --- /dev/null +++ b/src/demo-app/stepper/stepper-demo.ts @@ -0,0 +1,16 @@ +import {Component} from '@angular/core'; + +@Component({ + moduleId: module.id, + selector: 'stepper-demo', + templateUrl: 'stepper-demo.html', + styleUrls: ['stepper-demo.scss'], +}) +export class StepperDemo { + 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!'} + ]; +} diff --git a/src/lib/core/compatibility/compatibility.ts b/src/lib/core/compatibility/compatibility.ts index bbb5ec11027b..3b6f0bf3cda5 100644 --- a/src/lib/core/compatibility/compatibility.ts +++ b/src/lib/core/compatibility/compatibility.ts @@ -34,6 +34,7 @@ export const MAT_ELEMENTS_SELECTOR = ` [matDialogContent], [matDialogTitle], [matLine], + [matStepLabel], [matTabLabel], [matTabLink], [matTabNav], @@ -64,6 +65,7 @@ export const MAT_ELEMENTS_SELECTOR = ` mat-grid-tile-header, mat-header-cell, mat-hint, + mat-horizontal-stepper, mat-icon, mat-list, mat-list-item, @@ -81,10 +83,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 = ` @@ -100,6 +104,7 @@ export const MD_ELEMENTS_SELECTOR = ` [mdDialogContent], [mdDialogTitle], [mdLine], + [mdStepLabel], [mdTabLabel], [mdTabLink], [mdTabNav], @@ -130,6 +135,7 @@ export const MD_ELEMENTS_SELECTOR = ` md-grid-tile-header, md-header-cell, md-hint, + md-horizontal-stepper, md-icon, md-list, md-list-item, @@ -147,10 +153,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}) diff --git a/src/lib/module.ts b/src/lib/module.ts index e5fa0671e79b..a78d65b2f9e6 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -48,6 +48,7 @@ import {MdExpansionModule} from './expansion/index'; import {MdTableModule} from './table/index'; import {MdSortModule} from './sort/index'; import {MdPaginatorModule} from './paginator/index'; +import {MdStepperModule} from './stepper/index'; const MATERIAL_MODULES = [ MdAutocompleteModule, @@ -86,7 +87,8 @@ const MATERIAL_MODULES = [ A11yModule, PlatformModule, MdCommonModule, - ObserveContentModule + ObserveContentModule, + MdStepperModule, ]; /** @deprecated */ diff --git a/src/lib/public_api.ts b/src/lib/public_api.ts index fbaf29694867..7059416aae3d 100644 --- a/src/lib/public_api.ts +++ b/src/lib/public_api.ts @@ -44,3 +44,4 @@ export * from './tabs/index'; export * from './tabs/tab-nav-bar/index'; export * from './toolbar/index'; export * from './tooltip/index'; +export * from './stepper/index'; diff --git a/src/lib/stepper/index.ts b/src/lib/stepper/index.ts new file mode 100644 index 000000000000..844198c1e7cb --- /dev/null +++ b/src/lib/stepper/index.ts @@ -0,0 +1,30 @@ +/** + * @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'; +import {MdButtonModule} from '../button/index'; +import {MdHorizontalStepper} from './stepper-horizontal'; +import {MdVerticalStepper} from './stepper-vertical'; +import {MdStep, MdStepper} from './stepper'; +import {CdkStepperModule} from '@angular/cdk'; +import {MdCommonModule} from '../core'; +import {MdStepLabel} from './step-label'; + +@NgModule({ + imports: [MdCommonModule, CommonModule, PortalModule, MdButtonModule, CdkStepperModule], + exports: [MdCommonModule, MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper], + declarations: [MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper], +}) +export class MdStepperModule {} + +export * from './stepper-horizontal'; +export * from './stepper-vertical'; +export * from './step-label'; +export * from './stepper'; diff --git a/src/lib/stepper/step-label.ts b/src/lib/stepper/step-label.ts new file mode 100644 index 000000000000..4c17615cf1dd --- /dev/null +++ b/src/lib/stepper/step-label.ts @@ -0,0 +1,19 @@ +/** + * @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'; + +@Directive({ + selector: '[mdStepLabel], [matStepLabel]', +}) +export class MdStepLabel extends CdkStepLabel { + constructor(template: TemplateRef) { + super(template); + } +} diff --git a/src/lib/stepper/step.html b/src/lib/stepper/step.html new file mode 100644 index 000000000000..cd48c06b9917 --- /dev/null +++ b/src/lib/stepper/step.html @@ -0,0 +1 @@ + diff --git a/src/lib/stepper/stepper-horizontal.html b/src/lib/stepper/stepper-horizontal.html new file mode 100644 index 000000000000..937de1074045 --- /dev/null +++ b/src/lib/stepper/stepper-horizontal.html @@ -0,0 +1,35 @@ + + +
+ +
+ +
+ + +
diff --git a/src/lib/stepper/stepper-horizontal.ts b/src/lib/stepper/stepper-horizontal.ts new file mode 100644 index 000000000000..80f23ad348c2 --- /dev/null +++ b/src/lib/stepper/stepper-horizontal.ts @@ -0,0 +1,24 @@ +/** + * @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} from '@angular/core'; +import {MdStepper} from './stepper'; + +@Component({ + moduleId: module.id, + selector: 'md-horizontal-stepper, mat-horizontal-stepper', + templateUrl: 'stepper-horizontal.html', + styleUrls: ['stepper.scss'], + inputs: ['selectedIndex'], + host: { + 'class': 'mat-stepper-horizontal', + 'role': 'tablist', + }, + providers: [{provide: MdStepper, useExisting: MdHorizontalStepper}] +}) +export class MdHorizontalStepper extends MdStepper { } diff --git a/src/lib/stepper/stepper-vertical.html b/src/lib/stepper/stepper-vertical.html new file mode 100644 index 000000000000..1ea8ef9e6789 --- /dev/null +++ b/src/lib/stepper/stepper-vertical.html @@ -0,0 +1,35 @@ +
+ +
+
+ + + +
+ + +
+
+
diff --git a/src/lib/stepper/stepper-vertical.ts b/src/lib/stepper/stepper-vertical.ts new file mode 100644 index 000000000000..25ae4c2de376 --- /dev/null +++ b/src/lib/stepper/stepper-vertical.ts @@ -0,0 +1,24 @@ +/** + * @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} from '@angular/core'; +import {MdStepper} from './stepper'; + +@Component({ + moduleId: module.id, + selector: 'md-vertical-stepper, mat-vertical-stepper', + templateUrl: 'stepper-vertical.html', + styleUrls: ['stepper.scss'], + inputs: ['selectedIndex'], + host: { + 'class': 'mat-stepper-vertical', + 'role': 'tablist', + }, + providers: [{provide: MdStepper, useExisting: MdVerticalStepper}] +}) +export class MdVerticalStepper extends MdStepper { } diff --git a/src/lib/stepper/stepper.scss b/src/lib/stepper/stepper.scss new file mode 100644 index 000000000000..239fda3fb58d --- /dev/null +++ b/src/lib/stepper/stepper.scss @@ -0,0 +1,7 @@ +.mat-stepper-content[aria-expanded='false'] { + display: none; +} + +.mat-stepper-index, .mat-stepper-label { + display: inline-block; +} diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts new file mode 100644 index 000000000000..9a0d9cf69e4e --- /dev/null +++ b/src/lib/stepper/stepper.ts @@ -0,0 +1,43 @@ +/** + * @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'; +import { + Component, + ContentChild, + ContentChildren, + // 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, + QueryList, + ViewChildren +}from '@angular/core'; +import {MdStepLabel} from './step-label'; + +@Component({ + moduleId: module.id, + selector: 'md-step, mat-step', + templateUrl: 'step.html', +}) +export class MdStep extends CdkStep { + /** Content for step label given by or . */ + @ContentChild(MdStepLabel) stepLabel: MdStepLabel; + + constructor(mdStepper: MdStepper) { + super(mdStepper); + } +} + +export class MdStepper extends CdkStepper { + /** The list of step headers of the steps in the stepper. */ + @ViewChildren('stepHeader') _stepHeader: QueryList; + + /** Steps that the stepper holds. */ + @ContentChildren(MdStep) _steps: QueryList; +}