Skip to content

Commit 4ba35a3

Browse files
committed
feat(stepper): Merge initial prototype of stepper into the upstream stepper branch. (angular#5742)
* Prototyping * Further work * Further prototyping * Further prototyping * Further work * Adding event emitters * Adding "selectedIndex" attribute to stepper and working on TemplateOulet. * Prototyping * Further work * Further prototyping * Further prototyping * Further work * Adding event emitters * Template rendering and selectIndex control done. * Work in progress for accessibility * Added functionalities based on the tentative API doc. * Refactor code for cdk-stepper and cdk-step * Add support for templated label * Added support for keyboard events and focus changes for accessibility. * Updated vertical stepper + added comments * Fix package-lock.json * Fix indention * Changes made based on the review * Changes based on review - event properties, selectors, SPACE support, etc. + demo * Add select() for step component + refactor to avoid circular dependency + support cycling using arrow keys * API change based on review * Minor code clean up based on review. * Several name changes, etc based on review * Add to compatibility mode list and refactor to avoid circular dependency
1 parent 7f0e58e commit 4ba35a3

21 files changed

+486
-3
lines changed

src/cdk/stepper/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {NgModule} from '@angular/core';
10+
import {CdkStepper, CdkStep} from './stepper';
11+
import {CommonModule} from '@angular/common';
12+
import {CdkStepLabel} from './step-label';
13+
14+
@NgModule({
15+
imports: [CommonModule],
16+
exports: [CdkStep, CdkStepper, CdkStepLabel],
17+
declarations: [CdkStep, CdkStepper, CdkStepLabel]
18+
})
19+
export class CdkStepperModule {}
20+
21+
export * from './stepper';
22+
export * from './step-label';

src/cdk/stepper/step-label.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Directive, TemplateRef} from '@angular/core';
10+
11+
@Directive({
12+
selector: '[cdkStepLabel]',
13+
})
14+
export class CdkStepLabel {
15+
constructor(public template: TemplateRef<any>) { }
16+
}

src/cdk/stepper/step.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<ng-template><ng-content></ng-content></ng-template>

