diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 8fec6f00156c..4f1232786eb9 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -321,6 +321,9 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { /** Event emitted when the selected step has changed. */ @Output() readonly selectionChange = new EventEmitter(); + /** Output to support two-way binding on `[(selectedIndex)]` */ + @Output() readonly selectedIndexChange: EventEmitter = new EventEmitter(); + /** Used to track unique ID for each stepper component. */ _groupId: number; @@ -526,6 +529,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { : this._keyManager.updateActiveItem(newIndex); this._selectedIndex = newIndex; + this.selectedIndexChange.emit(this._selectedIndex); this._stateChanged(); } diff --git a/src/material/stepper/stepper.spec.ts b/src/material/stepper/stepper.spec.ts index 3f50fc20af6c..ac8fcefb6a69 100644 --- a/src/material/stepper/stepper.spec.ts +++ b/src/material/stepper/stepper.spec.ts @@ -1571,6 +1571,35 @@ describe('MatStepper', () => { expect(element.textContent).toContain('Step 3 content'); }); }); + + describe('stepper with two-way binding on selectedIndex', () => { + it('should update selectedIndex in component on navigation', () => { + const fixture = createComponent(StepperWithTwoWayBindingOnSelectedIndex); + fixture.detectChanges(); + + expect(fixture.componentInstance.index).toBe(0); + + const stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header')); + + let lastStepHeaderEl = stepHeaders[2].nativeElement; + lastStepHeaderEl.click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.index).toBe(2); + + let middleStepHeaderEl = stepHeaders[1].nativeElement; + middleStepHeaderEl.click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.index).toBe(1); + + let firstStepHeaderEl = stepHeaders[0].nativeElement; + firstStepHeaderEl.click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.index).toBe(0); + }); + }); }); /** Asserts that keyboard interaction works correctly. */ @@ -2166,3 +2195,16 @@ class StepperWithLazyContent { class HorizontalStepperWithDelayedStep { renderSecondStep = false; } + +@Component({ + template: ` + + + + + + `, +}) +class StepperWithTwoWayBindingOnSelectedIndex { + index: number = 0; +} diff --git a/tools/public_api_guard/cdk/stepper.md b/tools/public_api_guard/cdk/stepper.md index c7c659bebfc5..a7e966b0f452 100644 --- a/tools/public_api_guard/cdk/stepper.md +++ b/tools/public_api_guard/cdk/stepper.md @@ -115,13 +115,14 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { set selected(step: CdkStep | undefined); get selectedIndex(): number; set selectedIndex(index: NumberInput); + readonly selectedIndexChange: EventEmitter; readonly selectionChange: EventEmitter; _stateChanged(): void; _stepHeader: QueryList; readonly steps: QueryList; _steps: QueryList; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }