Skip to content

Commit 2a8943b

Browse files
committed
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.
1 parent 4d97271 commit 2a8943b

File tree

1 file changed

+43
-29
lines changed

1 file changed

+43
-29
lines changed

src/cdk/a11y/focus-trap.ts

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
Input,
1313
NgZone,
1414
OnDestroy,
15-
AfterContentInit,
15+
DoCheck,
1616
Injectable,
1717
} from '@angular/core';
1818
import {coerceBooleanProperty} from '@angular/cdk/coercion';
@@ -32,6 +32,7 @@ import {InteractivityChecker} from './interactivity-checker';
3232
export class FocusTrap {
3333
private _startAnchor: HTMLElement | null;
3434
private _endAnchor: HTMLElement | null;
35+
private _hasAttached = false;
3536

3637
/** Whether the focus trap is active. */
3738
get enabled(): boolean { return this._enabled; }
@@ -72,35 +73,35 @@ export class FocusTrap {
7273
/**
7374
* Inserts the anchors into the DOM. This is usually done automatically
7475
* in the constructor, but can be deferred for cases like directives with `*ngIf`.
76+
* @returns Whether the focus trap managed to attach successfuly. This may not be the case
77+
* if the target element isn't currently in the DOM.
7578
*/
76-
attachAnchors(): void {
79+
attachAnchors(): boolean {
7780
// If we're not on the browser, there can be no focus to trap.
78-
if (!this._platform.isBrowser) {
79-
return;
80-
}
81-
82-
if (!this._startAnchor) {
83-
this._startAnchor = this._createAnchor();
84-
}
85-
86-
if (!this._endAnchor) {
87-
this._endAnchor = this._createAnchor();
81+
if (!this._platform.isBrowser || this._hasAttached) {
82+
this._hasAttached = true;
83+
return true;
8884
}
8985

9086
this._ngZone.runOutsideAngular(() => {
91-
this._startAnchor!.addEventListener('focus', () => {
92-
this.focusLastTabbableElement();
93-
});
94-
95-
this._endAnchor!.addEventListener('focus', () => {
96-
this.focusFirstTabbableElement();
97-
});
87+
if (!this._startAnchor) {
88+
this._startAnchor = this._createAnchor();
89+
this._startAnchor!.addEventListener('focus', () => this.focusLastTabbableElement());
90+
}
9891

99-
if (this._element.parentNode) {
100-
this._element.parentNode.insertBefore(this._startAnchor!, this._element);
101-
this._element.parentNode.insertBefore(this._endAnchor!, this._element.nextSibling);
92+
if (!this._endAnchor) {
93+
this._endAnchor = this._createAnchor();
94+
this._endAnchor!.addEventListener('focus', () => this.focusFirstTabbableElement());
10295
}
10396
});
97+
98+
if (this._element.parentNode) {
99+
this._element.parentNode.insertBefore(this._startAnchor!, this._element);
100+
this._element.parentNode.insertBefore(this._endAnchor!, this._element.nextSibling);
101+
this._hasAttached = true;
102+
}
103+
104+
return this._hasAttached;
104105
}
105106

106107
/**
@@ -206,6 +207,13 @@ export class FocusTrap {
206207
return !!redirectToElement;
207208
}
208209

210+
/**
211+
* Checks whether the focus trap has successfuly been attached.
212+
*/
213+
hasAttached(): boolean {
214+
return this._hasAttached;
215+
}
216+
209217
/** Get the first tabbable element from a DOM subtree (inclusive). */
210218
private _getFirstTabbableElement(root: HTMLElement): HTMLElement | null {
211219
if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) {
@@ -287,12 +295,12 @@ export class FocusTrapFactory {
287295

288296
/**
289297
* Directive for trapping focus within a region.
290-
* @deprecated
298+
* @deprecated Use `cdkTrapFocus` instead.
291299
*/
292300
@Directive({
293301
selector: 'cdk-focus-trap',
294302
})
295-
export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit {
303+
export class FocusTrapDeprecatedDirective implements OnDestroy, DoCheck {
296304
focusTrap: FocusTrap;
297305

298306
/** Whether the focus trap is active. */
@@ -310,8 +318,10 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
310318
this.focusTrap.destroy();
311319
}
312320

313-
ngAfterContentInit() {
314-
this.focusTrap.attachAnchors();
321+
ngDoCheck() {
322+
if (!this.focusTrap.hasAttached()) {
323+
this.focusTrap.attachAnchors();
324+
}
315325
}
316326
}
317327

@@ -321,7 +331,7 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
321331
selector: '[cdkTrapFocus]',
322332
exportAs: 'cdkTrapFocus',
323333
})
324-
export class FocusTrapDirective implements OnDestroy, AfterContentInit {
334+
export class FocusTrapDirective implements OnDestroy, DoCheck {
325335
focusTrap: FocusTrap;
326336

327337
/** Whether the focus trap is active. */
@@ -337,7 +347,11 @@ export class FocusTrapDirective implements OnDestroy, AfterContentInit {
337347
this.focusTrap.destroy();
338348
}
339349

340-
ngAfterContentInit() {
341-
this.focusTrap.attachAnchors();
350+
ngDoCheck() {
351+
// Since the element may not be attached to the DOM (or another element)
352+
// immediately, keep trying until it is.
353+
if (!this.focusTrap.hasAttached()) {
354+
this.focusTrap.attachAnchors();
355+
}
342356
}
343357
}

0 commit comments

Comments
 (0)