src/cdk/stepper/stepper.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
ContentChildren,
11+
EventEmitter,
12+
Input,
13+
Output,
14+
QueryList,
15+
Directive,
16+
// This import is only used to define a generic type. The current TypeScript version incorrectly
17+
// considers such imports as unused (https://github.com/Microsoft/TypeScript/issues/14953)
18+
// tslint:disable-next-line:no-unused-variable
19+
ElementRef,
20+
Component,
21+
ContentChild,
22+
ViewChild,
23+
TemplateRef
24+
} from '@angular/core';
25+
import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '../keyboard/keycodes';
26+
import {CdkStepLabel} from './step-label';
27+
28+
/** Used to generate unique ID for each stepper component. */
29+
let nextId = 0;
30+
31+
/** Change event emitted on selection changes. */
32+
export class CdkStepperSelectionEvent {
33+
/** Index of the step now selected. */
34+
selectedIndex: number;
35+
36+
/** Index of the step previously selected. */
37+
previouslySelectedIndex: number;
38+
39+
/** The step instance now selected. */
40+
selectedStep: CdkStep;
41+
42+
/** The step instance previously selected. */
43+
previouslySelectedStep: CdkStep;
44+
}
45+
46+
@Component({
47+
selector: 'cdk-step',
48+
templateUrl: 'step.html',
49+
})
50+
export class CdkStep {
51+
/** Template for step label if it exists. */
52+
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel;
53+
54+
/** Template for step content. */
55+
@ViewChild(TemplateRef) content: TemplateRef<any>;
56+
57+
/** Label of the step. */
58+
@Input()
59+
label: string;
60+
61+
constructor(private _stepper: CdkStepper) { }
62+
63+
/** Selects this step component. */
64+
select(): void {
65+
this._stepper.selected = this;
66+
}
67+
}
68+
69+
@Directive({
70+
selector: 'cdk-stepper',
71+
host: {
72+
'(focus)': '_focusStep()',
73+
'(keydown)': '_onKeydown($event)',
74+
},
75+
})
76+
export class CdkStepper {
77+
/** The list of step components that the stepper is holding. */
78+
@ContentChildren(CdkStep) _steps: QueryList<CdkStep>;
79+
80+
/** The list of step headers of the steps in the stepper. */
81+
_stepHeader: QueryList<ElementRef>;
82+
83+
/** The index of the selected step. */
84+
@Input()
85+
get selectedIndex() { return this._selectedIndex; }
86+
set selectedIndex(index: number) {
87+
if (this._selectedIndex != index) {
88+
this._emitStepperSelectionEvent(index);
89+
this._focusStep(this._selectedIndex);
90+
}
91+
}
92+
private _selectedIndex: number = 0;
93+
94+
/** The step that is selected. */
95+
@Input()
96+
get selected() { return this._steps[this.selectedIndex]; }
97+
set selected(step: CdkStep) {
98+
let index = this._steps.toArray().indexOf(step);
99+
this.selectedIndex = index;
100+
}
101+
102+
/** Event emitted when the selected step has changed. */
103+
@Output() selectionChange = new EventEmitter<CdkStepperSelectionEvent>();
104+
105+
/** The index of the step that the focus can be set. */
106+
_focusIndex: number = 0;
107+
108+
/** Used to track unique ID for each stepper component. */
109+
private _groupId: number;
110+
111+
constructor() {
112+
this._groupId = nextId++;
113+
}
114+
115+
/** Selects and focuses the next step in list. */
116+
next(): void {
117+
this.selectedIndex = Math.min(this._selectedIndex + 1, this._steps.length - 1);
118+
}
119+
120+
/** Selects and focuses the previous step in list. */
121+
previous(): void {
122+
this.selectedIndex = Math.max(this._selectedIndex - 1, 0);
123+
}
124+
125+
/** Returns a unique id for each step label element. */
126+
_getStepLabelId(i: number): string {
127+
return `mat-step-label-${this._groupId}-${i}`;
128+
}
129+
130+
/** Returns nique id for each step content element. */
131+
_getStepContentId(i: number): string {
132+
return `mat-step-content-${this._groupId}-${i}`;
133+
}
134+
135+
private _emitStepperSelectionEvent(newIndex: number): void {
136+
const stepsArray = this._steps.toArray();
137+
this.selectionChange.emit({
138+
selectedIndex: newIndex,
139+
previouslySelectedIndex: this._selectedIndex,
140+
selectedStep: stepsArray[newIndex],
141+
previouslySelectedStep: stepsArray[this._selectedIndex],
142+
});
143+
this._selectedIndex = newIndex;
144+
}
145+
146+
_onKeydown(event: KeyboardEvent) {
147+
switch (event.keyCode) {
148+
case RIGHT_ARROW:
149+
this._focusStep((this._focusIndex + 1) % this._steps.length);
150+
break;
151+
case LEFT_ARROW:
152+
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
153+
break;
154+
case SPACE:
155+
case ENTER:
156+
this._emitStepperSelectionEvent(this._focusIndex);
157+
break;
158+
default:
159+
// Return to avoid calling preventDefault on keys that are not explicitly handled.
160+
return;
161+
}
162+
event.preventDefault();
163+
}
164+
165+
private _focusStep(index: number) {
166+
this._focusIndex = index;
167+
this._stepHeader.toArray()[this._focusIndex].nativeElement.focus();
168+
}
169+
}

src/demo-app/demo-app/demo-app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export class DemoApp {
7272
{name: 'Slider', route: '/slider'},
7373
{name: 'Slide Toggle', route: '/slide-toggle'},
7474
{name: 'Snack Bar', route: '/snack-bar'},
75+
{name: 'Stepper', route: 'stepper'},
7576
{name: 'Table', route: '/table'},
7677
{name: 'Tabs', route: '/tabs'},
7778
{name: 'Toolbar', route: '/toolbar'},

src/demo-app/demo-app/routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {DatepickerDemo} from '../datepicker/datepicker-demo';
3636
import {TableDemo} from '../table/table-demo';
3737
import {TypographyDemo} from '../typography/typography-demo';
3838
import {ExpansionDemo} from '../expansion/expansion-demo';
39+
import {StepperDemo} from '../stepper/stepper-demo';
3940
import {DemoApp} from './demo-app';
4041
import {AccessibilityDemo} from '../a11y/a11y';
4142
import {ACCESSIBILITY_DEMO_ROUTES} from '../a11y/routes';
@@ -78,6 +79,7 @@ export const DEMO_APP_ROUTES: Routes = [
7879
{path: 'style', component: StyleDemo},
7980
{path: 'typography', component: TypographyDemo},
8081
{path: 'expansion', component: ExpansionDemo},
82+
{path: 'stepper', component: StepperDemo}
8183
]}
8284
];
8385

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<h2>Horizontal Stepper Demo</h2>
2+
<md-horizontal-stepper>
3+
<md-step *ngFor="let step of steps" [label]="step.label">
4+
<md-input-container>
5+
<input mdInput placeholder="Answer" [(ngModel)]="step.content">
6+
</md-input-container>
7+
</md-step>
8+
</md-horizontal-stepper>
9+
10+
<h2>Horizontal Stepper Demo with Templated Label</h2>
11+
<md-horizontal-stepper>
12+
<md-step *ngFor="let step of steps">
13+
<ng-template mdStepLabel>{{step.label}}</ng-template>
14+
<md-input-container>
15+
<input mdInput placeholder="Answer" [(ngModel)]="step.content">
16+
</md-input-container>
17+
</md-step>
18+
</md-horizontal-stepper>
19+
20+
<h2>Vertical Stepper Demo</h2>
21+
<md-vertical-stepper>
22+
<md-step *ngFor="let step of steps" [label]="step.label">
23+
<md-input-container>
24+
<input mdInput placeholder="Answer" [(ngModel)]="step.content">
25+
</md-input-container>
26+
</md-step>
27+
</md-vertical-stepper>

