Skip to content

Commit 4438480

Browse files
authored
feat(stepper): Merge initial prototype of stepper into the upstream stepper branch. (#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 a34787d commit 4438480

23 files changed

+493
-5
lines changed

src/cdk/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from './portal/index';
1515
export * from './rxjs/index';
1616
export * from './observe-content/index';
1717
export * from './keyboard/index';
18+
export * from './stepper/index';

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-module.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,12 @@ import {
7676
MdToolbarModule,
7777
MdTooltipModule,
7878
OverlayContainer,
79-
StyleModule
79+
StyleModule,
80+
MdStepperModule,
8081
} from '@angular/material';
8182
import {CdkTableModule} from '@angular/cdk';
8283
import {TableHeaderDemo} from './table/table-header-demo';
84+
import {StepperDemo} from './stepper/stepper-demo';
8385

8486
/**
8587
* 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';
118120
MdTooltipModule,
119121
MdNativeDateModule,
120122
CdkTableModule,
121-
StyleModule
123+
StyleModule,
124+
MdStepperModule,
122125
]
123126
})
124127
export class DemoMaterialModule {}
@@ -184,6 +187,7 @@ export class DemoMaterialModule {}
184187
PlatformDemo,
185188
TypographyDemo,
186189
ExpansionDemo,
190+
StepperDemo,
187191
],
188192
providers: [
189193
{provide: OverlayContainer, useClass: FullscreenOverlayContainer},

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export class DemoApp {
6161
{name: 'Slider', route: 'slider'},
6262
{name: 'Slide Toggle', route: 'slide-toggle'},
6363
{name: 'Snack Bar', route: 'snack-bar'},
64+
{name: 'Stepper', route: 'stepper'},
6465
{name: 'Table', route: 'table'},
6566
{name: 'Tabs', route: 'tabs'},
6667
{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

4041
export const DEMO_APP_ROUTES: Routes = [
4142
{path: '', component: Home},
@@ -74,4 +75,5 @@ export const DEMO_APP_ROUTES: Routes = [
7475
{path: 'style', component: StyleDemo},
7576
{path: 'typography', component: TypographyDemo},
7677
{path: 'expansion', component: ExpansionDemo},
78+
{path: 'stepper', component: StepperDemo},
7779
];
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
@@ -34,6 +34,7 @@ export const MAT_ELEMENTS_SELECTOR = `
3434
[matDialogContent],
3535
[matDialogTitle],
3636
[matLine],
37+
[matStepLabel],
3738
[matTabLabel],
3839
[matTabLink],
3940
[matTabNav],
@@ -64,6 +65,7 @@ export const MAT_ELEMENTS_SELECTOR = `
6465
mat-grid-tile-header,
6566
mat-header-cell,
6667
mat-hint,
68+
mat-horizontal-stepper,
6769
mat-icon,
6870
mat-list,
6971
mat-list-item,
@@ -81,10 +83,12 @@ export const MAT_ELEMENTS_SELECTOR = `
8183
mat-sidenav-container,
8284
mat-slider,
8385
mat-spinner,
86+
mat-step,
8487
mat-tab,
8588
mat-table,
8689
mat-tab-group,
87-
mat-toolbar`;
90+
mat-toolbar,
91+
mat-vertical-stepper`;
8892

8993
/** Selector that matches all elements that may have style collisions with AngularJS Material. */
9094
export const MD_ELEMENTS_SELECTOR = `
@@ -100,6 +104,7 @@ export const MD_ELEMENTS_SELECTOR = `
100104
[mdDialogContent],
101105
[mdDialogTitle],
102106
[mdLine],
107+
[mdStepLabel],
103108
[mdTabLabel],
104109
[mdTabLink],
105110
[mdTabNav],
@@ -130,6 +135,7 @@ export const MD_ELEMENTS_SELECTOR = `
130135
md-grid-tile-header,
131136
md-header-cell,
132137
md-hint,
138+
md-horizontal-stepper,
133139
md-icon,
134140
md-list,
135141
md-list-item,
@@ -147,10 +153,12 @@ export const MD_ELEMENTS_SELECTOR = `
147153
md-sidenav-container,
148154
md-slider,
149155
md-spinner,
156+
md-step,
150157
md-tab,
151158
md-table,
152159
md-tab-group,
153-
md-toolbar`;
160+
md-toolbar,
161+
md-vertical-stepper`;
154162

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

src/lib/module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {MdExpansionModule} from './expansion/index';
4848
import {MdTableModule} from './table/index';
4949
import {MdSortModule} from './sort/index';
5050
import {MdPaginatorModule} from './paginator/index';
51+
import {MdStepperModule} from './stepper/index';
5152

5253
const MATERIAL_MODULES = [
5354
MdAutocompleteModule,
@@ -86,7 +87,8 @@ const MATERIAL_MODULES = [
8687
A11yModule,
8788
PlatformModule,
8889
MdCommonModule,
89-
ObserveContentModule
90+
ObserveContentModule,
91+
MdStepperModule,
9092
];
9193

9294
/** @deprecated */

src/lib/public_api.ts

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

0 commit comments

Comments
 (0)