Skip to content

Commit 0fdeab8

Browse files
crisbetojelbourn
authored andcommitted
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.
1 parent b5a768f commit 0fdeab8

File tree

4 files changed

+106
-9
lines changed

4 files changed

+106
-9
lines changed

src/cdk/stepper/stepper.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ keyboard interactions and exposing an API for advancing or rewinding through the
77

88
#### Linear stepper
99
A stepper marked as `linear` requires the user to complete previous steps before proceeding.
10-
For each step, the `stepControl` attribute can be set to the top level
11-
`AbstractControl` that is used to check the validity of the step.
10+
For each step, the `stepControl` attribute can be set to the top level `AbstractControl` that
11+
is used to check the validity of the step.
1212

1313
There are two possible approaches. One is using a single form for stepper, and the other is
1414
using a different form for each step.
1515

16+
Alternatively, if you don't want to use the Angular forms, you can pass in the `completed` property
17+
to each of the steps which won't allow the user to continue until it becomes `true`. Note that if
18+
both `completed` and `stepControl` are set, the `stepControl` will take precedence.
19+
1620
#### Using a single form for the entire stepper
1721
When using a single form for the stepper, any intermediate next/previous buttons within the steps
1822
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
5660
step content is automatically set based on step selection change.
5761

5862
The stepper and each step should be given a meaningful label via `aria-label` or `aria-labelledby`.
59-
63+

src/cdk/stepper/stepper.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,12 @@ export class CdkStepper implements OnDestroy {
297297
steps[this._selectedIndex].interacted = true;
298298

299299
if (this._linear && index >= 0) {
300-
return steps.slice(0, index).some(step =>
301-
step.stepControl && (step.stepControl.invalid || step.stepControl.pending)
302-
);
300+
return steps.slice(0, index).some(step => {
301+
const control = step.stepControl;
302+
return control ? (control.invalid || control.pending) : !step.completed;
303+
});
303304
}
305+
304306
return false;
305307
}
306308

src/lib/stepper/stepper.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ There are two button directives to support navigation between different steps:
5454

5555
### Linear stepper
5656
The `linear` attribute can be set on `mat-horizontal-stepper` and `mat-vertical-stepper` to create
57-
a linear stepper that requires the user to complete previous steps before proceeding
58-
to following steps. For each `mat-step`, the `stepControl` attribute can be set to the top level
57+
a linear stepper that requires the user to complete previous steps before proceeding to following
58+
steps. For each `mat-step`, the `stepControl` attribute can be set to the top level
5959
`AbstractControl` that is used to check the validity of the step.
6060

6161
There are two possible approaches. One is using a single form for stepper, and the other is
6262
using a different form for each step.
6363

64+
Alternatively, if you don't want to use the Angular forms, you can pass in the `completed` property
65+
to each of the steps which won't allow the user to continue until it becomes `true`. Note that if
66+
both `completed` and `stepControl` are set, the `stepControl` will take precedence.
67+
6468
#### Using a single form
6569
When using a single form for the stepper, `matStepperPrevious` and `matStepperNext` have to be
6670
set to `type="button"` in order to prevent submission of the form before all steps

src/lib/stepper/stepper.spec.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ describe('MatHorizontalStepper', () => {
2727
declarations: [
2828
SimpleMatHorizontalStepperApp,
2929
SimplePreselectedMatHorizontalStepperApp,
30-
LinearMatHorizontalStepperApp
30+
LinearMatHorizontalStepperApp,
31+
SimpleStepperWithoutStepControl,
32+
SimpleStepperWithStepControlAndCompletedBinding
3133
],
3234
providers: [
3335
{provide: Directionality, useFactory: () => ({value: dir})}
@@ -199,6 +201,54 @@ describe('MatHorizontalStepper', () => {
199201
let stepHeaders = debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
200202
assertSelectionChangeOnHeaderClick(preselectedFixture, stepHeaders);
201203
});
204+
205+
it('should not move to the next step if the current one is not completed ' +
206+
'and there is no `stepControl`', () => {
207+
fixture.destroy();
208+
209+
const noStepControlFixture = TestBed.createComponent(SimpleStepperWithoutStepControl);
210+
211+
noStepControlFixture.detectChanges();
212+
213+
const stepper: MatHorizontalStepper = noStepControlFixture.debugElement
214+
.query(By.directive(MatHorizontalStepper)).componentInstance;
215+
216+
const headers = noStepControlFixture.debugElement
217+
.queryAll(By.css('.mat-horizontal-stepper-header'));
218+
219+
expect(stepper.selectedIndex).toBe(0);
220+
221+
headers[1].nativeElement.click();
222+
noStepControlFixture.detectChanges();
223+
224+
expect(stepper.selectedIndex).toBe(0);
225+
});
226+
227+
it('should have the `stepControl` take precedence when both `completed` and ' +
228+
'`stepControl` are set', () => {
229+
fixture.destroy();
230+
231+
const controlAndBindingFixture =
232+
TestBed.createComponent(SimpleStepperWithStepControlAndCompletedBinding);
233+
234+
controlAndBindingFixture.detectChanges();
235+
236+
expect(controlAndBindingFixture.componentInstance.steps[0].control.valid).toBe(true);
237+
expect(controlAndBindingFixture.componentInstance.steps[0].completed).toBe(false);
238+
239+
const stepper: MatHorizontalStepper = controlAndBindingFixture.debugElement
240+
.query(By.directive(MatHorizontalStepper)).componentInstance;
241+
242+
const headers = controlAndBindingFixture.debugElement
243+
.queryAll(By.css('.mat-horizontal-stepper-header'));
244+
245+
expect(stepper.selectedIndex).toBe(0);
246+
247+
headers[1].nativeElement.click();
248+
controlAndBindingFixture.detectChanges();
249+
250+
expect(stepper.selectedIndex).toBe(1);
251+
});
202252
});
203253
});
204254

@@ -988,3 +1038,40 @@ class LinearMatVerticalStepperApp {
9881038
class SimplePreselectedMatHorizontalStepperApp {
9891039
index = 0;
9901040
}
1041+
1042+
@Component({
1043+
template: `
1044+
<mat-horizontal-stepper linear>
1045+
<mat-step
1046+
*ngFor="let step of steps"
1047+
[label]="step.label"
1048+
[completed]="step.completed"></mat-step>
1049+
</mat-horizontal-stepper>
1050+
`
1051+
})
1052+
class SimpleStepperWithoutStepControl {
1053+
steps = [
1054+
{label: 'One', completed: false},
1055+
{label: 'Two', completed: false},
1056+
{label: 'Three', completed: false}
1057+
];
1058+
}
1059+
1060+
@Component({
1061+
template: `
1062+
<mat-horizontal-stepper linear>
1063+
<mat-step
1064+
*ngFor="let step of steps"
1065+
[label]="step.label"
1066+
[stepControl]="step.control"
1067+
[completed]="step.completed"></mat-step>
1068+
</mat-horizontal-stepper>
1069+
`
1070+
})
1071+
class SimpleStepperWithStepControlAndCompletedBinding {
1072+
steps = [
1073+
{label: 'One', completed: false, control: new FormControl()},
1074+
{label: 'Two', completed: false, control: new FormControl()},
1075+
{label: 'Three', completed: false, control: new FormControl()}
1076+
];
1077+
}

0 commit comments

Comments
 (0)