src/demo-app/stepper/stepper-demo.scss

Whitespace-only changes.

src/demo-app/stepper/stepper-demo.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
moduleId: module.id,
5+
selector: 'stepper-demo',
6+
templateUrl: 'stepper-demo.html',
7+
styleUrls: ['stepper-demo.scss'],
8+
})
9+
export class StepperDemo {
10+
steps = [
11+
{label: 'Confirm your name', content: 'Last name, First name.'},
12+
{label: 'Confirm your contact information', content: '123-456-7890'},
13+
{label: 'Confirm your address', content: '1600 Amphitheater Pkwy MTV'},
14+
{label: 'You are now done', content: 'Finished!'}
15+
];
16+
}

src/lib/core/compatibility/compatibility.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const MAT_ELEMENTS_SELECTOR = `
3939
[matHeaderRowDef],
4040
[matLine],
4141
[matRowDef],
42+
[matStepLabel],
4243
[matTabLabel],
4344
[matTabLink],
4445
[matTabNav],
@@ -73,6 +74,7 @@ export const MAT_ELEMENTS_SELECTOR = `
7374
mat-header-cell,
7475
mat-header-row,
7576
mat-hint,
77+
mat-horizontal-stepper,
7678
mat-icon,
7779
mat-input-container,
7880
mat-form-field,
@@ -92,10 +94,12 @@ export const MAT_ELEMENTS_SELECTOR = `
9294
mat-sidenav-container,
9395
mat-slider,
9496
mat-spinner,
97+
mat-step,
9598
mat-tab,
9699
mat-table,
97100
mat-tab-group,
98-
mat-toolbar`;
101+
mat-toolbar,
102+
mat-vertical-stepper`;
99103

100104
/** Selector that matches all elements that may have style collisions with AngularJS Material. */
101105
export const MD_ELEMENTS_SELECTOR = `
@@ -116,6 +120,7 @@ export const MD_ELEMENTS_SELECTOR = `
116120
[mdHeaderRowDef],
117121
[mdLine],
118122
[mdRowDef],
123+
[mdStepLabel],
119124
[mdTabLabel],
120125
[mdTabLink],
121126
[mdTabNav],
@@ -150,6 +155,7 @@ export const MD_ELEMENTS_SELECTOR = `
150155
md-header-cell,
151156
md-header-row,
152157
md-hint,
158+
md-horizontal-stepper,
153159
md-icon,
154160
md-input-container,
155161
md-form-field,
@@ -169,10 +175,12 @@ export const MD_ELEMENTS_SELECTOR = `
169175
md-sidenav-container,
170176
md-slider,
171177
md-spinner,
178+
md-step,
172179
md-tab,
173180
md-table,
174181
md-tab-group,
175-
md-toolbar`;
182+
md-toolbar,
183+
md-vertical-stepper`;
176184

177185
/** Directive that enforces that the `mat-` prefix cannot be used. */
178186
@Directive({selector: MAT_ELEMENTS_SELECTOR})

src/lib/module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {MdTableModule} from './table/index';
4646
import {MdSortModule} from './sort/index';
4747
import {MdPaginatorModule} from './paginator/index';
4848
import {MdFormFieldModule} from './form-field/index';
49+
import {MdStepperModule} from './stepper/index';
4950

5051
const MATERIAL_MODULES = [
5152
MdAutocompleteModule,
@@ -75,6 +76,7 @@ const MATERIAL_MODULES = [
7576
MdSlideToggleModule,
7677
MdSnackBarModule,
7778
MdSortModule,
79+
MdStepperModule,
7880
MdTabsModule,
7981
MdToolbarModule,
8082
MdTooltipModule,
@@ -85,7 +87,7 @@ const MATERIAL_MODULES = [
8587
A11yModule,
8688
PlatformModule,
8789
MdCommonModule,
88-
ObserversModule
90+
ObserversModule,
8991
];
9092

9193
/** @deprecated */

src/lib/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ export * from './tabs/index';
4646
export * from './tabs/tab-nav-bar/index';
4747
export * from './toolbar/index';
4848
export * from './tooltip/index';
49+
export * from './stepper/index';

0 commit comments

Comments
 (0)