feat(stepper): Address previous comments + add directionality support (#6775)

* code cleanup + rtl support

* Fix based on review

* modify code for unit tests
This commit is contained in:
Ji Won Shin
2017-09-06 10:28:18 -07:00
committed by tinayuangao
parent 835014a709
commit c396596385
11 changed files with 238 additions and 131 deletions
+26 -17
View File
@@ -30,7 +30,7 @@ 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';
import {Direction, Directionality} from '@angular/cdk/bidi';
/** Used to generate unique ID for each stepper component. */
let nextId = 0;
@@ -151,8 +151,7 @@ export class CdkStepper {
@Input()
get selected() { return this._steps[this.selectedIndex]; }
set selected(step: CdkStep) {
let index = this._steps.toArray().indexOf(step);
this.selectedIndex = index;
this.selectedIndex = this._steps.toArray().indexOf(step);
}
/** Event emitted when the selected step has changed. */
@@ -192,12 +191,11 @@ export class CdkStepper {
_getAnimationDirection(index: number): StepContentPositionState {
const position = index - this._selectedIndex;
if (position < 0) {
return 'previous';
return this._layoutDirection() === 'rtl' ? 'next' : 'previous';
} else if (position > 0) {
return 'next';
} else {
return 'current';
return this._layoutDirection() === 'rtl' ? 'previous' : 'next';
}
return 'current';
}
/** Returns the type of icon to be displayed. */
@@ -224,17 +222,17 @@ export class CdkStepper {
_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);
if (this._layoutDirection() === 'rtl') {
this._focusPreviousStep();
} else {
this._focusStep((this._focusIndex + 1) % this._steps.length);
this._focusNextStep();
}
break;
case LEFT_ARROW:
if (this._dir && this._dir.value === 'rtl') {
this._focusStep((this._focusIndex + 1) % this._steps.length);
if (this._layoutDirection() === 'rtl') {
this._focusNextStep();
} else {
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
this._focusPreviousStep();
}
break;
case SPACE:
@@ -248,17 +246,28 @@ export class CdkStepper {
event.preventDefault();
}
private _focusNextStep() {
this._focusStep((this._focusIndex + 1) % this._steps.length);
}
private _focusPreviousStep() {
this._focusStep((this._focusIndex + this._steps.length - 1) % this._steps.length);
}
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);
this._steps.toArray()[this._selectedIndex].interacted = true;
if (this._linear && index >= 0) {
return this._steps.toArray().slice(0, index).some(step => step.stepControl.invalid);
}
return false;
}
private _layoutDirection(): Direction {
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
}
}
+2 -4
View File
@@ -1,8 +1,6 @@
import {Component} from '@angular/core';
import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms';
const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
@Component({
moduleId: module.id,
selector: 'stepper-demo',
@@ -36,7 +34,7 @@ export class StepperDemo {
lastNameFormCtrl: ['', Validators.required],
}),
this._formBuilder.group({
emailFormCtrl: ['', Validators.pattern(EMAIL_REGEX)]
emailFormCtrl: ['', Validators.email]
}),
])
});
@@ -47,7 +45,7 @@ export class StepperDemo {
});
this.emailFormGroup = this._formBuilder.group({
emailCtrl: ['', Validators.pattern(EMAIL_REGEX)]
emailCtrl: ['', Validators.email]
});
}
}
+5 -5
View File
@@ -13,11 +13,7 @@
background-color: mat-color($background, hover);
}
.mat-step-label-active {
color: mat-color($foreground, text);
}
.mat-step-label-inactive,
.mat-step-label,
.mat-step-optional {
color: mat-color($foreground, disabled-text);
}
@@ -31,6 +27,10 @@
background-color: mat-color($foreground, disabled-text);
color: mat-color($primary, default-contrast);
}
.mat-step-label.mat-step-label-active {
color: mat-color($foreground, text);
}
}
.mat-stepper-horizontal, .mat-stepper-vertical {
+11 -2
View File
@@ -27,8 +27,17 @@ import {MdStepHeader} from './step-header';
CdkStepperModule,
MdIconModule
],
exports: [MdCommonModule, MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper,
MdStepperNext, MdStepperPrevious, MdStepHeader],
exports: [
MdCommonModule,
MdHorizontalStepper,
MdVerticalStepper,
MdStep,
MdStepLabel,
MdStepper,
MdStepperNext,
MdStepperPrevious,
MdStepHeader
],
declarations: [MdHorizontalStepper, MdVerticalStepper, MdStep, MdStepLabel, MdStepper,
MdStepperNext, MdStepperPrevious, MdStepHeader],
})
+10 -9
View File
@@ -1,16 +1,17 @@
<div [class.mat-step-icon]="icon != 'number' || selected"
[class.mat-step-icon-not-touched]="icon == 'number' && !selected">
<span *ngIf="icon == 'number'">{{index + 1}}</span>
<md-icon *ngIf="icon == 'edit'">create</md-icon>
<md-icon *ngIf="icon == 'done'">done</md-icon>
<div [class.mat-step-icon]="icon !== 'number' || selected"
[class.mat-step-icon-not-touched]="icon == 'number' && !selected"
[ngSwitch]="icon">
<span *ngSwitchCase="'number'">{{index + 1}}</span>
<md-icon *ngSwitchCase="'edit'">create</md-icon>
<md-icon *ngSwitchCase="'done'">done</md-icon>
</div>
<div [class.mat-step-label-active]="active"
[class.mat-step-label-inactive]="!active">
<div class="mat-step-label"
[class.mat-step-label-active]="active">
<!-- If there is a label template, use it. -->
<ng-container *ngIf="_templateLabel" [ngTemplateOutlet]="label.template">
<ng-container *ngIf="_templateLabel()" [ngTemplateOutlet]="label.template">
</ng-container>
<!-- It there is no label template, fall back to the text label. -->
<div class="mat-step-text-label" *ngIf="_stringLabel">{{label}}</div>
<div class="mat-step-text-label" *ngIf="_stringLabel()">{{label}}</div>
<div class="mat-step-optional" *ngIf="optional">Optional</div>
</div>
+1 -2
View File
@@ -26,8 +26,7 @@ $mat-step-header-icon-size: 16px !default;
width: $mat-step-header-icon-size;
}
.mat-step-label-active,
.mat-step-label-inactive {
.mat-step-label {
display: inline-block;
white-space: nowrap;
overflow: hidden;
+4 -6
View File
@@ -23,12 +23,10 @@ import {MdStepLabel} from './step-label';
})
export class MdStepHeader {
/** Icon for the given step. */
@Input()
icon: string;
@Input() icon: string;
/** Label of the given step. */
@Input()
label: MdStepLabel | string;
@Input() label: MdStepLabel | string;
/** Index of the given step. */
@Input()
@@ -63,12 +61,12 @@ export class MdStepHeader {
private _optional: boolean;
/** Returns string label of given step if it is a text label. */
get _stringLabel(): string | null {
_stringLabel(): string | null {
return this.label instanceof MdStepLabel ? null : this.label;
}
/** Returns MdStepLabel if the label of given step is a template label. */
get _templateLabel(): MdStepLabel | null {
_templateLabel(): MdStepLabel | null {
return this.label instanceof MdStepLabel ? this.label : null;
}
}
+4 -4
View File
@@ -3,15 +3,15 @@
<md-step-header class="mat-horizontal-stepper-header"
(click)="step.select()"
(keydown)="_onKeydown($event)"
[tabIndex]="_focusIndex == i ? 0 : -1"
[tabIndex]="_focusIndex === i ? 0 : -1"
[id]="_getStepLabelId(i)"
[attr.aria-controls]="_getStepContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[index]="i"
[icon]="_getIndicatorType(i)"
[label]="step.stepLabel || step.label"
[selected]="selectedIndex == i"
[active]="step.completed || selectedIndex == i"
[selected]="selectedIndex === i"
[active]="step.completed || selectedIndex === i"
[optional]="step.optional">
</md-step-header>
<div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
@@ -24,7 +24,7 @@
[@stepTransition]="_getAnimationDirection(i)"
[id]="_getStepContentId(i)"
[attr.aria-labelledby]="_getStepLabelId(i)"
[attr.aria-expanded]="selectedIndex == i">
[attr.aria-expanded]="selectedIndex === i">
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
</div>
</div>
+4 -4
View File
@@ -5,12 +5,12 @@
[tabIndex]="_focusIndex == i ? 0 : -1"
[id]="_getStepLabelId(i)"
[attr.aria-controls]="_getStepContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[attr.aria-selected]="selectedIndex === i"
[index]="i"
[icon]="_getIndicatorType(i)"
[label]="step.stepLabel || step.label"
[selected]="selectedIndex == i"
[active]="step.completed || selectedIndex == i"
[selected]="selectedIndex === i"
[active]="step.completed || selectedIndex === i"
[optional]="step.optional">
</md-step-header>
@@ -19,7 +19,7 @@
[@stepTransition]="_getAnimationDirection(i)"
[id]="_getStepContentId(i)"
[attr.aria-labelledby]="_getStepLabelId(i)"
[attr.aria-expanded]="selectedIndex == i">
[attr.aria-expanded]="selectedIndex === i">
<div class="mat-vertical-content">
<ng-container [ngTemplateOutlet]="step.content"></ng-container>
</div>
+166 -75
View File
@@ -8,16 +8,22 @@ import {MdStepperNext, MdStepperPrevious} from './stepper-button';
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
import {MdStepper, MdHorizontalStepper, MdVerticalStepper} from './stepper';
import {Directionality} from '../core';
const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
const VALID_REGEX = /valid/;
describe('MdHorizontalStepper', () => {
let dir = 'ltr';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdStepperModule, NoopAnimationsModule, ReactiveFormsModule],
declarations: [
SimpleMdHorizontalStepperApp,
LinearMdHorizontalStepperApp
],
providers: [
{provide: Directionality, useFactory: () => ({value: dir})}
]
});
@@ -26,23 +32,21 @@ describe('MdHorizontalStepper', () => {
describe('basic horizontal stepper', () => {
let fixture: ComponentFixture<SimpleMdHorizontalStepperApp>;
let stepperComponent: MdHorizontalStepper;
beforeEach(() => {
fixture = TestBed.createComponent(SimpleMdHorizontalStepperApp);
fixture.detectChanges();
stepperComponent = fixture.debugElement
.query(By.css('md-horizontal-stepper')).componentInstance;
});
it('should default to the first step', () => {
let stepperComponent = fixture.debugElement
.query(By.css('md-horizontal-stepper')).componentInstance;
expect(stepperComponent.selectedIndex).toBe(0);
});
it('should change selected index on header click', () => {
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
assertSelectionChangeOnHeaderClick(stepperComponent, fixture, stepHeaders);
assertSelectionChangeOnHeaderClick(fixture, stepHeaders);
});
it('should set the "tablist" role on stepper', () => {
@@ -52,44 +56,63 @@ describe('MdHorizontalStepper', () => {
it('should set aria-expanded of content correctly', () => {
let stepContents = fixture.debugElement.queryAll(By.css(`.mat-horizontal-stepper-content`));
assertCorrectAriaExpandedAttribute(stepperComponent, fixture, stepContents);
assertCorrectAriaExpandedAttribute(fixture, stepContents);
});
it('should display the correct label', () => {
assertCorrectStepLabel(stepperComponent, fixture);
assertCorrectStepLabel(fixture);
});
it('should go to next available step when the next button is clicked', () => {
assertNextStepperButtonClick(stepperComponent, fixture);
assertNextStepperButtonClick(fixture);
});
it('should go to previous available step when the previous button is clicked', () => {
assertPreviousStepperButtonClick(stepperComponent, fixture);
assertPreviousStepperButtonClick(fixture);
});
it('should set the correct step position for animation', () => {
assertCorrectStepPosition(stepperComponent, fixture);
assertCorrectStepAnimationDirection(fixture);
});
it('should support keyboard events to move and select focus', () => {
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
assertCorrectKeyboardInteraction(stepperComponent, fixture, stepHeaders);
assertCorrectKeyboardInteraction(fixture, stepHeaders);
});
it('should not set focus on header of selected step if header is not clicked', () => {
assertStepHeaderFocusNotCalled(stepperComponent, fixture);
assertStepHeaderFocusNotCalled(fixture);
});
it('should only be able to return to a previous step if it is editable', () => {
assertEditableStepChange(stepperComponent, fixture);
assertEditableStepChange(fixture);
});
it('should set create icon if step is editable and completed', () => {
assertCorrectStepIcon(stepperComponent, fixture, true, 'edit');
assertCorrectStepIcon(fixture, true, 'edit');
});
it('should set done icon if step is not editable and is completed', () => {
assertCorrectStepIcon(stepperComponent, fixture, false, 'done');
assertCorrectStepIcon(fixture, false, 'done');
});
});
describe('RTL', () => {
let fixture: ComponentFixture<SimpleMdHorizontalStepperApp>;
beforeEach(() => {
dir = 'rtl';
fixture = TestBed.createComponent(SimpleMdHorizontalStepperApp);
fixture.detectChanges();
});
it('should reverse arrow key focus in RTL mode', () => {
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
assertArrowKeyInteractionInRtl(fixture, stepHeaders);
});
it('should reverse animation in RTL mode', () => {
assertCorrectStepAnimationDirection(fixture, 'rtl');
});
});
@@ -119,7 +142,7 @@ describe('MdHorizontalStepper', () => {
let stepHeaderEl = fixture.debugElement
.queryAll(By.css('.mat-horizontal-stepper-header'))[1].nativeElement;
assertLinearStepperValidity(stepHeaderEl, stepperComponent, testComponent, fixture);
assertLinearStepperValidity(stepHeaderEl, testComponent, fixture);
});
it('should not focus step header upon click if it is not able to be selected', () => {
@@ -127,18 +150,23 @@ describe('MdHorizontalStepper', () => {
});
it('should be able to move to next step even when invalid if current step is optional', () => {
assertOptionalStepValidity(stepperComponent, testComponent, fixture);
assertOptionalStepValidity(testComponent, fixture);
});
});
});
describe('MdVerticalStepper', () => {
let dir = 'ltr';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdStepperModule, NoopAnimationsModule, ReactiveFormsModule],
declarations: [
SimpleMdVerticalStepperApp,
LinearMdVerticalStepperApp
],
providers: [
{provide: Directionality, useFactory: () => ({value: dir})}
]
});
@@ -147,23 +175,21 @@ describe('MdVerticalStepper', () => {
describe('basic vertical stepper', () => {
let fixture: ComponentFixture<SimpleMdVerticalStepperApp>;
let stepperComponent: MdVerticalStepper;
beforeEach(() => {
fixture = TestBed.createComponent(SimpleMdVerticalStepperApp);
fixture.detectChanges();
stepperComponent = fixture.debugElement
.query(By.css('md-vertical-stepper')).componentInstance;
});
it('should default to the first step', () => {
let stepperComponent = fixture.debugElement
.query(By.css('md-vertical-stepper')).componentInstance;
expect(stepperComponent.selectedIndex).toBe(0);
});
it('should change selected index on header click', () => {
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
assertSelectionChangeOnHeaderClick(stepperComponent, fixture, stepHeaders);
assertSelectionChangeOnHeaderClick(fixture, stepHeaders);
});
@@ -174,44 +200,63 @@ describe('MdVerticalStepper', () => {
it('should set aria-expanded of content correctly', () => {
let stepContents = fixture.debugElement.queryAll(By.css(`.mat-vertical-stepper-content`));
assertCorrectAriaExpandedAttribute(stepperComponent, fixture, stepContents);
assertCorrectAriaExpandedAttribute(fixture, stepContents);
});
it('should display the correct label', () => {
assertCorrectStepLabel(stepperComponent, fixture);
assertCorrectStepLabel(fixture);
});
it('should go to next available step when the next button is clicked', () => {
assertNextStepperButtonClick(stepperComponent, fixture);
assertNextStepperButtonClick(fixture);
});
it('should go to previous available step when the previous button is clicked', () => {
assertPreviousStepperButtonClick(stepperComponent, fixture);
assertPreviousStepperButtonClick(fixture);
});
it('should set the correct step position for animation', () => {
assertCorrectStepPosition(stepperComponent, fixture);
assertCorrectStepAnimationDirection(fixture);
});
it('should support keyboard events to move and select focus', () => {
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
assertCorrectKeyboardInteraction(stepperComponent, fixture, stepHeaders);
assertCorrectKeyboardInteraction(fixture, stepHeaders);
});
it('should not set focus on header of selected step if header is not clicked', () => {
assertStepHeaderFocusNotCalled(stepperComponent, fixture);
assertStepHeaderFocusNotCalled(fixture);
});
it('should only be able to return to a previous step if it is editable', () => {
assertEditableStepChange(stepperComponent, fixture);
assertEditableStepChange(fixture);
});
it('should set create icon if step is editable and completed', () => {
assertCorrectStepIcon(stepperComponent, fixture, true, 'edit');
assertCorrectStepIcon(fixture, true, 'edit');
});
it('should set done icon if step is not editable and is completed', () => {
assertCorrectStepIcon(stepperComponent, fixture, false, 'done');
assertCorrectStepIcon(fixture, false, 'done');
});
});
describe('RTL', () => {
let fixture: ComponentFixture<SimpleMdVerticalStepperApp>;
beforeEach(() => {
dir = 'rtl';
fixture = TestBed.createComponent(SimpleMdVerticalStepperApp);
fixture.detectChanges();
});
it('should reverse arrow key focus in RTL mode', () => {
let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
assertArrowKeyInteractionInRtl(fixture, stepHeaders);
});
it('should reverse animation in RTL mode', () => {
assertCorrectStepAnimationDirection(fixture, 'rtl');
});
});
@@ -242,7 +287,7 @@ describe('MdVerticalStepper', () => {
let stepHeaderEl = fixture.debugElement
.queryAll(By.css('.mat-vertical-stepper-header'))[1].nativeElement;
assertLinearStepperValidity(stepHeaderEl, stepperComponent, testComponent, fixture);
assertLinearStepperValidity(stepHeaderEl, testComponent, fixture);
});
it('should not focus step header upon click if it is not able to be selected', () => {
@@ -250,15 +295,16 @@ describe('MdVerticalStepper', () => {
});
it('should be able to move to next step even when invalid if current step is optional', () => {
assertOptionalStepValidity(stepperComponent, testComponent, fixture);
assertOptionalStepValidity(testComponent, fixture);
});
});
});
/** Asserts that `selectedIndex` updates correctly when header of another step is clicked. */
function assertSelectionChangeOnHeaderClick(stepperComponent: MdStepper,
fixture: ComponentFixture<any>,
stepHeaders: DebugElement[]) {
function assertSelectionChangeOnHeaderClick(fixture: ComponentFixture<any>,
stepHeaders: DebugElement[]) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
expect(stepperComponent.selectedIndex).toBe(0);
// select the second step
@@ -277,9 +323,9 @@ function assertSelectionChangeOnHeaderClick(stepperComponent: MdStepper,
}
/** Asserts that 'aria-expanded' attribute is correct for expanded content of step. */
function assertCorrectAriaExpandedAttribute(stepperComponent: MdStepper,
fixture: ComponentFixture<any>,
stepContents: DebugElement[]) {
function assertCorrectAriaExpandedAttribute(fixture: ComponentFixture<any>,
stepContents: DebugElement[]) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
let firstStepContentEl = stepContents[0].nativeElement;
expect(firstStepContentEl.getAttribute('aria-expanded')).toBe('true');
@@ -292,7 +338,8 @@ function assertCorrectAriaExpandedAttribute(stepperComponent: MdStepper,
}
/** Asserts that step has correct label. */
function assertCorrectStepLabel(stepperComponent: MdStepper, fixture: ComponentFixture<any>) {
function assertCorrectStepLabel(fixture: ComponentFixture<any>) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
let selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
expect(selectedLabel.textContent).toMatch('Step 1');
@@ -310,7 +357,9 @@ function assertCorrectStepLabel(stepperComponent: MdStepper, fixture: ComponentF
}
/** Asserts that clicking on MdStepperNext button updates `selectedIndex` correctly. */
function assertNextStepperButtonClick(stepperComponent: MdStepper, fixture: ComponentFixture<any>) {
function assertNextStepperButtonClick(fixture: ComponentFixture<any>) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
expect(stepperComponent.selectedIndex).toBe(0);
let nextButtonNativeEl = fixture.debugElement
@@ -336,8 +385,9 @@ function assertNextStepperButtonClick(stepperComponent: MdStepper, fixture: Comp
}
/** Asserts that clicking on MdStepperPrevious button updates `selectedIndex` correctly. */
function assertPreviousStepperButtonClick(stepperComponent: MdStepper,
fixture: ComponentFixture<any>) {
function assertPreviousStepperButtonClick(fixture: ComponentFixture<any>) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
expect(stepperComponent.selectedIndex).toBe(0);
stepperComponent.selectedIndex = 2;
@@ -364,30 +414,48 @@ function assertPreviousStepperButtonClick(stepperComponent: MdStepper,
}
/** Asserts that step position is correct for animation. */
function assertCorrectStepPosition(stepperComponent: MdStepper, fixture: ComponentFixture<any>) {
function assertCorrectStepAnimationDirection(fixture: ComponentFixture<any>, rtl?: 'rtl') {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
expect(stepperComponent._getAnimationDirection(0)).toBe('current');
expect(stepperComponent._getAnimationDirection(1)).toBe('next');
expect(stepperComponent._getAnimationDirection(2)).toBe('next');
if (rtl === 'rtl') {
expect(stepperComponent._getAnimationDirection(1)).toBe('previous');
expect(stepperComponent._getAnimationDirection(2)).toBe('previous');
} else {
expect(stepperComponent._getAnimationDirection(1)).toBe('next');
expect(stepperComponent._getAnimationDirection(2)).toBe('next');
}
stepperComponent.selectedIndex = 1;
fixture.detectChanges();
expect(stepperComponent._getAnimationDirection(0)).toBe('previous');
if (rtl === 'rtl') {
expect(stepperComponent._getAnimationDirection(0)).toBe('next');
expect(stepperComponent._getAnimationDirection(2)).toBe('previous');
} else {
expect(stepperComponent._getAnimationDirection(0)).toBe('previous');
expect(stepperComponent._getAnimationDirection(2)).toBe('next');
}
expect(stepperComponent._getAnimationDirection(1)).toBe('current');
expect(stepperComponent._getAnimationDirection(2)).toBe('next');
stepperComponent.selectedIndex = 2;
fixture.detectChanges();
expect(stepperComponent._getAnimationDirection(0)).toBe('previous');
expect(stepperComponent._getAnimationDirection(1)).toBe('previous');
if (rtl === 'rtl') {
expect(stepperComponent._getAnimationDirection(0)).toBe('next');
expect(stepperComponent._getAnimationDirection(1)).toBe('next');
} else {
expect(stepperComponent._getAnimationDirection(0)).toBe('previous');
expect(stepperComponent._getAnimationDirection(1)).toBe('previous');
}
expect(stepperComponent._getAnimationDirection(2)).toBe('current');
}
/** Asserts that keyboard interaction works correctly. */
function assertCorrectKeyboardInteraction(stepperComponent: MdStepper,
fixture: ComponentFixture<any>,
stepHeaders: DebugElement[]) {
function assertCorrectKeyboardInteraction(fixture: ComponentFixture<any>,
stepHeaders: DebugElement[]) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
expect(stepperComponent._focusIndex).toBe(0);
expect(stepperComponent.selectedIndex).toBe(0);
@@ -396,7 +464,7 @@ function assertCorrectKeyboardInteraction(stepperComponent: MdStepper,
fixture.detectChanges();
expect(stepperComponent._focusIndex)
.toBe(1, 'Expected index of focused step to be increased by 1 after RIGHT_ARROW event.');
.toBe(1, 'Expected index of focused step to increase by 1 after RIGHT_ARROW event.');
expect(stepperComponent.selectedIndex)
.toBe(0, 'Expected index of selected step to remain unchanged after RIGHT_ARROW event.');
@@ -408,7 +476,7 @@ function assertCorrectKeyboardInteraction(stepperComponent: MdStepper,
.toBe(1, 'Expected index of focused step to remain unchanged after ENTER event.');
expect(stepperComponent.selectedIndex)
.toBe(1,
'Expected index of selected step to change to index of focused step after EVENT event.');
'Expected index of selected step to change to index of focused step after ENTER event.');
stepHeaderEl = stepHeaders[1].nativeElement;
dispatchKeyboardEvent(stepHeaderEl, 'keydown', LEFT_ARROW);
@@ -444,10 +512,9 @@ function assertCorrectKeyboardInteraction(stepperComponent: MdStepper,
}
/** Asserts that step selection change using stepper buttons does not focus step header. */
function assertStepHeaderFocusNotCalled(stepperComponent: MdStepper,
fixture: ComponentFixture<any>) {
let stepHeaderEl = fixture.debugElement
.queryAll(By.css('md-step-header'))[1].nativeElement;
function assertStepHeaderFocusNotCalled(fixture: ComponentFixture<any>) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
let stepHeaderEl = fixture.debugElement.queryAll(By.css('md-step-header'))[1].nativeElement;
let nextButtonNativeEl = fixture.debugElement
.queryAll(By.directive(MdStepperNext))[0].nativeElement;
spyOn(stepHeaderEl, 'focus');
@@ -458,14 +525,35 @@ function assertStepHeaderFocusNotCalled(stepperComponent: MdStepper,
expect(stepHeaderEl.focus).not.toHaveBeenCalled();
}
/** Asserts that arrow key direction works correctly in RTL mode. */
function assertArrowKeyInteractionInRtl(fixture: ComponentFixture<any>,
stepHeaders: DebugElement[]) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
expect(stepperComponent._focusIndex).toBe(0);
let stepHeaderEl = stepHeaders[0].nativeElement;
dispatchKeyboardEvent(stepHeaderEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(stepperComponent._focusIndex).toBe(1);
stepHeaderEl = stepHeaders[1].nativeElement;
dispatchKeyboardEvent(stepHeaderEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(stepperComponent._focusIndex).toBe(0);
}
/**
* Asserts that linear stepper does not allow step selection change if current step is not valid.
*/
function assertLinearStepperValidity(stepHeaderEl: HTMLElement,
stepperComponent: MdStepper,
testComponent:
LinearMdHorizontalStepperApp | LinearMdVerticalStepperApp,
fixture: ComponentFixture<any>) {
testComponent:
LinearMdHorizontalStepperApp | LinearMdVerticalStepperApp,
fixture: ComponentFixture<any>) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
stepHeaderEl.click();
fixture.detectChanges();
@@ -498,8 +586,9 @@ function assertStepHeaderBlurred(fixture: ComponentFixture<any>) {
}
/** Asserts that it is only possible to go back to a previous step if the step is editable. */
function assertEditableStepChange(stepperComponent: MdStepper,
fixture: ComponentFixture<any>) {
function assertEditableStepChange(fixture: ComponentFixture<any>) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
stepperComponent.selectedIndex = 1;
stepperComponent._steps.toArray()[0].editable = false;
let previousButtonNativeEl = fixture.debugElement
@@ -520,9 +609,11 @@ function assertEditableStepChange(stepperComponent: MdStepper,
* Asserts that it is possible to skip an optional step in linear stepper if there is no input
* or the input is valid.
*/
function assertOptionalStepValidity(stepperComponent: MdStepper,
testComponent: LinearMdHorizontalStepperApp | LinearMdVerticalStepperApp,
fixture: ComponentFixture<any>) {
function assertOptionalStepValidity(testComponent:
LinearMdHorizontalStepperApp | LinearMdVerticalStepperApp,
fixture: ComponentFixture<any>) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
testComponent.oneGroup.get('oneCtrl')!.setValue('input');
testComponent.twoGroup.get('twoCtrl')!.setValue('input');
stepperComponent.selectedIndex = 2;
@@ -548,7 +639,7 @@ function assertOptionalStepValidity(stepperComponent: MdStepper,
expect(stepperComponent.selectedIndex)
.toBe(2, 'Expected selectedIndex to remain unchanged when optional step input is invalid.');
testComponent.threeGroup.get('threeCtrl')!.setValue('123@gmail.com');
testComponent.threeGroup.get('threeCtrl')!.setValue('valid');
nextButtonNativeEl.click();
fixture.detectChanges();
@@ -558,10 +649,10 @@ function assertOptionalStepValidity(stepperComponent: MdStepper,
}
/** Asserts that step header set the correct icon depending on the state of step. */
function assertCorrectStepIcon(stepperComponent: MdStepper,
fixture: ComponentFixture<any>,
function assertCorrectStepIcon(fixture: ComponentFixture<any>,
isEditable: boolean,
icon: String) {
let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance;
let nextButtonNativeEl = fixture.debugElement
.queryAll(By.directive(MdStepperNext))[0].nativeElement;
expect(stepperComponent._getIndicatorType(0)).toBe('number');
@@ -663,7 +754,7 @@ class LinearMdHorizontalStepperApp {
twoCtrl: new FormControl('', Validators.required)
});
this.threeGroup = new FormGroup({
threeCtrl: new FormControl('', Validators.pattern(EMAIL_REGEX))
threeCtrl: new FormControl('', Validators.pattern(VALID_REGEX))
});
}
}
@@ -759,7 +850,7 @@ class LinearMdVerticalStepperApp {
twoCtrl: new FormControl('', Validators.required)
});
this.threeGroup = new FormGroup({
threeCtrl: new FormControl('', Validators.pattern(EMAIL_REGEX))
threeCtrl: new FormControl('', Validators.pattern(VALID_REGEX))
});
}
}
+5 -3
View File
@@ -55,9 +55,11 @@ export class MdStep extends _MdStep implements ErrorOptions {
constructor(@Inject(forwardRef(() => MdStepper)) mdStepper: MdStepper,
@Optional() @SkipSelf() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
super(mdStepper);
this._originalErrorStateMatcher =
errorOptions ? errorOptions.errorStateMatcher || defaultErrorStateMatcher
: defaultErrorStateMatcher;
if (errorOptions && errorOptions.errorStateMatcher) {
this._originalErrorStateMatcher = errorOptions.errorStateMatcher;
} else {
this._originalErrorStateMatcher = defaultErrorStateMatcher;
}
}
/** Custom error state matcher that additionally checks for validity of interacted form. */