Skip to content

Commit 87318bc

Browse files
jwshinjwshinjelbourn
authored andcommitted
feat(stepper): initial version of stepper (#6594)
1 parent 70bd5fc commit 87318bc

35 files changed

+2126
-4
lines changed

Diff for: src/cdk/stepper/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
export * from './public_api';

Diff for: src/cdk/stepper/public_api.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
import {CdkStepperNext, CdkStepperPrevious} from './stepper-button';
14+
import {BidiModule} from '@angular/cdk/bidi';
15+
16+
@NgModule({
17+
imports: [BidiModule, CommonModule],
18+
exports: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious],
19+
declarations: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious]
20+
})
21+
export class CdkStepperModule {}
22+
23+
export * from './stepper';
24+
export * from './step-label';
25+
export * from './stepper-button';

Diff for: src/cdk/stepper/step-label.ts

+16
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+
}

Diff for: src/cdk/stepper/step.html

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<ng-template><ng-content></ng-content></ng-template>

Diff for: src/cdk/stepper/stepper-button.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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} from '@angular/core';
10+
import {CdkStepper} from './stepper';
11+
12+
/** Button that moves to the next step in a stepper workflow. */
13+
@Directive({
14+
selector: 'button[cdkStepperNext]',
15+
host: {'(click)': '_stepper.next()'}
16+
})
17+
export class CdkStepperNext {
18+
constructor(public _stepper: CdkStepper) { }
19+
}
20+
21+
/** Button that moves to the previous step in a stepper workflow. */
22+
@Directive({
23+
selector: 'button[cdkStepperPrevious]',
24+
host: {'(click)': '_stepper.previous()'}
25+
})
26+
export class CdkStepperPrevious {
27+
constructor(public _stepper: CdkStepper) { }
28+
}

Diff for: src/cdk/stepper/stepper.ts

+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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+
ViewEncapsulation,
25+
Optional,
26+
Inject,
27+
forwardRef
28+
} from '@angular/core';
29+
import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keycodes';
30+
import {CdkStepLabel} from './step-label';
31+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
32+
import {AbstractControl} from '@angular/forms';
33+
import {Directionality} from '@angular/cdk/bidi';
34+
35+
/** Used to generate unique ID for each stepper component. */
36+
let nextId = 0;
37+
38+
/**
39+
* Position state of the content of each step in stepper that is used for transitioning
40+
* the content into correct position upon step selection change.
41+
*/
42+
export type StepContentPositionState = 'previous' | 'current' | 'next';
43+
44+
/** Change event emitted on selection changes. */
45+
export class StepperSelectionEvent {
46+
/** Index of the step now selected. */
47+
selectedIndex: number;
48+
49+
/** Index of the step previously selected. */
50+
previouslySelectedIndex: number;
51+
52+
/** The step instance now selected. */
53+
selectedStep: CdkStep;
54+
55+
/** The step instance previously selected. */
56+
previouslySelectedStep: CdkStep;
57+
}
58+
59+
@Component({
60+
selector: 'cdk-step',
61+
templateUrl: 'step.html',
62+
encapsulation: ViewEncapsulation.None
63+
})
64+
export class CdkStep {
65+
/** Template for step label if it exists. */
66+
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel;
67+
68+
/** Template for step content. */
69+
@ViewChild(TemplateRef) content: TemplateRef<any>;
70+
71+
/** The top level abstract control of the step. */
72+
@Input() stepControl: AbstractControl;
73+
74+
/** Whether user has seen the expanded step content or not . */
75+
interacted = false;
76+
77+
/** Label of the step. */
78+
@Input()
79+
label: string;
80+
81+
@Input()
82+
get editable() { return this._editable; }
83+
set editable(value: any) {
84+
this._editable = coerceBooleanProperty(value);
85+
}
86+
private _editable = true;
87+
88+
/** Whether the completion of step is optional or not. */
89+
@Input()
90+
get optional() { return this._optional; }
91+
set optional(value: any) {
92+
this._optional = coerceBooleanProperty(value);
93+
}
94+
private _optional = false;
95+
96+
/** Return whether step is completed or not. */
97+
@Input()
98+
get completed() {
99+
return this._customCompleted == null ? this._defaultCompleted : this._customCompleted;
100+
}
101+
set completed(value: any) {
102+
this._customCompleted = coerceBooleanProperty(value);
103+
}
104+
private _customCompleted: boolean | null = null;
105+
106+
private get _defaultCompleted() {
107+
return this.stepControl ? this.stepControl.valid && this.interacted : this.interacted;
108+
}
109+
110+
constructor(@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper) { }
111+
112+
/** Selects this step component. */
113+
select(): void {
114+
this._stepper.selected = this;
115+
}
116+
}
117+
118+
@Directive({
119+
selector: '[cdkStepper]',
120+
})
121+
export class CdkStepper {
122+
/** The list of step components that the stepper is holding. */
123+
@ContentChildren(CdkStep) _steps: QueryList<CdkStep>;
124+
125+
/** The list of step headers of the steps in the stepper. */
126+
_stepHeader: QueryList<ElementRef>;
127+
128+
/** Whether the validity of previous steps should be checked or not. */
129+
@Input()
130+
get linear() { return this._linear; }
131+
set linear(value: any) { this._linear = coerceBooleanProperty(value); }
132+
private _linear = false;
133+
134+
/** The index of the selected step. */
135+
@Input()
136+
get selectedIndex() { return this._selectedIndex; }
137+
set selectedIndex(index: number) {
138+
if (this._anyControlsInvalid(index)
139+
|| index < this._selectedIndex && !this._steps.toArray()[index].editable) {
140+
// remove focus from clicked step header if the step is not able to be selected
141+
this._stepHeader.toArray()[index].nativeElement.blur();
142+
} else if (this._selectedIndex != index) {
143+
this._emitStepperSelectionEvent(index);
144+
this._focusIndex = this._selectedIndex;
145+
}
146+
}
147+
private _selectedIndex: number = 0;
148+
149+
/** The step that is selected. */
150+
@Input()
151+
get selected() { return this._steps[this.selectedIndex]; }
152+
set selected(step: CdkStep) {
153+
let index = this._steps.toArray().indexOf(step);
154+
this.selectedIndex = index;
155+
}
156+
157+
/** Event emitted when the selected step has changed. */
158+
@Output() selectionChange = new EventEmitter<StepperSelectionEvent>();
159+
160+
/** The index of the step that the focus can be set. */
161+
_focusIndex: number = 0;
162+
163+
/** Used to track unique ID for each stepper component. */
164+
_groupId: number;
165+
166+
constructor(@Optional() private _dir: Directionality) {
167+
this._groupId = nextId++;
168+
}
169+
170+
/** Selects and focuses the next step in list. */
171+
next(): void {
172+
this.selectedIndex = Math.min(this._selectedIndex + 1, this._steps.length - 1);
173+
}
174+
175+
/** Selects and focuses the previous step in list. */
176+
previous(): void {
177+
this.selectedIndex = Math.max(this._selectedIndex - 1, 0);
178+
}
179+
180+
/** Returns a unique id for each step label element. */
181+
_getStepLabelId(i: number): string {
182+
return `mat-step-label-${this._groupId}-${i}`;
183+
}
184+
185+
/** Returns unique id for each step content element. */
186+
_getStepContentId(i: number): string {
187+
return `mat-step-content-${this._groupId}-${i}`;
188+
}
189+
190+
/** Returns position state of the step with the given index. */
191+
_getAnimationDirection(index: number): StepContentPositionState {
192+
const position = index - this._selectedIndex;
193+
if (position < 0) {
194+
return 'previous';
195+
} else if (position > 0) {
196+
return 'next';
197+
} else {
198+
return 'current';
199+
}
200+
}
201+
202+
/** Returns the type of icon to be displayed. */
203+
_getIndicatorType(index: number): 'number' | 'edit' | 'done' {
204+
const step = this._steps.toArray()[index];
205+
if (!step.completed || this._selectedIndex == index) {
206+
return 'number';
207+
} else {
208+
return step.editable ? 'edit' : 'done';
209+
}
210+
}
211+
212+
private _emitStepperSelectionEvent(newIndex: number): void {
213+
const stepsArray = this._steps.toArray();
214+
this.selectionChange.emit({
215+
selectedIndex: newIndex,
216+
previouslySelectedIndex: this._selectedIndex,
217+
selectedStep: stepsArray[newIndex],
218+
previouslySelectedStep: stepsArray[this._selectedIndex],
219+
});
220+
this._selectedIndex = newIndex;
221+
}
222+
223+
_onKeydown(event: KeyboardEvent) {
224+
switch (event.keyCode) {
225+
case RIGHT_ARROW:
226+
if (this._dir && this._dir.value === 'rtl') {
227+
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
228+
} else {
229+
this._focusStep((this._focusIndex + 1) % this._steps.length);
230+
}
231+
break;
232+
case LEFT_ARROW:
233+
if (this._dir && this._dir.value === 'rtl') {
234+
this._focusStep((this._focusIndex + 1) % this._steps.length);
235+
} else {
236+
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
237+
}
238+
break;
239+
case SPACE:
240+
case ENTER:
241+
this.selectedIndex = this._focusIndex;
242+
break;
243+
default:
244+
// Return to avoid calling preventDefault on keys that are not explicitly handled.
245+
return;
246+
}
247+
event.preventDefault();
248+
}
249+
250+
private _focusStep(index: number) {
251+
this._focusIndex = index;
252+
this._stepHeader.toArray()[this._focusIndex].nativeElement.focus();
253+
}
254+
255+
private _anyControlsInvalid(index: number): boolean {
256+
const stepsArray = this._steps.toArray();
257+
stepsArray[this._selectedIndex].interacted = true;
258+
if (this._linear) {
259+
return stepsArray.slice(0, index).some(step => step.stepControl.invalid);
260+
}
261+
return false;
262+
}
263+
}

Diff for: src/cdk/stepper/tsconfig-build.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "../tsconfig-build",
3+
"files": [
4+
"public_api.ts"
5+
],
6+
"angularCompilerOptions": {
7+
"annotateForClosureCompiler": true,
8+
"strictMetadataEmit": true,
9+
"flatModuleOutFile": "index.js",
10+
"flatModuleId": "@angular/cdk/stepper",
11+
"skipTemplateCodegen": true
12+
}
13+
}

Diff for: src/demo-app/demo-app/demo-app.ts

+1
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'},

Diff for: src/demo-app/demo-app/demo-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {PeopleDatabase} from '../table/people-database';
4040
import {DatepickerDemo} from '../datepicker/datepicker-demo';
4141
import {TypographyDemo} from '../typography/typography-demo';
4242
import {ExpansionDemo} from '../expansion/expansion-demo';
43+
import {StepperDemo} from '../stepper/stepper-demo';
4344
import {DemoMaterialModule} from '../demo-material-module';
4445
import {
4546
FullscreenOverlayContainer,
@@ -92,6 +93,7 @@ import {TableHeaderDemo} from '../table/table-header-demo';
9293
SliderDemo,
9394
SlideToggleDemo,
9495
SpagettiPanel,
96+
StepperDemo,
9597
StyleDemo,
9698
TableHeaderDemo,
9799
ToolbarDemo,

0 commit comments

Comments
 (0)