Skip to content

Commit d26aa6e

Browse files
mmalerbajelbourn
authored andcommitted
feat(stepper): require users to visit non-optional steps (#10048)
1 parent 2837196 commit d26aa6e

File tree

2 files changed

+76
-11
lines changed

2 files changed

+76
-11
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,9 @@ export class CdkStepper implements OnDestroy {
349349
if (this._linear && index >= 0) {
350350
return steps.slice(0, index).some(step => {
351351
const control = step.stepControl;
352-
const isIncomplete = control ? (control.invalid || control.pending) : !step.completed;
352+
const isIncomplete = control ?
353+
(control.invalid || control.pending || !step.interacted) :
354+
!step.completed;
353355
return isIncomplete && !step.optional;
354356
});
355357
}

src/lib/stepper/stepper.spec.ts

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
1-
import {Directionality, Direction} from '@angular/cdk/bidi';
1+
import {Direction, Directionality} from '@angular/cdk/bidi';
22
import {
3+
DOWN_ARROW,
4+
END,
35
ENTER,
6+
HOME,
47
LEFT_ARROW,
58
RIGHT_ARROW,
6-
UP_ARROW,
7-
DOWN_ARROW,
89
SPACE,
9-
HOME,
10-
END,
10+
UP_ARROW,
1111
} from '@angular/cdk/keycodes';
12+
import {StepperOrientation} from '@angular/cdk/stepper';
1213
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
1314
import {Component, DebugElement} from '@angular/core';
14-
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
15-
import {AbstractControl, AsyncValidatorFn, FormControl, FormGroup, ReactiveFormsModule,
16-
ValidationErrors, Validators} from '@angular/forms';
15+
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
16+
import {
17+
AbstractControl,
18+
AsyncValidatorFn,
19+
FormControl,
20+
FormGroup,
21+
ReactiveFormsModule,
22+
ValidationErrors,
23+
Validators
24+
} from '@angular/forms';
1725
import {By} from '@angular/platform-browser';
1826
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
19-
import {StepperOrientation} from '@angular/cdk/stepper';
27+
import {Observable} from 'rxjs/Observable';
2028
import {map} from 'rxjs/operators/map';
2129
import {take} from 'rxjs/operators/take';
22-
import {Observable} from 'rxjs/Observable';
2330
import {Subject} from 'rxjs/Subject';
2431
import {MatStepperModule} from './index';
2532
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
@@ -44,6 +51,7 @@ describe('MatStepper', () => {
4451
SimpleStepperWithoutStepControl,
4552
SimpleStepperWithStepControlAndCompletedBinding,
4653
SimpleMatHorizontalStepperApp,
54+
LinearStepperWithValidOptionalStep,
4755
],
4856
providers: [
4957
{provide: Directionality, useFactory: () => ({value: dir})}
@@ -491,6 +499,8 @@ describe('MatStepper', () => {
491499
testComponent.oneGroup.get('oneCtrl')!.setValue('input');
492500
testComponent.twoGroup.get('twoCtrl')!.setValue('input');
493501
testComponent.validationTrigger.next();
502+
stepperComponent.selectedIndex = 1;
503+
fixture.detectChanges();
494504
stepperComponent.selectedIndex = 2;
495505
fixture.detectChanges();
496506

@@ -559,6 +569,8 @@ describe('MatStepper', () => {
559569
testComponent.twoGroup.get('twoCtrl')!.setValue('input');
560570
testComponent.threeGroup.get('threeCtrl')!.setValue('valid');
561571
testComponent.validationTrigger.next();
572+
stepperComponent.selectedIndex = 1;
573+
fixture.detectChanges();
562574
stepperComponent.selectedIndex = 2;
563575
fixture.detectChanges();
564576
stepperComponent.selectedIndex = 3;
@@ -695,6 +707,43 @@ describe('MatStepper', () => {
695707
assertArrowKeyInteractionInRtl(fixture, stepHeaders);
696708
});
697709
});
710+
711+
describe('valid step in linear stepper', () => {
712+
let fixture: ComponentFixture<LinearStepperWithValidOptionalStep>;
713+
let testComponent: LinearStepperWithValidOptionalStep;
714+
let stepper: MatStepper;
715+
716+
beforeEach(() => {
717+
fixture = TestBed.createComponent(LinearStepperWithValidOptionalStep);
718+
fixture.detectChanges();
719+
720+
testComponent = fixture.componentInstance;
721+
stepper = fixture.debugElement
722+
.query(By.css('mat-horizontal-stepper')).componentInstance;
723+
});
724+
725+
it('must be visited if not optional', () => {
726+
stepper.selectedIndex = 2;
727+
fixture.detectChanges();
728+
expect(stepper.selectedIndex).toBe(0);
729+
730+
stepper.selectedIndex = 1;
731+
fixture.detectChanges();
732+
expect(stepper.selectedIndex).toBe(1);
733+
734+
stepper.selectedIndex = 2;
735+
fixture.detectChanges();
736+
expect(stepper.selectedIndex).toBe(2);
737+
});
738+
739+
it('can be skipped entirely if optional', () => {
740+
testComponent.step2Optional = true;
741+
fixture.detectChanges();
742+
stepper.selectedIndex = 2;
743+
fixture.detectChanges();
744+
expect(stepper.selectedIndex).toBe(2);
745+
});
746+
});
698747
});
699748

700749
/** Asserts that keyboard interaction works correctly. */
@@ -986,3 +1035,17 @@ class SimpleStepperWithStepControlAndCompletedBinding {
9861035
`
9871036
})
9881037
class IconOverridesStepper {}
1038+
1039+
@Component({
1040+
template: `
1041+
<mat-horizontal-stepper linear>
1042+
<mat-step label="Step 1" [stepControl]="controls[0]"></mat-step>
1043+
<mat-step label="Step 2" [stepControl]="controls[1]" [optional]="step2Optional"></mat-step>
1044+
<mat-step label="Step 3" [stepControl]="controls[2]"></mat-step>
1045+
</mat-horizontal-stepper>
1046+
`
1047+
})
1048+
class LinearStepperWithValidOptionalStep {
1049+
controls = [0, 0, 0].map(() => new FormControl());
1050+
step2Optional = false;
1051+
}

0 commit comments

Comments
 (0)