Skip to content

Commit

Permalink
fix(stepper): parent stepper picking up steps from child stepper (#18458
Browse files Browse the repository at this point in the history
)

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.

(cherry picked from commit 9d309f7)
  • Loading branch information
crisbeto authored and andrewseguin committed Aug 12, 2020
1 parent 67247ac commit 22d645c
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 18 deletions.
32 changes: 19 additions & 13 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
TemplateRef,
ViewChild,
ViewEncapsulation,
AfterContentInit,
} from '@angular/core';
import {Observable, of as observableOf, Subject} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';
Expand Down Expand Up @@ -201,7 +202,7 @@ export class CdkStep implements OnChanges {

/** @breaking-change 8.0.0 remove the `?` after `stepperOptions` */
constructor(
@Inject(forwardRef(() => CdkStepper)) private _stepper: CdkStepper,
@Inject(forwardRef(() => CdkStepper)) public _stepper: CdkStepper,
@Optional() @Inject(STEPPER_GLOBAL_OPTIONS) stepperOptions?: StepperOptions) {
this._stepperOptions = stepperOptions ? stepperOptions : {};
this._displayDefaultIndicatorType = this._stepperOptions.displayDefaultIndicatorType !== false;
Expand Down Expand Up @@ -246,7 +247,7 @@ export class CdkStep implements OnChanges {
selector: '[cdkStepper]',
exportAs: 'cdkStepper',
})
export class CdkStepper implements AfterViewInit, OnDestroy {
export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
/** Emits when the component is destroyed. */
protected _destroyed = new Subject<void>();

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

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

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

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

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

ngAfterContentInit() {
this._steps.changes
.pipe(startWith(this._steps), takeUntil(this._destroyed))
.subscribe((steps: QueryList<CdkStep>) => {
this.steps.reset(steps.filter(step => step._stepper === this));
this.steps.notifyOnChanges();
});
}

ngAfterViewInit() {
// Note that while the step headers are content children by default, any components that
// extend this one might have them as view children. We initialize the keyboard handling in
Expand All @@ -353,14 +357,16 @@ export class CdkStepper implements AfterViewInit, OnDestroy {

this._keyManager.updateActiveItem(this._selectedIndex);

this.steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => {
// No need to `takeUntil` here, because we're the ones destroying `steps`.
this.steps.changes.subscribe(() => {
if (!this.selected) {
this._selectedIndex = Math.max(this._selectedIndex - 1, 0);
}
});
}

ngOnDestroy() {
this.steps.destroy();
this._destroyed.next();
this._destroyed.complete();
}
Expand Down
39 changes: 38 additions & 1 deletion src/material/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ import {
createKeyboardEvent,
dispatchEvent,
} from '@angular/cdk/testing/private';
import {Component, DebugElement, EventEmitter, OnInit, Type, Provider} from '@angular/core';
import {
Component,
DebugElement,
EventEmitter,
OnInit,
Type,
Provider,
ViewChildren,
QueryList,
} from '@angular/core';
import {ComponentFixture, fakeAsync, flush, inject, TestBed} from '@angular/core/testing';
import {
AbstractControl,
Expand Down Expand Up @@ -1178,6 +1187,15 @@ describe('MatStepper', () => {

expect(fixture.nativeElement.querySelectorAll('.mat-step-header').length).toBe(2);
});

it('should not pick up the steps from descendant steppers', () => {
const fixture = createComponent(NestedSteppers);
fixture.detectChanges();
const steppers = fixture.componentInstance.steppers.toArray();

expect(steppers[0].steps.length).toBe(3);
expect(steppers[1].steps.length).toBe(2);
});
});

/** Asserts that keyboard interaction works correctly. */
Expand Down Expand Up @@ -1665,3 +1683,22 @@ class StepperWithIndirectDescendantSteps {
class StepperWithNgIf {
showStep2 = false;
}


@Component({
template: `
<mat-vertical-stepper>
<mat-step label="Step 1">Content 1</mat-step>
<mat-step label="Step 2">Content 2</mat-step>
<mat-step label="Step 3">
<mat-horizontal-stepper>
<mat-step label="Sub-Step 1">Sub-Content 1</mat-step>
<mat-step label="Sub-Step 2">Sub-Content 2</mat-step>
</mat-horizontal-stepper>
</mat-step>
</mat-vertical-stepper>
`
})
class NestedSteppers {
@ViewChildren(MatStepper) steppers: QueryList<MatStepper>;
}
8 changes: 6 additions & 2 deletions src/material/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,12 @@ export class MatStepper extends CdkStepper implements AfterContentInit {
/** The list of step headers of the steps in the stepper. */
@ViewChildren(MatStepHeader) _stepHeader: QueryList<MatStepHeader>;

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

/** Steps that belong to the current stepper, excluding ones from nested steppers. */
readonly steps: QueryList<MatStep> = new QueryList<MatStep>();

/** Custom icon overrides passed in by the consumer. */
@ContentChildren(MatStepperIcon, {descendants: true}) _icons: QueryList<MatStepperIcon>;

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

ngAfterContentInit() {
super.ngAfterContentInit();
this._icons.forEach(({name, templateRef}) => this._iconOverrides[name] = templateRef);

// Mark the component for change detection whenever the content children query changes
this._steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => {
this.steps.changes.pipe(takeUntil(this._destroyed)).subscribe(() => {
this._stateChanged();
});

Expand Down
6 changes: 4 additions & 2 deletions tools/public_api_guard/cdk/stepper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export declare class CdkStep implements OnChanges {
_completedOverride: boolean | null;
_displayDefaultIndicatorType: boolean;
_showError: boolean;
_stepper: CdkStepper;
ariaLabel: string;
ariaLabelledby: string;
get completed(): boolean;
Expand Down Expand Up @@ -46,7 +47,7 @@ export declare class CdkStepLabel {
static ɵfac: i0.ɵɵFactoryDef<CdkStepLabel, never>;
}

export declare class CdkStepper implements AfterViewInit, OnDestroy {
export declare class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
protected _destroyed: Subject<void>;
_groupId: number;
protected _orientation: StepperOrientation;
Expand All @@ -59,7 +60,7 @@ export declare class CdkStepper implements AfterViewInit, OnDestroy {
get selectedIndex(): number;
set selectedIndex(index: number);
selectionChange: EventEmitter<StepperSelectionEvent>;
get steps(): QueryList<CdkStep>;
readonly steps: QueryList<CdkStep>;
constructor(_dir: Directionality, _changeDetectorRef: ChangeDetectorRef, _elementRef?: ElementRef<HTMLElement> | undefined, _document?: any);
_getAnimationDirection(index: number): StepContentPositionState;
_getFocusIndex(): number | null;
Expand All @@ -69,6 +70,7 @@ export declare class CdkStepper implements AfterViewInit, OnDestroy {
_onKeydown(event: KeyboardEvent): void;
_stateChanged(): void;
next(): void;
ngAfterContentInit(): void;
ngAfterViewInit(): void;
ngOnDestroy(): void;
previous(): void;
Expand Down
1 change: 1 addition & 0 deletions tools/public_api_guard/material/stepper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export declare class MatStepper extends CdkStepper implements AfterContentInit {
_steps: QueryList<MatStep>;
readonly animationDone: EventEmitter<void>;
disableRipple: boolean;
readonly steps: QueryList<MatStep>;
ngAfterContentInit(): void;
static ngAcceptInputType_completed: BooleanInput;
static ngAcceptInputType_editable: BooleanInput;
Expand Down

0 comments on commit 22d645c

Please sign in to comment.