Skip to content

Commit

Permalink
fix(stepper): completed binding not being considered when moving from…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
crisbeto authored and jelbourn committed Jan 4, 2018
1 parent a0bd162 commit 32d0dbb
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 9 deletions.
10 changes: 7 additions & 3 deletions src/cdk/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.

8 changes: 5 additions & 3 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
8 changes: 6 additions & 2 deletions src/lib/stepper/stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 88 additions & 1 deletion src/lib/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ describe('MatHorizontalStepper', () => {
declarations: [
SimpleMatHorizontalStepperApp,
SimplePreselectedMatHorizontalStepperApp,
LinearMatHorizontalStepperApp
LinearMatHorizontalStepperApp,
SimpleStepperWithoutStepControl,
SimpleStepperWithStepControlAndCompletedBinding
],
providers: [
{provide: Directionality, useFactory: () => ({value: dir})}
Expand Down Expand Up @@ -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);
});
});
});

Expand Down Expand Up @@ -988,3 +1038,40 @@ class LinearMatVerticalStepperApp {
class SimplePreselectedMatHorizontalStepperApp {
index = 0;
}

@Component({
template: `
<mat-horizontal-stepper linear>
<mat-step
*ngFor="let step of steps"
[label]="step.label"
[completed]="step.completed"></mat-step>
</mat-horizontal-stepper>
`
})
class SimpleStepperWithoutStepControl {
steps = [
{label: 'One', completed: false},
{label: 'Two', completed: false},
{label: 'Three', completed: false}
];
}

@Component({
template: `
<mat-horizontal-stepper linear>
<mat-step
*ngFor="let step of steps"
[label]="step.label"
[stepControl]="step.control"
[completed]="step.completed"></mat-step>
</mat-horizontal-stepper>
`
})
class SimpleStepperWithStepControlAndCompletedBinding {
steps = [
{label: 'One', completed: false, control: new FormControl()},
{label: 'Two', completed: false, control: new FormControl()},
{label: 'Three', completed: false, control: new FormControl()}
];
}

0 comments on commit 32d0dbb

Please sign in to comment.