From c3965963854729535a8297c894b95c4af2f7ae3f Mon Sep 17 00:00:00 2001 From: Ji Won Shin Date: Wed, 6 Sep 2017 10:28:18 -0700 Subject: [PATCH] feat(stepper): Address previous comments + add directionality support (#6775) * code cleanup + rtl support * Fix based on review * modify code for unit tests --- src/cdk/stepper/stepper.ts | 43 +++-- src/demo-app/stepper/stepper-demo.ts | 6 +- src/lib/stepper/_stepper-theme.scss | 10 +- src/lib/stepper/index.ts | 13 +- src/lib/stepper/step-header.html | 19 +- src/lib/stepper/step-header.scss | 3 +- src/lib/stepper/step-header.ts | 10 +- src/lib/stepper/stepper-horizontal.html | 8 +- src/lib/stepper/stepper-vertical.html | 8 +- src/lib/stepper/stepper.spec.ts | 241 ++++++++++++++++-------- src/lib/stepper/stepper.ts | 8 +- 11 files changed, 238 insertions(+), 131 deletions(-) diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 089555589f21..eb0b5b9b4449 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -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'; + } } diff --git a/src/demo-app/stepper/stepper-demo.ts b/src/demo-app/stepper/stepper-demo.ts index 77cfb3535913..373c298c5fd4 100644 --- a/src/demo-app/stepper/stepper-demo.ts +++ b/src/demo-app/stepper/stepper-demo.ts @@ -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] }); } } diff --git a/src/lib/stepper/_stepper-theme.scss b/src/lib/stepper/_stepper-theme.scss index fc4e339a49c0..c7b17278a122 100644 --- a/src/lib/stepper/_stepper-theme.scss +++ b/src/lib/stepper/_stepper-theme.scss @@ -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 { diff --git a/src/lib/stepper/index.ts b/src/lib/stepper/index.ts index a574fbc53997..46ba69495ace 100644 --- a/src/lib/stepper/index.ts +++ b/src/lib/stepper/index.ts @@ -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], }) diff --git a/src/lib/stepper/step-header.html b/src/lib/stepper/step-header.html index 3f2310862f6f..b385c5b43974 100644 --- a/src/lib/stepper/step-header.html +++ b/src/lib/stepper/step-header.html @@ -1,16 +1,17 @@ -
- {{index + 1}} - create - done +
+ {{index + 1}} + create + done
-
+
- + -
{{label}}
+
{{label}}
Optional
diff --git a/src/lib/stepper/step-header.scss b/src/lib/stepper/step-header.scss index 9313a57e4a48..33cb813902ac 100644 --- a/src/lib/stepper/step-header.scss +++ b/src/lib/stepper/step-header.scss @@ -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; diff --git a/src/lib/stepper/step-header.ts b/src/lib/stepper/step-header.ts index b6624e2424c5..ae79e5cdf70e 100644 --- a/src/lib/stepper/step-header.ts +++ b/src/lib/stepper/step-header.ts @@ -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; } } diff --git a/src/lib/stepper/stepper-horizontal.html b/src/lib/stepper/stepper-horizontal.html index 5e2d2ea80fa7..8b32e92e93a3 100644 --- a/src/lib/stepper/stepper-horizontal.html +++ b/src/lib/stepper/stepper-horizontal.html @@ -3,15 +3,15 @@
@@ -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">
diff --git a/src/lib/stepper/stepper-vertical.html b/src/lib/stepper/stepper-vertical.html index b73bed61929b..bccd9bf5f4df 100644 --- a/src/lib/stepper/stepper-vertical.html +++ b/src/lib/stepper/stepper-vertical.html @@ -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"> @@ -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">
diff --git a/src/lib/stepper/stepper.spec.ts b/src/lib/stepper/stepper.spec.ts index b5965f9d4751..13c357d80b62 100644 --- a/src/lib/stepper/stepper.spec.ts +++ b/src/lib/stepper/stepper.spec.ts @@ -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; - 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; + + 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; - 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; + + 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, - stepHeaders: DebugElement[]) { +function assertSelectionChangeOnHeaderClick(fixture: ComponentFixture, + 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, - stepContents: DebugElement[]) { +function assertCorrectAriaExpandedAttribute(fixture: ComponentFixture, + 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) { +function assertCorrectStepLabel(fixture: ComponentFixture) { + 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) { +function assertNextStepperButtonClick(fixture: ComponentFixture) { + 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) { +function assertPreviousStepperButtonClick(fixture: ComponentFixture) { + 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) { +function assertCorrectStepAnimationDirection(fixture: ComponentFixture, 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, - stepHeaders: DebugElement[]) { +function assertCorrectKeyboardInteraction(fixture: ComponentFixture, + 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) { - let stepHeaderEl = fixture.debugElement - .queryAll(By.css('md-step-header'))[1].nativeElement; +function assertStepHeaderFocusNotCalled(fixture: ComponentFixture) { + 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, + 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) { + testComponent: + LinearMdHorizontalStepperApp | LinearMdVerticalStepperApp, + fixture: ComponentFixture) { + let stepperComponent = fixture.debugElement.query(By.directive(MdStepper)).componentInstance; + stepHeaderEl.click(); fixture.detectChanges(); @@ -498,8 +586,9 @@ function assertStepHeaderBlurred(fixture: ComponentFixture) { } /** Asserts that it is only possible to go back to a previous step if the step is editable. */ -function assertEditableStepChange(stepperComponent: MdStepper, - fixture: ComponentFixture) { +function assertEditableStepChange(fixture: ComponentFixture) { + 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) { +function assertOptionalStepValidity(testComponent: + LinearMdHorizontalStepperApp | LinearMdVerticalStepperApp, + fixture: ComponentFixture) { + 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, +function assertCorrectStepIcon(fixture: ComponentFixture, 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)) }); } } diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts index 4ec7fd207d29..6f40496d2cef 100644 --- a/src/lib/stepper/stepper.ts +++ b/src/lib/stepper/stepper.ts @@ -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. */