Skip to content

Commit 943ecf7

Browse files
committed
fix(stepper): unable to skip step if completed value is overwritten
Currently we allow for the `completed` value of a step to be overwritten, however setting it to `true` still won't allow the user to skip the step, because the `interacted` flag won't be flipped if they haven't been on a particular step before. Fixes #15310.
1 parent d232d7c commit 943ecf7

File tree

3 files changed

+76
-56
lines changed

3 files changed

+76
-56
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 54 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import {FocusableOption, FocusKeyManager} from '@angular/cdk/a11y';
1010
import {Direction, Directionality} from '@angular/cdk/bidi';
1111
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
12-
import {END, ENTER, HOME, SPACE, hasModifierKey} from '@angular/cdk/keycodes';
12+
import {END, ENTER, hasModifierKey, HOME, SPACE} from '@angular/cdk/keycodes';
13+
import {DOCUMENT} from '@angular/common';
1314
import {
1415
AfterViewInit,
1516
ChangeDetectionStrategy,
@@ -18,10 +19,11 @@ import {
1819
ContentChild,
1920
ContentChildren,
2021
Directive,
21-
EventEmitter,
2222
ElementRef,
23+
EventEmitter,
2324
forwardRef,
2425
Inject,
26+
InjectionToken,
2527
Input,
2628
OnChanges,
2729
OnDestroy,
@@ -31,13 +33,12 @@ import {
3133
TemplateRef,
3234
ViewChild,
3335
ViewEncapsulation,
34-
InjectionToken,
3536
} from '@angular/core';
36-
import {DOCUMENT} from '@angular/common';
37-
import {CdkStepLabel} from './step-label';
38-
import {Observable, Subject, of as obaservableOf} from 'rxjs';
37+
import {Observable, of as obaservableOf, Subject} from 'rxjs';
3938
import {startWith, takeUntil} from 'rxjs/operators';
39+
4040
import {CdkStepHeader} from './step-header';
41+
import {CdkStepLabel} from './step-label';
4142

4243
/** Used to generate unique ID for each stepper component. */
4344
let nextId = 0;
@@ -46,10 +47,10 @@ let nextId = 0;
4647
* Position state of the content of each step in stepper that is used for transitioning
4748
* the content into correct position upon step selection change.
4849
*/
49-
export type StepContentPositionState = 'previous' | 'current' | 'next';
50+
export type StepContentPositionState = 'previous'|'current'|'next';
5051

5152
/** Possible orientation of a stepper. */
52-
export type StepperOrientation = 'horizontal' | 'vertical';
53+
export type StepperOrientation = 'horizontal'|'vertical';
5354

5455
/** Change event emitted on selection changes. */
5556
export class StepperSelectionEvent {
@@ -67,7 +68,7 @@ export class StepperSelectionEvent {
6768
}
6869

6970
/** The state of each step. */
70-
export type StepState = 'number' | 'edit' | 'done' | 'error' | string;
71+
export type StepState = 'number'|'edit'|'done'|'error'|string;
7172

7273
/** Enum to represent the different states of the steps. */
7374
export const STEP_STATE = {
@@ -78,8 +79,7 @@ export const STEP_STATE = {
7879
};
7980

8081
/** InjectionToken that can be used to specify the global stepper options. */
81-
export const STEPPER_GLOBAL_OPTIONS =
82-
new InjectionToken<StepperOptions>('STEPPER_GLOBAL_OPTIONS');
82+
export const STEPPER_GLOBAL_OPTIONS = new InjectionToken<StepperOptions>('STEPPER_GLOBAL_OPTIONS');
8383

8484
/**
8585
* InjectionToken that can be used to specify the global stepper options.
@@ -124,12 +124,7 @@ export class CdkStep implements OnChanges {
124124
@ViewChild(TemplateRef) content: TemplateRef<any>;
125125

126126
/** The top level abstract control of the step. */
127-
@Input() stepControl: {
128-
valid: boolean;
129-
invalid: boolean;
130-
pending: boolean;
131-
reset: () => void;
132-
};
127+
@Input() stepControl: {valid: boolean; invalid: boolean; pending: boolean; reset: () => void};
133128

134129
/** Whether user has seen the expanded step content or not. */
135130
interacted = false;
@@ -154,15 +149,19 @@ export class CdkStep implements OnChanges {
154149

155150
/** Whether the user can return to this step once it has been marked as completed. */
156151
@Input()
157-
get editable(): boolean { return this._editable; }
152+
get editable(): boolean {
153+
return this._editable;
154+
}
158155
set editable(value: boolean) {
159156
this._editable = coerceBooleanProperty(value);
160157
}
161158
private _editable = true;
162159

163160
/** Whether the completion of step is optional. */
164161
@Input()
165-
get optional(): boolean { return this._optional; }
162+
get optional(): boolean {
163+
return this._optional;
164+
}
166165
set optional(value: boolean) {
167166
this._optional = coerceBooleanProperty(value);
168167
}
@@ -171,12 +170,12 @@ export class CdkStep implements OnChanges {
171170
/** Whether step is marked as completed. */
172171
@Input()
173172
get completed(): boolean {
174-
return this._customCompleted == null ? this._getDefaultCompleted() : this._customCompleted;
173+
return this._completedOverride == null ? this._getDefaultCompleted() : this._completedOverride;
175174
}
176175
set completed(value: boolean) {
177-
this._customCompleted = coerceBooleanProperty(value);
176+
this._completedOverride = coerceBooleanProperty(value);
178177
}
179-
private _customCompleted: boolean | null = null;
178+
_completedOverride: boolean|null = null;
180179

181180
private _getDefaultCompleted() {
182181
return this.stepControl ? this.stepControl.valid && this.interacted : this.interacted;
@@ -190,16 +189,16 @@ export class CdkStep implements OnChanges {
190189
set hasError(value: boolean) {
191190
this._customError = coerceBooleanProperty(value);
192191
}
193-
private _customError: boolean | null = null;
192+
private _customError: boolean|null = null;
194193

195194
private _getDefaultError() {
196195
return this.stepControl && this.stepControl.invalid && this.interacted;
197196
}
198197

199198
/** @breaking-change 8.0.0 remove the `?` after `stepperOptions` */
200199
constructor(
201-
@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper,
202-
@Optional() @Inject(STEPPER_GLOBAL_OPTIONS) stepperOptions?: StepperOptions) {
200+
@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper,
201+
@Optional() @Inject(STEPPER_GLOBAL_OPTIONS) stepperOptions?: StepperOptions) {
203202
this._stepperOptions = stepperOptions ? stepperOptions : {};
204203
this._displayDefaultIndicatorType = this._stepperOptions.displayDefaultIndicatorType !== false;
205204
this._showError = !!this._stepperOptions.showError;
@@ -214,8 +213,8 @@ export class CdkStep implements OnChanges {
214213
reset(): void {
215214
this.interacted = false;
216215

217-
if (this._customCompleted != null) {
218-
this._customCompleted = false;
216+
if (this._completedOverride != null) {
217+
this._completedOverride = false;
219218
}
220219

221220
if (this._customError != null) {
@@ -249,7 +248,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
249248
* @breaking-change 8.0.0 Remove `| undefined` once the `_document`
250249
* constructor param is required.
251250
*/
252-
private _document: Document | undefined;
251+
private _document: Document|undefined;
253252

254253
/**
255254
* The list of step components that the stepper is holding.
@@ -259,7 +258,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
259258
@ContentChildren(CdkStep) _steps: QueryList<CdkStep>;
260259

261260
/** The list of step components that the stepper is holding. */
262-
get steps(): QueryList<CdkStep> {
261+
get steps(): QueryList<CdkStep> {
263262
return this._steps;
264263
}
265264

@@ -272,13 +271,19 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
272271

273272
/** Whether the validity of previous steps should be checked or not. */
274273
@Input()
275-
get linear(): boolean { return this._linear; }
276-
set linear(value: boolean) { this._linear = coerceBooleanProperty(value); }
274+
get linear(): boolean {
275+
return this._linear;
276+
}
277+
set linear(value: boolean) {
278+
this._linear = coerceBooleanProperty(value);
279+
}
277280
private _linear = false;
278281

279282
/** The index of the selected step. */
280283
@Input()
281-
get selectedIndex() { return this._selectedIndex; }
284+
get selectedIndex() {
285+
return this._selectedIndex;
286+
}
282287
set selectedIndex(index: number) {
283288
const newIndex = coerceNumberProperty(index);
284289

@@ -288,8 +293,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
288293
throw Error('cdkStepper: Cannot assign out-of-bounds value to `selectedIndex`.');
289294
}
290295

291-
if (this._selectedIndex != newIndex &&
292-
!this._anyControlsInvalidOrPending(newIndex) &&
296+
if (this._selectedIndex != newIndex && !this._anyControlsInvalidOrPending(newIndex) &&
293297
(newIndex >= this._selectedIndex || this.steps.toArray()[newIndex].editable)) {
294298
this._updateSelectedItemIndex(index);
295299
}
@@ -310,20 +314,18 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
310314
}
311315

312316
/** Event emitted when the selected step has changed. */
313-
@Output() selectionChange: EventEmitter<StepperSelectionEvent>
314-
= new EventEmitter<StepperSelectionEvent>();
317+
@Output()
318+
selectionChange: EventEmitter<StepperSelectionEvent> = new EventEmitter<StepperSelectionEvent>();
315319

316320
/** Used to track unique ID for each stepper component. */
317321
_groupId: number;
318322

319323
protected _orientation: StepperOrientation = 'horizontal';
320324

321325
constructor(
322-
@Optional() private _dir: Directionality,
323-
private _changeDetectorRef: ChangeDetectorRef,
324-
// @breaking-change 8.0.0 `_elementRef` and `_document` parameters to become required.
325-
private _elementRef?: ElementRef<HTMLElement>,
326-
@Inject(DOCUMENT) _document?: any) {
326+
@Optional() private _dir: Directionality, private _changeDetectorRef: ChangeDetectorRef,
327+
// @breaking-change 8.0.0 `_elementRef` and `_document` parameters to become required.
328+
private _elementRef?: ElementRef<HTMLElement>, @Inject(DOCUMENT) _document?: any) {
327329
this._groupId = nextId++;
328330
this._document = _document;
329331
}
@@ -333,12 +335,12 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
333335
// extend this one might have them as view chidren. We initialize the keyboard handling in
334336
// AfterViewInit so we're guaranteed for both view and content children to be defined.
335337
this._keyManager = new FocusKeyManager<FocusableOption>(this._stepHeader)
336-
.withWrap()
337-
.withVerticalOrientation(this._orientation === 'vertical');
338+
.withWrap()
339+
.withVerticalOrientation(this._orientation === 'vertical');
338340

339-
(this._dir ? this._dir.change as Observable<Direction> : obaservableOf<Direction>())
340-
.pipe(startWith(this._layoutDirection()), takeUntil(this._destroyed))
341-
.subscribe(direction => this._keyManager.withHorizontalOrientation(direction));
341+
(this._dir ? (this._dir.change as Observable<Direction>) : obaservableOf<Direction>())
342+
.pipe(startWith(this._layoutDirection()), takeUntil(this._destroyed))
343+
.subscribe(direction => this._keyManager.withHorizontalOrientation(direction));
342344

343345
this._keyManager.updateActiveItemIndex(this._selectedIndex);
344346

@@ -402,9 +404,8 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
402404
const step = this.steps.toArray()[index];
403405
const isCurrentStep = this._isCurrentStep(index);
404406

405-
return step._displayDefaultIndicatorType
406-
? this._getDefaultIndicatorLogic(step, isCurrentStep)
407-
: this._getGuidelineLogic(step, isCurrentStep, state);
407+
return step._displayDefaultIndicatorType ? this._getDefaultIndicatorLogic(step, isCurrentStep) :
408+
this._getGuidelineLogic(step, isCurrentStep, state);
408409
}
409410

410411
private _getDefaultIndicatorLogic(step: CdkStep, isCurrentStep: boolean): StepState {
@@ -418,9 +419,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
418419
}
419420

420421
private _getGuidelineLogic(
421-
step: CdkStep,
422-
isCurrentStep: boolean,
423-
state: StepState = STEP_STATE.NUMBER): StepState {
422+
step: CdkStep, isCurrentStep: boolean, state: StepState = STEP_STATE.NUMBER): StepState {
424423
if (step._showError && step.hasError && !isCurrentStep) {
425424
return STEP_STATE.ERROR;
426425
} else if (step.completed && !isCurrentStep) {
@@ -491,10 +490,9 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
491490
if (this._linear && index >= 0) {
492491
return steps.slice(0, index).some(step => {
493492
const control = step.stepControl;
494-
const isIncomplete = control ?
495-
(control.invalid || control.pending || !step.interacted) :
496-
!step.completed;
497-
return isIncomplete && !step.optional;
493+
const isIncomplete =
494+
control ? (control.invalid || control.pending || !step.interacted) : !step.completed;
495+
return isIncomplete && !step.optional && !step._completedOverride;
498496
});
499497
}
500498

src/lib/stepper/stepper.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,27 @@ describe('MatStepper', () => {
718718
expect(steps[2].completed).toBe(true,
719719
'Expected third step to be considered complete when doing a run after a reset.');
720720
});
721+
722+
it('should be able to skip past the current step if a custom `completed` value is set', () => {
723+
expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe('');
724+
expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false);
725+
expect(testComponent.oneGroup.valid).toBe(false);
726+
expect(stepperComponent.selectedIndex).toBe(0);
727+
728+
const nextButtonNativeEl = fixture.debugElement
729+
.queryAll(By.directive(MatStepperNext))[0].nativeElement;
730+
nextButtonNativeEl.click();
731+
fixture.detectChanges();
732+
733+
expect(stepperComponent.selectedIndex).toBe(0);
734+
735+
stepperComponent.steps.first.completed = true;
736+
nextButtonNativeEl.click();
737+
fixture.detectChanges();
738+
739+
expect(testComponent.oneGroup.valid).toBe(false);
740+
expect(stepperComponent.selectedIndex).toBe(1);
741+
});
721742
});
722743

723744
describe('linear stepper with a pre-defined selectedIndex', () => {

tools/public_api_guard/cdk/stepper.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export declare class CdkStep implements OnChanges {
2+
_completedOverride: boolean | null;
23
_displayDefaultIndicatorType: boolean;
34
_showError: boolean;
45
ariaLabel: string;

0 commit comments

Comments
 (0)