diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index 03cd15f597d..ae892d376ab 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -19,6 +19,7 @@ import { ContentChildren, Directive, EventEmitter, + ElementRef, forwardRef, Inject, Input, @@ -31,6 +32,7 @@ import { ViewChild, ViewEncapsulation, } from '@angular/core'; +import {DOCUMENT} from '@angular/common'; import {AbstractControl} from '@angular/forms'; import {CdkStepLabel} from './step-label'; import {Observable, Subject, of as obaservableOf} from 'rxjs'; @@ -164,6 +166,12 @@ export class CdkStepper implements AfterViewInit, OnDestroy { /** Used for managing keyboard focus. */ private _keyManager: FocusKeyManager; + /** + * @breaking-change 8.0.0 Remove `| undefined` once the `_document` + * constructor param is required. + */ + private _document: Document | undefined; + /** The list of step components that the stepper is holding. */ @ContentChildren(CdkStep) _steps: QueryList; @@ -218,8 +226,12 @@ export class CdkStepper implements AfterViewInit, OnDestroy { constructor( @Optional() private _dir: Directionality, - private _changeDetectorRef: ChangeDetectorRef) { + private _changeDetectorRef: ChangeDetectorRef, + // @breaking-change 8.0.0 `_elementRef` and `_document` parameters to become required. + private _elementRef?: ElementRef, + @Inject(DOCUMENT) _document?: any) { this._groupId = nextId++; + this._document = _document; } ngAfterViewInit() { @@ -305,7 +317,14 @@ export class CdkStepper implements AfterViewInit, OnDestroy { selectedStep: stepsArray[newIndex], previouslySelectedStep: stepsArray[this._selectedIndex], }); - this._keyManager.updateActiveItemIndex(newIndex); + + // If focus is inside the stepper, move it to the next header, otherwise it may become + // lost when the active step content is hidden. We can't be more granular with the check + // (e.g. checking whether focus is inside the active step), because we don't have a + // reference to the elements that are rendering out the content. + this._containsFocus() ? this._keyManager.setActiveItem(newIndex) : + this._keyManager.updateActiveItemIndex(newIndex); + this._selectedIndex = newIndex; this._stateChanged(); } @@ -348,4 +367,15 @@ export class CdkStepper implements AfterViewInit, OnDestroy { private _layoutDirection(): Direction { return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr'; } + + /** Checks whether the stepper contains the focused element. */ + private _containsFocus(): boolean { + if (!this._document || !this._elementRef) { + return false; + } + + const stepperElement = this._elementRef.nativeElement; + const focusedElement = this._document.activeElement; + return stepperElement === focusedElement || stepperElement.contains(focusedElement); + } } diff --git a/src/lib/stepper/stepper.spec.ts b/src/lib/stepper/stepper.spec.ts index 3cb330ed073..7aa19ef3da5 100644 --- a/src/lib/stepper/stepper.spec.ts +++ b/src/lib/stepper/stepper.spec.ts @@ -259,6 +259,20 @@ describe('MatStepper', () => { expect(stepHeaderEl.focus).not.toHaveBeenCalled(); }); + it('should focus next step header if focus is inside the stepper', () => { + let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance; + let stepHeaderEl = fixture.debugElement.queryAll(By.css('mat-step-header'))[1].nativeElement; + let nextButtonNativeEl = fixture.debugElement + .queryAll(By.directive(MatStepperNext))[0].nativeElement; + spyOn(stepHeaderEl, 'focus'); + nextButtonNativeEl.focus(); + nextButtonNativeEl.click(); + fixture.detectChanges(); + + expect(stepperComponent.selectedIndex).toBe(1); + expect(stepHeaderEl.focus).toHaveBeenCalled(); + }); + it('should only be able to return to a previous step if it is editable', () => { let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance; diff --git a/src/lib/stepper/stepper.ts b/src/lib/stepper/stepper.ts index 250f2c816cc..df3358c2595 100644 --- a/src/lib/stepper/stepper.ts +++ b/src/lib/stepper/stepper.ts @@ -17,6 +17,7 @@ import { ContentChild, ContentChildren, Directive, + ElementRef, EventEmitter, forwardRef, Inject, @@ -29,6 +30,7 @@ import { ViewEncapsulation, } from '@angular/core'; import {FormControl, FormGroupDirective, NgForm} from '@angular/forms'; +import {DOCUMENT} from '@angular/common'; import {ErrorStateMatcher} from '@angular/material/core'; import {MatStepHeader} from './step-header'; import {MatStepLabel} from './step-label'; @@ -147,8 +149,13 @@ export class MatHorizontalStepper extends MatStepper { } changeDetection: ChangeDetectionStrategy.OnPush, }) export class MatVerticalStepper extends MatStepper { - constructor(@Optional() dir: Directionality, changeDetectorRef: ChangeDetectorRef) { - super(dir, changeDetectorRef); + constructor( + @Optional() dir: Directionality, + changeDetectorRef: ChangeDetectorRef, + // @breaking-change 8.0.0 `elementRef` and `_document` parameters to become required. + elementRef?: ElementRef, + @Inject(DOCUMENT) _document?: any) { + super(dir, changeDetectorRef, elementRef, _document); this._orientation = 'vertical'; } }