Skip to content

Commit

Permalink
feat(stepper): initial version of stepper (#6594)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwshinjwshin authored and jelbourn committed Aug 31, 2017
1 parent 70bd5fc commit 87318bc
Show file tree
Hide file tree
Showing 35 changed files with 2,126 additions and 4 deletions.
9 changes: 9 additions & 0 deletions src/cdk/stepper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @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
*/

export * from './public_api';
25 changes: 25 additions & 0 deletions src/cdk/stepper/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @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, CdkStep} from './stepper';
import {CommonModule} from '@angular/common';
import {CdkStepLabel} from './step-label';
import {CdkStepperNext, CdkStepperPrevious} from './stepper-button';
import {BidiModule} from '@angular/cdk/bidi';

@NgModule({
imports: [BidiModule, CommonModule],
exports: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious],
declarations: [CdkStep, CdkStepper, CdkStepLabel, CdkStepperNext, CdkStepperPrevious]
})
export class CdkStepperModule {}

export * from './stepper';
export * from './step-label';
export * from './stepper-button';
16 changes: 16 additions & 0 deletions src/cdk/stepper/step-label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @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} from '@angular/core';

@Directive({
selector: '[cdkStepLabel]',
})
export class CdkStepLabel {
constructor(public template: TemplateRef<any>) { }
}
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>
28 changes: 28 additions & 0 deletions src/cdk/stepper/stepper-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @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} from '@angular/core';
import {CdkStepper} from './stepper';

/** Button that moves to the next step in a stepper workflow. */
@Directive({
selector: 'button[cdkStepperNext]',
host: {'(click)': '_stepper.next()'}
})
export class CdkStepperNext {
constructor(public _stepper: CdkStepper) { }
}

/** Button that moves to the previous step in a stepper workflow. */
@Directive({
selector: 'button[cdkStepperPrevious]',
host: {'(click)': '_stepper.previous()'}
})
export class CdkStepperPrevious {
constructor(public _stepper: CdkStepper) { }
}
263 changes: 263 additions & 0 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/**
* @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 {
ContentChildren,
EventEmitter,
Input,
Output,
QueryList,
Directive,
// This import is only used to define a generic type. The current TypeScript version incorrectly
// considers such imports as unused (https://github.com/Microsoft/TypeScript/issues/14953)
// tslint:disable-next-line:no-unused-variable
ElementRef,
Component,
ContentChild,
ViewChild,
TemplateRef,
ViewEncapsulation,
Optional,
Inject,
forwardRef
} from '@angular/core';
import {LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE} from '@angular/cdk/keycodes';
import {CdkStepLabel} from './step-label';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {AbstractControl} from '@angular/forms';
import {Directionality} from '@angular/cdk/bidi';

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

/**
* Position state of the content of each step in stepper that is used for transitioning
* the content into correct position upon step selection change.
*/
export type StepContentPositionState = 'previous' | 'current' | 'next';

/** Change event emitted on selection changes. */
export class StepperSelectionEvent {
/** Index of the step now selected. */
selectedIndex: number;

/** Index of the step previously selected. */
previouslySelectedIndex: number;

/** The step instance now selected. */
selectedStep: CdkStep;

/** The step instance previously selected. */
previouslySelectedStep: CdkStep;
}

@Component({
selector: 'cdk-step',
templateUrl: 'step.html',
encapsulation: ViewEncapsulation.None
})
export class CdkStep {
/** Template for step label if it exists. */
@ContentChild(CdkStepLabel) stepLabel: CdkStepLabel;

/** Template for step content. */
@ViewChild(TemplateRef) content: TemplateRef<any>;

/** The top level abstract control of the step. */
@Input() stepControl: AbstractControl;

/** Whether user has seen the expanded step content or not . */
interacted = false;

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

@Input()
get editable() { return this._editable; }
set editable(value: any) {
this._editable = coerceBooleanProperty(value);
}
private _editable = true;

/** Whether the completion of step is optional or not. */
@Input()
get optional() { return this._optional; }
set optional(value: any) {
this._optional = coerceBooleanProperty(value);
}
private _optional = false;

/** Return whether step is completed or not. */
@Input()
get completed() {
return this._customCompleted == null ? this._defaultCompleted : this._customCompleted;
}
set completed(value: any) {
this._customCompleted = coerceBooleanProperty(value);
}
private _customCompleted: boolean | null = null;

private get _defaultCompleted() {
return this.stepControl ? this.stepControl.valid && this.interacted : this.interacted;
}

constructor(@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper) { }

/** Selects this step component. */
select(): void {
this._stepper.selected = this;
}
}

