Skip to content

Commit 1e51fae

Browse files
jwshinjwshinmmalerba
authored andcommitted
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 feat(stepper): Create stepper button directives to enable adding buttons to stepper (#5951) * Create stepper button directives to enable adding buttons to stepper * Changes made based on review * Minor changes with click handlers Build changes feat(stepper): Add initial styles to stepper based on Material guidelines (#6242) * Add initial styles to stepper based on Material guidelines * Fix flex-shrink and min-width * Changes made based on review * Fix alignment * Margin modifications feat(stepper): Add support for linear stepper (#6116) * Add form controls and custom error state matcher * Modify form controls for stepper-demo and add custom validator * Move custom step validation function so that users can simply import and use * Implement @input() stepControl for each step * Add linear attribute to stepper * Add enabling/disabling linear state of demo feat(stepper): Add animation to stepper (#6361) * Add animation * Implement Angular animation * Clean up unnecessary code * Generalize animation so that vertical and horizontal steppers can use the same function Rebase onto upstream/master feat(stepper): Add unit tests for stepper (#6428) * Add unit tests for stepper * Changes made based on review * More changes based on review feat(stepper): Add support for linear stepper #2 - each step as its own form. (#6117) * Add form control - consider each step as its own form group * Comment edits * Add 'valid' to MdStep for form validation * Add [stepControl] to each step based on merging * Changes based on review Fix focus logic and CSS changes (#6507) feat(stepper): Add documentation for stepper (#6533) * Documentation for stepper * Revision based on review + add accessibility section feat(stepper): Support additional properties for step (#6509) * Additional properties for step * Unit tests * Code changes based on review + test name changes * Refactor code for shared functionality between vertical and horizontal stepper * Refactor md-step-header and md-step-content + optional step change * Simplify code based on review * Changes to step-header based on review * Minor changes Fix host style and demo page (#6592) Revert package.json and package-lock.json Changes made along with BUILD changes in google3 Add typography mixin Changes to address aot compiler failures fix rtl bugs
1 parent 595cffd commit 1e51fae

37 files changed

+2154
-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

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

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)