From f01e4bbe7e33072f9734c8b75c2eec1f18f28ce1 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 29 Apr 2018 23:14:45 -0400 Subject: [PATCH] fix(focus-trap): not attaching correctly if element is not in the DOM on init Currently the focus trap attempts to attach itself once on init (e.g. when it is projected inside of an overlay), however in some cases it might not be in the DOM yet. These changes make it so it re-tries to attach itself until it does so successfully. --- src/cdk/a11y/focus-trap/focus-trap.ts | 55 ++++++++++++++++++--------- 1 file changed, 37 insertions(+), 18 deletions(-) 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(); + } + } }