Skip to content

Commit 9d309f7

Browse files
authored
fix(stepper): parent stepper picking up steps from child stepper (#18458)
When we initially made some changes to handle Ivy, we made an assumption that people wouldn't nest steppers so we took one shortcut. It looks like that assumption wasn't correct so these changes make it possible to properly nest steppers again. Fixes #18448.
1 parent 984ca88 commit 9d309f7

File tree

5 files changed

+68
-18
lines changed

5 files changed

+68
-18
lines changed

src/cdk/stepper/stepper.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
TemplateRef,
3939
ViewChild,
4040
ViewEncapsulation,
41+
AfterContentInit,
4142
} from '@angular/core';
4243
import {Observable, of as observableOf, Subject} from 'rxjs';
4344
import {startWith, takeUntil} from 'rxjs/operators';
@@ -201,7 +202,7 @@ export class CdkStep implements OnChanges {
201202

202203
/** @breaking-change 8.0.0 remove the `?` after `stepperOptions` */
203204
constructor(
204-
@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper,
205+
@Inject(forwardRef(() => CdkStepper)) public _stepper: CdkStepper,
205206
@Optional() @Inject(STEPPER_GLOBAL_OPTIONS) stepperOptions?: StepperOptions) {
206207
this._stepperOptions = stepperOptions ? stepperOptions : {};
207208
this._displayDefaultIndicatorType = this._stepperOptions.displayDefaultIndicatorType !== false;
@@ -246,7 +247,7 @@ export class CdkStep implements OnChanges {
246247
selector: '[cdkStepper]',
247248
exportAs: 'cdkStepper',
248249
})
249-
export class CdkStepper implements AfterViewInit, OnDestroy {
250+
export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
250251
/** Emits when the component is destroyed. */
251252
protected _destroyed = new Subject<void>();
252253

@@ -259,17 +260,11 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
259260
*/
260261
private _document: Document|undefined;
261262

262-
/**
263-
* The list of step components that the stepper is holding.
264-
* @deprecated use `steps` instead
265-
* @breaking-change 9.0.0 remove this property
266-
*/
263+
/** Full list of steps inside the stepper, including inside nested steppers. */
267264
@ContentChildren(CdkStep, {descendants: true}) _steps: QueryList<CdkStep>;
268265

269-
/** The list of step components that the stepper is holding. */
270-
get steps(): QueryList<CdkStep> {
271-
return this._steps;
272-
}
266+
/** Steps that belong to the current stepper, excluding ones from nested steppers. */
267+
readonly steps: QueryList<CdkStep> = new QueryList<CdkStep>();
273268

274269
/**
275270
* The list of step headers of the steps in the stepper.
@@ -296,7 +291,7 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
296291
set selectedIndex(index: number) {
297292
const newIndex = coerceNumberProperty(index);
298293

299-
if (this.steps) {
294+
if (this.steps && this._steps) {
300295
// Ensure that the index can't be out of bounds.
301296
if (newIndex < 0 || newIndex > this.steps.length - 1) {
302297
throw Error('cdkStepper: Cannot assign out-of-bounds value to `selectedIndex`.');
@@ -339,6 +334,15 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
339334
this._document = _document;
340335
}
341336

337+
ngAfterContentInit() {
338+
this._steps.changes
339+
.pipe(startWith(this._steps), takeUntil(this._destroyed))
340+
.subscribe((steps: QueryList<CdkStep>) => {
341+
this.steps.reset(steps.filter(step => step._stepper === this));
342+
this.steps.notifyOnChanges();
343+
});
344+
}
345+
342346
ngAfterViewInit() {
343347
// Note that while the step headers are content children by default, any components that
344348
// extend this one might have them as view children. We initialize the keyboard handling in
@@ -353,14 +357,16 @@ export class CdkStepper implements AfterViewInit, OnDestroy {
353357

354358
this._keyManager.updateActiveItem(this._selectedIndex);
355359

356-
this.steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => {
360+
// No need to `takeUntil` here, because we're the ones destroying `steps`.
361+
this.steps.changes.subscribe(() => {
357362
if (!this.selected) {
358363
this._selectedIndex = Math.max(this._selectedIndex - 1, 0);
359364
}
360365
});
361366
}
362367

363368
ngOnDestroy() {
369+
this.steps.destroy();
364370
this._destroyed.next();
365371
this._destroyed.complete();
366372
}

src/material/stepper/stepper.spec.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@ import {
2020
createKeyboardEvent,
2121
dispatchEvent,
2222
} from '@angular/cdk/testing/private';
23-
import {Component, DebugElement, EventEmitter, OnInit, Type, Provider} from '@angular/core';
23+
import {
24+
Component,
25+
DebugElement,
26+
EventEmitter,
27+
OnInit,
28+
Type,
29+
Provider,
30+
ViewChildren,
31+
QueryList,
32+
} from '@angular/core';
2433
import {ComponentFixture, fakeAsync, flush, inject, TestBed} from '@angular/core/testing';
2534
import {
2635
AbstractControl,
@@ -1178,6 +1187,15 @@ describe('MatStepper', () => {
11781187

11791188
expect(fixture.nativeElement.querySelectorAll('.mat-step-header').length).toBe(2);
11801189
});
1190+
1191+
it('should not pick up the steps from descendant steppers', () => {
1192+
const fixture = createComponent(NestedSteppers);
1193+
fixture.detectChanges();
1194+
const steppers = fixture.componentInstance.steppers.toArray();
1195+
1196+
expect(steppers[0].steps.length).toBe(3);
1197+
expect(steppers[1].steps.length).toBe(2);
1198+
});
11811199
});
11821200

11831201
/** Asserts that keyboard interaction works correctly. */
@@ -1665,3 +1683,22 @@ class StepperWithIndirectDescendantSteps {
16651683
class StepperWithNgIf {
16661684
showStep2 = false;
16671685
}
1686+
1687+
1688+
@Component({
1689+
template: `
1690+
<mat-vertical-stepper>
1691+
<mat-step label="Step 1">Content 1</mat-step>
1692+
<mat-step label="Step 2">Content 2</mat-step>
1693+
<mat-step label="Step 3">
1694+
<mat-horizontal-stepper>
1695+
<mat-step label="Sub-Step 1">Sub-Content 1</mat-step>
1696+
<mat-step label="Sub-Step 2">Sub-Content 2</mat-step>
1697+
</mat-horizontal-stepper>
1698+
</mat-step>
1699+
</mat-vertical-stepper>
1700+
`
1701+
})
1702+
class NestedSteppers {
1703+
@ViewChildren(MatStepper) steppers: QueryList<MatStepper>;
1704+
}

src/material/stepper/stepper.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,12 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
8989
/** The list of step headers of the steps in the stepper. */
9090
@ViewChildren(MatStepHeader) _stepHeader: QueryList<MatStepHeader>;
9191

92-
/** Steps that the stepper holds. */
92+
/** Full list of steps inside the stepper, including inside nested steppers. */
9393
@ContentChildren(MatStep, {descendants: true}) _steps: QueryList<MatStep>;
9494

95+
/** Steps that belong to the current stepper, excluding ones from nested steppers. */
96+
readonly steps: QueryList<MatStep> = new QueryList<MatStep>();
97+
9598
/** Custom icon overrides passed in by the consumer. */
9699
@ContentChildren(MatStepperIcon, {descendants: true}) _icons: QueryList<MatStepperIcon>;
97100

@@ -108,10 +111,11 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
108111
_animationDone = new Subject<AnimationEvent>();
109112

110113
ngAfterContentInit() {
114+
super.ngAfterContentInit();
111115
this._icons.forEach(({name, templateRef}) => this._iconOverrides[name] = templateRef);
112116

113117
// Mark the component for change detection whenever the content children query changes
114-
this._steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => {
118+
this.steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => {
115119
this._stateChanged();
116120
});
117121

tools/public_api_guard/cdk/stepper.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export declare class CdkStep implements OnChanges {
22
_completedOverride: boolean | null;
33
_displayDefaultIndicatorType: boolean;
44
_showError: boolean;
5+
_stepper: CdkStepper;
56
ariaLabel: string;
67
ariaLabelledby: string;
78
get completed(): boolean;
@@ -46,7 +47,7 @@ export declare class CdkStepLabel {
4647
static ɵfac: i0.ɵɵFactoryDef<CdkStepLabel, never>;
4748
}
4849

49-
export declare class CdkStepper implements AfterViewInit, OnDestroy {
50+
export declare class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
5051
protected _destroyed: Subject<void>;
5152
_groupId: number;
5253
protected _orientation: StepperOrientation;
@@ -59,7 +60,7 @@ export declare class CdkStepper implements AfterViewInit, OnDestroy {
5960
get selectedIndex(): number;
6061
set selectedIndex(index: number);
6162
selectionChange: EventEmitter<StepperSelectionEvent>;
62-
get steps(): QueryList<CdkStep>;
63+
readonly steps: QueryList<CdkStep>;
6364
constructor(_dir: Directionality, _changeDetectorRef: ChangeDetectorRef, _elementRef?: ElementRef<HTMLElement> | undefined, _document?: any);
6465
_getAnimationDirection(index: number): StepContentPositionState;
6566
_getFocusIndex(): number | null;
@@ -69,6 +70,7 @@ export declare class CdkStepper implements AfterViewInit, OnDestroy {
6970
_onKeydown(event: KeyboardEvent): void;
7071
_stateChanged(): void;
7172
next(): void;
73+
ngAfterContentInit(): void;
7274
ngAfterViewInit(): void;
7375
ngOnDestroy(): void;
7476
previous(): void;

tools/public_api_guard/material/stepper.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export declare class MatStepper extends CdkStepper implements AfterContentInit {
6565
_steps: QueryList<MatStep>;
6666
readonly animationDone: EventEmitter<void>;
6767
disableRipple: boolean;
68+
readonly steps: QueryList<MatStep>;
6869
ngAfterContentInit(): void;
6970
static ngAcceptInputType_completed: BooleanInput;
7071
static ngAcceptInputType_editable: BooleanInput;

0 commit comments

Comments
 (0)