@Directive({
selector: '[cdkStepper]',
})
export class CdkStepper {
/** The list of step components that the stepper is holding. */
@ContentChildren(CdkStep) _steps: QueryList<CdkStep>;

/** The list of step headers of the steps in the stepper. */
_stepHeader: QueryList<ElementRef>;

/** Whether the validity of previous steps should be checked or not. */
@Input()
get linear() { return this._linear; }
set linear(value: any) { this._linear = coerceBooleanProperty(value); }
private _linear = false;

/** The index of the selected step. */
@Input()
get selectedIndex() { return this._selectedIndex; }
set selectedIndex(index: number) {
if (this._anyControlsInvalid(index)
|| index < this._selectedIndex && !this._steps.toArray()[index].editable) {
// remove focus from clicked step header if the step is not able to be selected
this._stepHeader.toArray()[index].nativeElement.blur();
} else if (this._selectedIndex != index) {
this._emitStepperSelectionEvent(index);
this._focusIndex = this._selectedIndex;
}
}
private _selectedIndex: number = 0;

/** The step that is selected. */
@Input()
get selected() { return this._steps[this.selectedIndex]; }
set selected(step: CdkStep) {
let index = this._steps.toArray().indexOf(step);
this.selectedIndex = index;
}

/** Event emitted when the selected step has changed. */
@Output() selectionChange = new EventEmitter<StepperSelectionEvent>();

/** The index of the step that the focus can be set. */
_focusIndex: number = 0;

/** Used to track unique ID for each stepper component. */
_groupId: number;

constructor(@Optional() private _dir: Directionality) {
this._groupId = nextId++;
}

/** Selects and focuses the next step in list. */
next(): void {
this.selectedIndex = Math.min(this._selectedIndex + 1, this._steps.length - 1);
}

/** Selects and focuses the previous step in list. */
previous(): void {
this.selectedIndex = Math.max(this._selectedIndex - 1, 0);
}

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

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

/** Returns position state of the step with the given index. */
_getAnimationDirection(index: number): StepContentPositionState {
const position = index - this._selectedIndex;
if (position < 0) {
return 'previous';
} else if (position > 0) {
return 'next';
} else {
return 'current';
}
}

/** Returns the type of icon to be displayed. */
_getIndicatorType(index: number): 'number' | 'edit' | 'done' {
const step = this._steps.toArray()[index];
if (!step.completed || this._selectedIndex == index) {
return 'number';
} else {
return step.editable ? 'edit' : 'done';
}
}

private _emitStepperSelectionEvent(newIndex: number): void {
const stepsArray = this._steps.toArray();
this.selectionChange.emit({
selectedIndex: newIndex,
previouslySelectedIndex: this._selectedIndex,
selectedStep: stepsArray[newIndex],
previouslySelectedStep: stepsArray[this._selectedIndex],
});
this._selectedIndex = newIndex;
}

_onKeydown(event: KeyboardEvent) {
switch (event.keyCode) {
case RIGHT_ARROW:
if (this._dir && this._dir.value === 'rtl') {
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
} else {
this._focusStep((this._focusIndex + 1) % this._steps.length);
}
break;
case LEFT_ARROW:
if (this._dir && this._dir.value === 'rtl') {
this._focusStep((this._focusIndex + 1) % this._steps.length);
} else {
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
}
break;
case SPACE:
case ENTER:
this.selectedIndex = this._focusIndex;
break;
default:
// Return to avoid calling preventDefault on keys that are not explicitly handled.
return;
}
event.preventDefault();
}

private _focusStep(index: number) {
this._focusIndex = index;
this._stepHeader.toArray()[this._focusIndex].nativeElement.focus();
}

private _anyControlsInvalid(index: number): boolean {
const stepsArray = this._steps.toArray();
stepsArray[this._selectedIndex].interacted = true;
if (this._linear) {
return stepsArray.slice(0, index).some(step => step.stepControl.invalid);
}
return false;
}
}
13 changes: 13 additions & 0 deletions src/cdk/stepper/tsconfig-build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../tsconfig-build",
"files": [
"public_api.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/cdk/stepper",
"skipTemplateCodegen": true
}
}
1 change: 1 addition & 0 deletions src/demo-app/demo-app/demo-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class DemoApp {
{name: 'Slider', route: '/slider'},
{name: 'Slide Toggle', route: '/slide-toggle'},
{name: 'Snack Bar', route: '/snack-bar'},
{name: 'Stepper', route: 'stepper'},
{name: 'Table', route: '/table'},
{name: 'Tabs', route: '/tabs'},
{name: 'Toolbar', route: '/toolbar'},
Expand Down
2 changes: 2 additions & 0 deletions src/demo-app/demo-app/demo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {PeopleDatabase} from '../table/people-database';
import {DatepickerDemo} from '../datepicker/datepicker-demo';
import {TypographyDemo} from '../typography/typography-demo';
import {ExpansionDemo} from '../expansion/expansion-demo';
import {StepperDemo} from '../stepper/stepper-demo';
import {DemoMaterialModule} from '../demo-material-module';
import {
FullscreenOverlayContainer,
Expand Down Expand Up @@ -92,6 +93,7 @@ import {TableHeaderDemo} from '../table/table-header-demo';
SliderDemo,
SlideToggleDemo,
SpagettiPanel,
StepperDemo,
StyleDemo,
TableHeaderDemo,
ToolbarDemo,
Expand Down
Loading

0 comments on commit 87318bc

Please sign in to comment.