Skip to content

feat(stepper): Merge initial prototype of stepper into the upstream stepper branch. #5742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Jul 21, 2017
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
33c6299
Prototyping
jwshinjwshin Jun 29, 2017
366b4c3
Further work
jwshinjwshin Jun 30, 2017
d8e3075
Further prototyping
jwshinjwshin Jul 1, 2017
f94d966
Further prototyping
jwshinjwshin Jul 1, 2017
23bfd0c
Further work
jwshinjwshin Jul 5, 2017
63d141e
Adding event emitters
jwshinjwshin Jul 6, 2017
471df0f
Adding "selectedIndex" attribute to stepper and working on TemplateOu…
jwshinjwshin Jul 6, 2017
1d6ebb2
Prototyping
jwshinjwshin Jun 29, 2017
03e5849
Further work
jwshinjwshin Jun 30, 2017
c1cb73f
Further prototyping
jwshinjwshin Jul 1, 2017
e90c839
Further prototyping
jwshinjwshin Jul 1, 2017
992a07a
Further work
jwshinjwshin Jul 5, 2017
9e5720a
Adding event emitters
jwshinjwshin Jul 6, 2017
66bec54
Merge branch 'stepper1' of github.com:g1shin/material2 into stepper1
jwshinjwshin Jul 6, 2017
617c270
Template rendering and selectIndex control done.
jwshinjwshin Jul 6, 2017
42c1f14
Work in progress for accessibility
jwshinjwshin Jul 7, 2017
b879732
Added functionalities based on the tentative API doc.
jwshinjwshin Jul 7, 2017
f89b8bc
Refactor code for cdk-stepper and cdk-step
jwshinjwshin Jul 11, 2017
a529bf3
Add support for templated label
jwshinjwshin Jul 11, 2017
34ac69e
Added support for keyboard events and focus changes for accessibility.
jwshinjwshin Jul 13, 2017
aee0abf
Updated vertical stepper + added comments
jwshinjwshin Jul 13, 2017
fcb94ff
Merge remote-tracking branch 'upstream/master' into stepper2
jwshinjwshin Jul 13, 2017
5e9085f
Fix package-lock.json
jwshinjwshin Jul 13, 2017
f417435
Fix indention
jwshinjwshin Jul 13, 2017
36ad2cc
Changes made based on the review
jwshinjwshin Jul 14, 2017
836c0ac
Changes based on review - event properties, selectors, SPACE support,…
jwshinjwshin Jul 15, 2017
78c9eb5
Add select() for step component + refactor to avoid circular dependen…
jwshinjwshin Jul 18, 2017
26f73f9
API change based on review
jwshinjwshin Jul 19, 2017
a56e57f
Minor code clean up based on review.
jwshinjwshin Jul 20, 2017
3c1f4fd
Several name changes, etc based on review
jwshinjwshin Jul 21, 2017
0c3f7c4
Add to compatibility mode list and refactor to avoid circular dependency
jwshinjwshin Jul 21, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cdk/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
24 changes: 24 additions & 0 deletions src/cdk/stepper/index.ts
Original file line number Diff line number Diff line change
@@ -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 {NgModule} from '@angular/core';
import {CdkStepper} from './stepper';
import {CommonModule} from '@angular/common';
import {CdkStep} from './step';
import {CdkStepLabel} from './step-label';
import {PortalModule} from '../portal';
@NgModule({
imports: [CommonModule, PortalModule],
exports: [CdkStep, CdkStepper, CdkStepLabel],
declarations: [CdkStep, CdkStepper, CdkStepLabel]
})
export class CdkStepperModule {}

export * from './stepper';
export * from './step';
export * from './step-label';
18 changes: 18 additions & 0 deletions src/cdk/stepper/step-label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @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, ViewContainerRef} from '@angular/core';
import {TemplatePortalDirective} from '../portal';
@Directive({
selector: '[cdk-step-label]',
})
export class CdkStepLabel extends TemplatePortalDirective {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can do this the same way you did CdkStep (just a TemplateRef and ngTemplateOutlet tabs used the portal stuff because it was written before ngTemplateOutlet was a thing

constructor(templateRef: TemplateRef<any>, viewContainerRef: ViewContainerRef) {
super(templateRef, viewContainerRef);
}
}
1 change: 1 addition & 0 deletions src/cdk/stepper/step.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<ng-template><ng-content></ng-content></ng-template>
56 changes: 56 additions & 0 deletions src/cdk/stepper/step.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @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, ContentChild, Directive, Input, OnInit, TemplateRef, ViewChild,
ViewContainerRef
} from '@angular/core';
import {CdkStepLabel} from './step-label';

@Component({
selector: '[cdk-step]',
templateUrl: 'step.html',
})
export class CdkStep {
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\n (and comment what these are)

@ViewChild(TemplateRef) content: TemplateRef<any>;

/** Label of the step. */
@Input()
label: string;

/** Whether the step is optional or not. */
@Input() optional: boolean = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split into setter & getter, and use coerceBooleanProperty


/** Whether the step is editable or not. */
@Input() editable: boolean = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use coerceBooleanProperty


/** Whether the step is the last one in the list. */
isLast: boolean = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this something we want to be part of the public API? (I asusme probably not, but we can't make it private since its referenced in a template file, so instead check to _isLast)


// /** Whether the step is active. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no longer needed?

// get active() { return this._active; }
// set active(value: boolean) {
// this._active = value;
// }
// private _active: boolean = false;

/** Whether the step has been selected. */
get selected(): boolean { return this._selected; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are selected and completed used for anything right now? I think its better to remove them and add them back when you're ready to hook them up

set selected(value: boolean) {
this._selected = value;
}
private _selected: boolean = false;

/** Whether the step has been completed. */
get completed() { return this._completed; }
set completed(value: boolean) {
this._completed = value;
}
private _completed: boolean = false;
}
153 changes: 153 additions & 0 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @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, ContentChildren, EventEmitter, Input, Output, QueryList, OnInit,
AfterViewChecked, AfterViewInit, Directive, ElementRef, ViewChild, ViewChildren
} from '@angular/core';
import {CdkStep} from './step';
import {Observable} from 'rxjs/Observable';
import {map} from 'rxjs/operator/map';
import {LEFT_ARROW, RIGHT_ARROW, ENTER, TAB} from '../keyboard/keycodes';

/** Used to generate unique ID for each stepper component. */
let nextId = 0;

/** Change event emitted on focus or selection changes. */
export class CdkStepEvent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CdkStepperSelectionEvent?

index: number;
step: CdkStep;
}

@Directive({
selector: '[cdkStepper]',
host: {
'(keydown)': '_onKeydown($event)'
},
})
export class CdkStepper {
@ContentChildren(CdkStep) _steps: QueryList<CdkStep>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment what these are


@ViewChildren('stepHeader') _stepHeader: QueryList<ElementRef>;

/** The index of the currently selected step. */
@Input()
set selectedIndex(value: number) {
this._selectedIndex = value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coerceNumberProperty(value)

}
get selectedIndex(): number { return this._selectedIndex; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: getter first, then setter

private _selectedIndex: number;

/** Optional input to support both linear and non-linear stepper component. */
@Input() linear: boolean = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove for now (add back if/when you hook it up to something)


/** Output to enable support for two-way binding on `[(selectedIndex)]` */
@Output() get selectedIndexChange(): Observable<number> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't the index part of the CdkStepEvent already emitted by this.stepEvent? why do we need this as well?

return map.call(this.stepEvent, event => event.index);
}

// @Output() get focusIndexChange(): Observable<number> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no longer needed?

// return map.call(this.focusChange, event => event.index);
// }

/** Event emitted when the selected step has changed. */
@Output() stepEvent = new EventEmitter<CdkStepEvent>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this selectionChange, stepEvent isn't very descriptive


/** Event emitted when the focused step has changed. */
@Output() focusChange = new EventEmitter<CdkStepEvent>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this? I would think the developer can manage this themselves with (focus) and (blur) events


/** The step that is currently selected. */
get selectedStep(): CdkStep {
return this._steps.toArray()[this._selectedIndex];
}
private _selectedStep: CdkStep;

/** The index of the step that the focus is currently on. */
get focusIndex(): number {return this._focusIndex; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need a public API for this? seems like an internal detail

private _focusIndex: number = 0;

private _groupId: number;

constructor() {
this._groupId = nextId++;
}

/** Selects and focuses the provided step. */
selectStep(step: CdkStep): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this similar to what tabs does? From an API perspective I would rather CdkStep have a select method, but I could see maybe not wanting a dependency from child to parent

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it seems like tabs doesn't have a separate method for selecting. It just updates and sets the selectedIndex in the html file. Since the CdkStepper already takes care of all the logic regarding the selection and selectedIndex tracking, I was thinking about just getting rid of the selected attribute completely from CdkStep.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm saying that as an API I prefer: step.select() to stepper.select(step), but this would require the step to know about its parent stepper, so maybe not worth it. @jelbourn do you have any opinion?

let stepsArray = this._steps.toArray();
this._selectedIndex = stepsArray.indexOf(step);
this.stepEvent.emit(this._emitStepEvent(this._selectedIndex));
this._focusIndex = this._selectedIndex;
this._setStepFocus();
}

/** Selects and focuses the next step in list. */
nextStep(): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just call it next and previous

if (this._selectedIndex == this._steps.length - 1) { return; }
this._selectedIndex++;
this.stepEvent.emit(this._emitStepEvent(this._selectedIndex));
this._focusIndex = this._selectedIndex;
this._setStepFocus();
}

/** Selects and focuses the previous step in list. */
previousStep(): void {
if (this._selectedIndex == 0) { return; }
this._selectedIndex--;
this.stepEvent.emit(this._emitStepEvent(this._selectedIndex));
this._focusIndex = this._selectedIndex;
this._setStepFocus();
}

/** Returns a unique id for each step label element. */
_getStepLabelId(i: number): string {
return `mat-step-label-${this._groupId}-${i}`;
}

/** Returns a unique id for each step content element. */
_getStepContentId(i: number): string {
return `mat-step-content-${this._groupId}-${i}`;
}

private _emitStepEvent(index: number): CdkStepEvent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is deceptive since this method doesn't actually cause an emit

const event = new CdkStepEvent();
event.index = index;
event.step = this._steps.toArray()[this._selectedIndex];
this._selectedStep = event.step;
return event;
}

_onKeydown(event: KeyboardEvent) {
switch (event.keyCode) {
case RIGHT_ARROW:
if (this._focusIndex != this._steps.length - 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just call nextStep?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wouldn't be equivalent to nextStep because RIGHT_ARROW does not mean that the next step has been chosen. Only the focus has changed. The user would need to press ENTER to make the actual selection change.

this._focusIndex++;
this._setStepFocus();
}
break;
case LEFT_ARROW:
if (this._focusIndex != 0) {
this._focusIndex--;
this._setStepFocus();
}
break;
case ENTER:
this._selectedIndex = this._focusIndex;
this._emitStepEvent(this._selectedIndex);
break;
}
if (event.keyCode != TAB) {
event.preventDefault();
}
}

_setStepFocus() {
this._stepHeader.toArray()[this._focusIndex].nativeElement.focus();
this.focusChange.emit(this._emitStepEvent(this._selectedIndex));
}
}
8 changes: 6 additions & 2 deletions src/demo-app/demo-app-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -118,7 +120,8 @@ import {TableHeaderDemo} from './table/table-header-demo';
MdTooltipModule,
MdNativeDateModule,
CdkTableModule,
StyleModule
StyleModule,
MdStepperModule,
]
})
export class DemoMaterialModule {}
Expand Down Expand Up @@ -184,6 +187,7 @@ export class DemoMaterialModule {}
PlatformDemo,
TypographyDemo,
ExpansionDemo,
StepperDemo,
],
providers: [
{provide: OverlayContainer, useClass: FullscreenOverlayContainer},
Expand Down
3 changes: 2 additions & 1 deletion src/demo-app/demo-app/demo-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class DemoApp {
{name: 'Tooltip', route: 'tooltip'},
{name: 'Platform', route: 'platform'},
{name: 'Style', route: 'style'},
{name: 'Typography', route: 'typography'}
{name: 'Typography', route: 'typography'},
{name: 'Stepper', route: 'stepper'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you place this in the list alphabetically

];

constructor(private _element: ElementRef) {
Expand Down
2 changes: 2 additions & 0 deletions src/demo-app/demo-app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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},
];
8 changes: 8 additions & 0 deletions src/demo-app/stepper/stepper-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<h2>Horizontal Stepper Demo</h2>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are ok for now, but we should eventually add examples that are more like what a developer would actually want to create.

<mat-horizontal-stepper [(selectedIndex)]="horizontalActiveIndex">
<mat-step *ngFor="let step of steps" [label]="step.label">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: indent 2

<md-input-container>
<input mdInput placeholder="Answer" [(ngModel)]="step.content">
</md-input-container>
</mat-step>
</mat-horizontal-stepper>
Empty file.
19 changes: 19 additions & 0 deletions src/demo-app/stepper/stepper-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Component} from '@angular/core';

@Component({
moduleId: module.id,
selector: 'stepper-demo',
templateUrl: 'stepper-demo.html',
styleUrls: ['stepper-demo.scss'],
})
export class StepperDemo {
verticalActiveIndex = 0;
horizontalActiveIndex = 0;
labelTemplateIndex = 0;
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!'}
];
}
4 changes: 3 additions & 1 deletion src/lib/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -86,7 +87,8 @@ const MATERIAL_MODULES = [
A11yModule,
PlatformModule,
MdCommonModule,
ObserveContentModule
ObserveContentModule,
MdStepperModule,
];

/** @deprecated */
Expand Down
1 change: 1 addition & 0 deletions src/lib/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading