Skip to content

Commit 53c94c7

Browse files
olivierlemasletinayuangao
authored andcommitted
fix(stepper): block linear stepper for pending components (#8646)
Fixes #8645
1 parent d0cb077 commit 53c94c7

File tree

2 files changed

+100
-8
lines changed

2 files changed

+100
-8
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export class CdkStepper implements OnDestroy {
152152
get selectedIndex() { return this._selectedIndex; }
153153
set selectedIndex(index: number) {
154154
if (this._steps) {
155-
if (this._anyControlsInvalid(index) || index < this._selectedIndex &&
155+
if (this._anyControlsInvalidOrPending(index) || index < this._selectedIndex &&
156156
!this._steps.toArray()[index].editable) {
157157
// remove focus from clicked step header if the step is not able to be selected
158158
this._stepHeader.toArray()[index].nativeElement.blur();
@@ -291,13 +291,15 @@ export class CdkStepper implements OnDestroy {
291291
this._stepHeader.toArray()[this._focusIndex].nativeElement.focus();
292292
}
293293

294-
private _anyControlsInvalid(index: number): boolean {
294+
private _anyControlsInvalidOrPending(index: number): boolean {
295295
const steps = this._steps.toArray();
296296

297297
steps[this._selectedIndex].interacted = true;
298298

299299
if (this._linear && index >= 0) {
300-
return steps.slice(0, index).some(step => step.stepControl && step.stepControl.invalid);
300+
return steps.slice(0, index).some(step =>
301+
step.stepControl && (step.stepControl.invalid || step.stepControl.pending)
302+
);
301303
}
302304
return false;
303305
}

src/lib/stepper/stepper.spec.ts

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
33
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
44
import {Component, DebugElement} from '@angular/core';
55
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
6-
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
6+
import {AbstractControl, AsyncValidatorFn, FormControl, FormGroup, ReactiveFormsModule,
7+
ValidationErrors, Validators} from '@angular/forms';
78
import {By} from '@angular/platform-browser';
89
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
10+
import {map} from 'rxjs/operators/map';
11+
import {take} from 'rxjs/operators/take';
12+
import {Observable} from 'rxjs/Observable';
13+
import {Subject} from 'rxjs/Subject';
914
import {MatStepperModule} from './index';
1015
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
1116
import {MatStepperNext, MatStepperPrevious} from './stepper-button';
@@ -156,17 +161,25 @@ describe('MatHorizontalStepper', () => {
156161
expect(stepperComponent.linear).toBe(true);
157162
});
158163

159-
it('should not move to next step if current step is not valid', () => {
164+
it('should not move to next step if current step is invalid', () => {
160165
expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe('');
161166
expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false);
162167
expect(testComponent.oneGroup.valid).toBe(false);
168+
expect(testComponent.oneGroup.invalid).toBe(true);
163169
expect(stepperComponent.selectedIndex).toBe(0);
164170

165171
let stepHeaderEl = fixture.debugElement
166172
.queryAll(By.css('.mat-horizontal-stepper-header'))[1].nativeElement;
167173
assertLinearStepperValidity(stepHeaderEl, testComponent, fixture);
168174
});
169175

176+
it('should not move to next step if current step is pending', () => {
177+
let stepHeaderEl = fixture.debugElement
178+
.queryAll(By.css('.mat-horizontal-stepper-header'))[2].nativeElement;
179+
180+
assertLinearStepperPending(stepHeaderEl, testComponent, fixture);
181+
});
182+
170183
it('should not focus step header upon click if it is not able to be selected', () => {
171184
assertStepHeaderBlurred(fixture);
172185
});
@@ -317,10 +330,11 @@ describe('MatVerticalStepper', () => {
317330
expect(stepperComponent.linear).toBe(true);
318331
});
319332

320-
it('should not move to next step if current step is not valid', () => {
333+
it('should not move to next step if current step is invalid', () => {
321334
expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe('');
322335
expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false);
323336
expect(testComponent.oneGroup.valid).toBe(false);
337+
expect(testComponent.oneGroup.invalid).toBe(true);
324338
expect(stepperComponent.selectedIndex).toBe(0);
325339

326340
let stepHeaderEl = fixture.debugElement
@@ -329,6 +343,13 @@ describe('MatVerticalStepper', () => {
329343
assertLinearStepperValidity(stepHeaderEl, testComponent, fixture);
330344
});
331345

346+
it('should not move to next step if current step is pending', () => {
347+
let stepHeaderEl = fixture.debugElement
348+
.queryAll(By.css('.mat-vertical-stepper-header'))[2].nativeElement;
349+
350+
assertLinearStepperPending(stepHeaderEl, testComponent, fixture);
351+
});
352+
332353
it('should not focus step header upon click if it is not able to be selected', () => {
333354
assertStepHeaderBlurred(fixture);
334355
});
@@ -617,6 +638,58 @@ function assertLinearStepperValidity(stepHeaderEl: HTMLElement,
617638
expect(stepperComponent.selectedIndex).toBe(1);
618639
}
619640

641+
/** Asserts that linear stepper does not allow step selection change if current step is pending. */
642+
function assertLinearStepperPending(stepHeaderEl: HTMLElement,
643+
testComponent:
644+
LinearMatHorizontalStepperApp |
645+
LinearMatVerticalStepperApp,
646+
fixture: ComponentFixture<any>) {
647+
let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;
648+
let nextButtonNativeEl = fixture.debugElement
649+
.queryAll(By.directive(MatStepperNext))[1].nativeElement;
650+
651+
testComponent.oneGroup.get('oneCtrl')!.setValue('input');
652+
testComponent.twoGroup.get('twoCtrl')!.setValue('input');
653+
stepperComponent.selectedIndex = 1;
654+
fixture.detectChanges();
655+
expect(stepperComponent.selectedIndex).toBe(1);
656+
657+
// Step status = PENDING
658+
// Assert that linear stepper does not allow step selection change
659+
expect(testComponent.twoGroup.pending).toBe(true);
660+
661+
stepHeaderEl.click();
662+
fixture.detectChanges();
663+
664+
expect(stepperComponent.selectedIndex).toBe(1);
665+
666+
nextButtonNativeEl.click();
667+
fixture.detectChanges();
668+
669+
expect(stepperComponent.selectedIndex).toBe(1);
670+
671+
// Trigger asynchronous validation
672+
testComponent.validationTrigger.next();
673+
// Asynchronous validation completed:
674+
// Step status = VALID
675+
expect(testComponent.twoGroup.pending).toBe(false);
676+
expect(testComponent.twoGroup.valid).toBe(true);
677+
678+
stepHeaderEl.click();
679+
fixture.detectChanges();
680+
681+
expect(stepperComponent.selectedIndex).toBe(2);
682+
683+
stepperComponent.selectedIndex = 1;
684+
fixture.detectChanges();
685+
expect(stepperComponent.selectedIndex).toBe(1);
686+
687+
nextButtonNativeEl.click();
688+
fixture.detectChanges();
689+
690+
expect(stepperComponent.selectedIndex).toBe(2);
691+
}
692+
620693
/** Asserts that step header focus is blurred if the step cannot be selected upon header click. */
621694
function assertStepHeaderBlurred(fixture: ComponentFixture<any>) {
622695
let stepHeaderEl = fixture.debugElement
@@ -659,6 +732,7 @@ function assertOptionalStepValidity(testComponent:
659732

660733
testComponent.oneGroup.get('oneCtrl')!.setValue('input');
661734
testComponent.twoGroup.get('twoCtrl')!.setValue('input');
735+
testComponent.validationTrigger.next();
662736
stepperComponent.selectedIndex = 2;
663737
fixture.detectChanges();
664738

@@ -706,6 +780,18 @@ function assertCorrectStepIcon(fixture: ComponentFixture<any>,
706780
expect(stepperComponent._getIndicatorType(0)).toBe(icon);
707781
}
708782

783+
function asyncValidator(minLength: number, validationTrigger: Observable<any>): AsyncValidatorFn {
784+
return (control: AbstractControl): Observable<ValidationErrors | null> => {
785+
return validationTrigger.pipe(
786+
map(() => {
787+
const success = control.value && control.value.length >= minLength;
788+
return success ? null : { 'asyncValidation': {}};
789+
}),
790+
take(1)
791+
);
792+
};
793+
}
794+
709795
@Component({
710796
template: `
711797
<mat-horizontal-stepper>
@@ -783,12 +869,14 @@ class LinearMatHorizontalStepperApp {
783869
twoGroup: FormGroup;
784870
threeGroup: FormGroup;
785871

872+
validationTrigger: Subject<any> = new Subject();
873+
786874
ngOnInit() {
787875
this.oneGroup = new FormGroup({
788876
oneCtrl: new FormControl('', Validators.required)
789877
});
790878
this.twoGroup = new FormGroup({
791-
twoCtrl: new FormControl('', Validators.required)
879+
twoCtrl: new FormControl('', Validators.required, asyncValidator(3, this.validationTrigger))
792880
});
793881
this.threeGroup = new FormGroup({
794882
threeCtrl: new FormControl('', Validators.pattern(VALID_REGEX))
@@ -873,12 +961,14 @@ class LinearMatVerticalStepperApp {
873961
twoGroup: FormGroup;
874962
threeGroup: FormGroup;
875963

964+
validationTrigger: Subject<any> = new Subject();
965+
876966
ngOnInit() {
877967
this.oneGroup = new FormGroup({
878968
oneCtrl: new FormControl('', Validators.required)
879969
});
880970
this.twoGroup = new FormGroup({
881-
twoCtrl: new FormControl('', Validators.required)
971+
twoCtrl: new FormControl('', Validators.required, asyncValidator(3, this.validationTrigger))
882972
});
883973
this.threeGroup = new FormGroup({
884974
threeCtrl: new FormControl('', Validators.pattern(VALID_REGEX))

0 commit comments

Comments
 (0)