diff --git a/src/cdk/a11y/focus-trap/focus-trap.ts b/src/cdk/a11y/focus-trap/focus-trap.ts index 8dbf83827aae..9e406e5fd6b3 100644 --- a/src/cdk/a11y/focus-trap/focus-trap.ts +++ b/src/cdk/a11y/focus-trap/focus-trap.ts @@ -17,6 +17,7 @@ import { Input, NgZone, OnDestroy, + DoCheck, } from '@angular/core'; import {take} from 'rxjs/operators'; import {InteractivityChecker} from '../interactivity-checker/interactivity-checker'; @@ -32,6 +33,7 @@ import {InteractivityChecker} from '../interactivity-checker/interactivity-check export class FocusTrap { private _startAnchor: HTMLElement | null; private _endAnchor: HTMLElement | null; + private _hasAttached = false; /** Whether the focus trap is active. */ get enabled(): boolean { return this._enabled; } @@ -72,30 +74,34 @@ export class FocusTrap { /** * Inserts the anchors into the DOM. This is usually done automatically * in the constructor, but can be deferred for cases like directives with `*ngIf`. + * @returns Whether the focus trap managed to attach successfuly. This may not be the case + * if the target element isn't currently in the DOM. */ - attachAnchors(): void { - if (!this._startAnchor) { - this._startAnchor = this._createAnchor(); - } - - if (!this._endAnchor) { - this._endAnchor = this._createAnchor(); + attachAnchors(): boolean { + // If we're not on the browser, there can be no focus to trap. + if (this._hasAttached) { + return true; } this._ngZone.runOutsideAngular(() => { - this._startAnchor!.addEventListener('focus', () => { - this.focusLastTabbableElement(); - }); - - this._endAnchor!.addEventListener('focus', () => { - this.focusFirstTabbableElement(); - }); + if (!this._startAnchor) { + this._startAnchor = this._createAnchor(); + this._startAnchor!.addEventListener('focus', () => this.focusLastTabbableElement()); + } - if (this._element.parentNode) { - this._element.parentNode.insertBefore(this._startAnchor!, this._element); - this._element.parentNode.insertBefore(this._endAnchor!, this._element.nextSibling); + if (!this._endAnchor) { + this._endAnchor = this._createAnchor(); + this._endAnchor!.addEventListener('focus', () => this.focusFirstTabbableElement()); } }); + + if (this._element.parentNode) { + this._element.parentNode.insertBefore(this._startAnchor!, this._element); + this._element.parentNode.insertBefore(this._endAnchor!, this._element.nextSibling); + this._hasAttached = true; + } + + return this._hasAttached; } /** @@ -217,6 +223,13 @@ export class FocusTrap { return !!redirectToElement; } + /** + * Checks whether the focus trap has successfuly been attached. + */ + hasAttached(): boolean { + return this._hasAttached; + } + /** Get the first tabbable element from a DOM subtree (inclusive). */ private _getFirstTabbableElement(root: HTMLElement): HTMLElement | null { if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) { @@ -313,7 +326,7 @@ export class FocusTrapFactory { selector: '[cdkTrapFocus]', exportAs: 'cdkTrapFocus', }) -export class CdkTrapFocus implements OnDestroy, AfterContentInit { +export class CdkTrapFocus implements OnDestroy, AfterContentInit, DoCheck { private _document: Document; /** Underlying FocusTrap instance. */ @@ -364,4 +377,10 @@ export class CdkTrapFocus implements OnDestroy, AfterContentInit { this.focusTrap.focusInitialElementWhenReady(); } } + + ngDoCheck() { + if (!this.focusTrap.hasAttached()) { + this.focusTrap.attachAnchors(); + } + } }