diff --git a/src/cdk/stepper/stepper.md b/src/cdk/stepper/stepper.md
index 6e5fe2f7f511..6d480d154e6f 100644
--- a/src/cdk/stepper/stepper.md
+++ b/src/cdk/stepper/stepper.md
@@ -7,12 +7,16 @@ keyboard interactions and exposing an API for advancing or rewinding through the
#### Linear stepper
A stepper marked as `linear` requires the user to complete previous steps before proceeding.
-For each step, the `stepControl` attribute can be set to the top level
-`AbstractControl` that is used to check the validity of the step.
+For each step, the `stepControl` attribute can be set to the top level `AbstractControl` that
+is used to check the validity of the step.
There are two possible approaches. One is using a single form for stepper, and the other is
using a different form for each step.
+Alternatively, if you don't want to use the Angular forms, you can pass in the `completed` property
+to each of the steps which won't allow the user to continue until it becomes `true`. Note that if
+both `completed` and `stepControl` are set, the `stepControl` will take precedence.
+
#### Using a single form for the entire stepper
When using a single form for the stepper, any intermediate next/previous buttons within the steps
must be set to `type="button"` in order to prevent submission of the form before all steps are
@@ -56,4 +60,4 @@ is given `role="tab"`, and the content that can be expanded upon selection is gi
step content is automatically set based on step selection change.
The stepper and each step should be given a meaningful label via `aria-label` or `aria-labelledby`.
-
+
diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts
index 4f79605634bc..14d9f3c5f486 100644
--- a/src/cdk/stepper/stepper.ts
+++ b/src/cdk/stepper/stepper.ts
@@ -297,10 +297,12 @@ export class CdkStepper implements OnDestroy {
steps[this._selectedIndex].interacted = true;
if (this._linear && index >= 0) {
- return steps.slice(0, index).some(step =>
- step.stepControl && (step.stepControl.invalid || step.stepControl.pending)
- );
+ return steps.slice(0, index).some(step => {
+ const control = step.stepControl;
+ return control ? (control.invalid || control.pending) : !step.completed;
+ });
}
+
return false;
}
diff --git a/src/lib/stepper/stepper.md b/src/lib/stepper/stepper.md
index 808a25b769c8..6a5c0fdafa02 100644
--- a/src/lib/stepper/stepper.md
+++ b/src/lib/stepper/stepper.md
@@ -54,13 +54,17 @@ There are two button directives to support navigation between different steps:
### Linear stepper
The `linear` attribute can be set on `mat-horizontal-stepper` and `mat-vertical-stepper` to create
-a linear stepper that requires the user to complete previous steps before proceeding
-to following steps. For each `mat-step`, the `stepControl` attribute can be set to the top level
+a linear stepper that requires the user to complete previous steps before proceeding to following
+steps. For each `mat-step`, the `stepControl` attribute can be set to the top level
`AbstractControl` that is used to check the validity of the step.
There are two possible approaches. One is using a single form for stepper, and the other is
using a different form for each step.
+Alternatively, if you don't want to use the Angular forms, you can pass in the `completed` property
+to each of the steps which won't allow the user to continue until it becomes `true`. Note that if
+both `completed` and `stepControl` are set, the `stepControl` will take precedence.
+
#### Using a single form
When using a single form for the stepper, `matStepperPrevious` and `matStepperNext` have to be
set to `type="button"` in order to prevent submission of the form before all steps
diff --git a/src/lib/stepper/stepper.spec.ts b/src/lib/stepper/stepper.spec.ts
index 4700d9b9cde5..93dc3bffcfa6 100644
--- a/src/lib/stepper/stepper.spec.ts
+++ b/src/lib/stepper/stepper.spec.ts
@@ -27,7 +27,9 @@ describe('MatHorizontalStepper', () => {
declarations: [
SimpleMatHorizontalStepperApp,
SimplePreselectedMatHorizontalStepperApp,
- LinearMatHorizontalStepperApp
+ LinearMatHorizontalStepperApp,
+ SimpleStepperWithoutStepControl,
+ SimpleStepperWithStepControlAndCompletedBinding
],
providers: [
{provide: Directionality, useFactory: () => ({value: dir})}
@@ -199,6 +201,54 @@ describe('MatHorizontalStepper', () => {
let stepHeaders = debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
assertSelectionChangeOnHeaderClick(preselectedFixture, stepHeaders);
});
+
+ it('should not move to the next step if the current one is not completed ' +
+ 'and there is no `stepControl`', () => {
+ fixture.destroy();
+
+ const noStepControlFixture = TestBed.createComponent(SimpleStepperWithoutStepControl);
+
+ noStepControlFixture.detectChanges();
+
+ const stepper: MatHorizontalStepper = noStepControlFixture.debugElement
+ .query(By.directive(MatHorizontalStepper)).componentInstance;
+
+ const headers = noStepControlFixture.debugElement
+ .queryAll(By.css('.mat-horizontal-stepper-header'));
+
+ expect(stepper.selectedIndex).toBe(0);
+
+ headers[1].nativeElement.click();
+ noStepControlFixture.detectChanges();
+
+ expect(stepper.selectedIndex).toBe(0);
+ });
+
+ it('should have the `stepControl` take precedence when both `completed` and ' +
+ '`stepControl` are set', () => {
+ fixture.destroy();
+
+ const controlAndBindingFixture =
+ TestBed.createComponent(SimpleStepperWithStepControlAndCompletedBinding);
+
+ controlAndBindingFixture.detectChanges();
+
+ expect(controlAndBindingFixture.componentInstance.steps[0].control.valid).toBe(true);
+ expect(controlAndBindingFixture.componentInstance.steps[0].completed).toBe(false);
+
+ const stepper: MatHorizontalStepper = controlAndBindingFixture.debugElement
+ .query(By.directive(MatHorizontalStepper)).componentInstance;
+
+ const headers = controlAndBindingFixture.debugElement
+ .queryAll(By.css('.mat-horizontal-stepper-header'));
+
+ expect(stepper.selectedIndex).toBe(0);
+
+ headers[1].nativeElement.click();
+ controlAndBindingFixture.detectChanges();
+
+ expect(stepper.selectedIndex).toBe(1);
+ });
});
});
@@ -988,3 +1038,40 @@ class LinearMatVerticalStepperApp {
class SimplePreselectedMatHorizontalStepperApp {
index = 0;
}
+
+@Component({
+ template: `
+
+
+
+ `
+})
+class SimpleStepperWithoutStepControl {
+ steps = [
+ {label: 'One', completed: false},
+ {label: 'Two', completed: false},
+ {label: 'Three', completed: false}
+ ];
+}
+
+@Component({
+ template: `
+
+
+
+ `
+})
+class SimpleStepperWithStepControlAndCompletedBinding {
+ steps = [
+ {label: 'One', completed: false, control: new FormControl()},
+ {label: 'Two', completed: false, control: new FormControl()},
+ {label: 'Three', completed: false, control: new FormControl()}
+ ];
+}