Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(stepper): parent stepper picking up steps from child stepper #18458

Merged
merged 1 commit into from
Aug 12, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -1187,6 +1196,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 @@ -1674,3 +1692,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>;
}

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 @@ -64,6 +64,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