Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()}
];
}