mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
+26
-17
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user