Skip to content

Commit

Permalink
feat(cdk/stepper): emit event when the user interacts with a step (#2…
Browse files Browse the repository at this point in the history
…2400)

Adds an `interacted` event that will emit when the user tries to move away from a step.

Fixes #19918.
  • Loading branch information
crisbeto committed Apr 13, 2021
1 parent 6408731 commit 9b4c503
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 10 deletions.
21 changes: 13 additions & 8 deletions src/cdk/stepper/stepper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,13 @@ export class CdkStep implements OnChanges {
/** The top level abstract control of the step. */
@Input() stepControl: AbstractControlLike;

/** Whether user has seen the expanded step content or not. */
/** Whether user has attempted to move away from the step. */
interacted = false;

/** Emits when the user has attempted to move away from the step. */
@Output('interacted')
readonly interactedStream: EventEmitter<CdkStep> = new EventEmitter<CdkStep>();

/** Plain text label of the step. */
@Input() label: string;

Expand Down Expand Up @@ -229,6 +233,13 @@ export class CdkStep implements OnChanges {
this._stepper._stateChanged();
}

_markAsInteracted() {
if (!this.interacted) {
this.interacted = true;
this.interactedStream.emit(this);
}
}

static ngAcceptInputType_editable: BooleanInput;
static ngAcceptInputType_hasError: BooleanInput;
static ngAcceptInputType_optional: BooleanInput;
Expand Down Expand Up @@ -281,13 +292,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
throw Error('cdkStepper: Cannot assign out-of-bounds value to `selectedIndex`.');
}

const selectedStep = this.selected;

if (selectedStep) {
// TODO: this should really be called something like `visited` instead. Just because
// the user has seen the step doesn't guarantee that they've interacted with it.
selectedStep.interacted = true;
}
this.selected?._markAsInteracted();

if (this._selectedIndex !== newIndex && !this._anyControlsInvalidOrPending(newIndex) &&
(newIndex >= this._selectedIndex || this.steps.toArray()[newIndex].editable)) {
Expand Down
28 changes: 27 additions & 1 deletion src/material/stepper/stepper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
import {MatRipple, ThemePalette} from '@angular/material/core';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Observable, Subject} from 'rxjs';
import {merge, Observable, Subject} from 'rxjs';
import {map, take} from 'rxjs/operators';
import {MatStepHeader, MatStepperModule} from './index';
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
Expand Down Expand Up @@ -1059,6 +1059,32 @@ describe('MatStepper', () => {
fixture.detectChanges();
expect(stepper.steps.map(step => step.interacted)).toEqual([true, true, true]);
});

it('should emit when the user has interacted with a step', () => {
const fixture = createComponent(SimpleMatHorizontalStepperApp);
fixture.detectChanges();

const stepper: MatStepper =
fixture.debugElement.query(By.directive(MatStepper)).componentInstance;
const interactedSteps: number[] = [];
const subscription = merge(...stepper.steps.map(step => step.interactedStream))
.subscribe(step => interactedSteps.push(stepper.steps.toArray().indexOf(step as MatStep)));

expect(interactedSteps).toEqual([]);

stepper.next();
fixture.detectChanges();
expect(interactedSteps).toEqual([0]);

stepper.next();
fixture.detectChanges();
expect(interactedSteps).toEqual([0, 1]);

stepper.next();
fixture.detectChanges();
expect(interactedSteps).toEqual([0, 1, 2]);
subscription.unsubscribe();
});
});

describe('linear stepper with valid step', () => {
Expand Down
4 changes: 3 additions & 1 deletion tools/public_api_guard/cdk/stepper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@ export declare class CdkStep implements OnChanges {
get hasError(): boolean;
set hasError(value: boolean);
interacted: boolean;
readonly interactedStream: EventEmitter<CdkStep>;
label: string;
get optional(): boolean;
set optional(value: boolean);
state: StepState;
stepControl: AbstractControlLike;
stepLabel: CdkStepLabel;
constructor(_stepper: CdkStepper, stepperOptions?: StepperOptions);
_markAsInteracted(): void;
ngOnChanges(): void;
reset(): void;
select(): void;
static ngAcceptInputType_completed: BooleanInput;
static ngAcceptInputType_editable: BooleanInput;
static ngAcceptInputType_hasError: BooleanInput;
static ngAcceptInputType_optional: BooleanInput;
static ɵcmp: i0.ɵɵComponentDeclaration<CdkStep, "cdk-step", ["cdkStep"], { "stepControl": "stepControl"; "label": "label"; "errorMessage": "errorMessage"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; "state": "state"; "editable": "editable"; "optional": "optional"; "completed": "completed"; "hasError": "hasError"; }, {}, ["stepLabel"], ["*"]>;
static ɵcmp: i0.ɵɵComponentDeclaration<CdkStep, "cdk-step", ["cdkStep"], { "stepControl": "stepControl"; "label": "label"; "errorMessage": "errorMessage"; "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; "state": "state"; "editable": "editable"; "optional": "optional"; "completed": "completed"; "hasError": "hasError"; }, { "interactedStream": "interacted"; }, ["stepLabel"], ["*"]>;
static ɵfac: i0.ɵɵFactoryDeclaration<CdkStep, [null, { optional: true; }]>;
}

Expand Down

0 comments on commit 9b4c503

Please sign in to comment.