From 4fdca7c16b3973895386c8c7f801a46398bb409b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 4 Jan 2018 21:49:52 +0200 Subject: [PATCH] fix(stepper): completed binding not being considered when moving from a step without a stepControl (#9126) Currently we only consider a step's validation state when determining whether the user can move forward in a linear stepper, however this means that there's no way to block navigation without using Angular forms. These changes switch the logic so it considers the `completed` binding, if there is `stepControl`. Fixes #8110. --- src/cdk/stepper/stepper.md | 10 ++-- src/cdk/stepper/stepper.ts | 8 +-- src/lib/stepper/stepper.md | 8 ++- src/lib/stepper/stepper.spec.ts | 89 ++++++++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 9 deletions(-) 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()} + ]; +}