@@ -12,7 +12,7 @@ import {
12
12
Input ,
13
13
NgZone ,
14
14
OnDestroy ,
15
- AfterContentInit ,
15
+ DoCheck ,
16
16
Injectable ,
17
17
} from '@angular/core' ;
18
18
import { coerceBooleanProperty } from '@angular/cdk/coercion' ;
@@ -32,6 +32,7 @@ import {InteractivityChecker} from './interactivity-checker';
32
32
export class FocusTrap {
33
33
private _startAnchor : HTMLElement | null ;
34
34
private _endAnchor : HTMLElement | null ;
35
+ private _hasAttached = false ;
35
36
36
37
/** Whether the focus trap is active. */
37
38
get enabled ( ) : boolean { return this . _enabled ; }
@@ -72,35 +73,35 @@ export class FocusTrap {
72
73
/**
73
74
* Inserts the anchors into the DOM. This is usually done automatically
74
75
* 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.
75
78
*/
76
- attachAnchors ( ) : void {
79
+ attachAnchors ( ) : boolean {
77
80
// 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 ;
88
84
}
89
85
90
86
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
+ }
98
91
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 ( ) ) ;
102
95
}
103
96
} ) ;
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 ;
104
105
}
105
106
106
107
/**
@@ -206,6 +207,13 @@ export class FocusTrap {
206
207
return ! ! redirectToElement ;
207
208
}
208
209
210
+ /**
211
+ * Checks whether the focus trap has successfuly been attached.
212
+ */
213
+ hasAttached ( ) : boolean {
214
+ return this . _hasAttached ;
215
+ }
216
+
209
217
/** Get the first tabbable element from a DOM subtree (inclusive). */
210
218
private _getFirstTabbableElement ( root : HTMLElement ) : HTMLElement | null {
211
219
if ( this . _checker . isFocusable ( root ) && this . _checker . isTabbable ( root ) ) {
@@ -287,12 +295,12 @@ export class FocusTrapFactory {
287
295
288
296
/**
289
297
* Directive for trapping focus within a region.
290
- * @deprecated
298
+ * @deprecated Use `cdkTrapFocus` instead.
291
299
*/
292
300
@Directive ( {
293
301
selector : 'cdk-focus-trap' ,
294
302
} )
295
- export class FocusTrapDeprecatedDirective implements OnDestroy , AfterContentInit {
303
+ export class FocusTrapDeprecatedDirective implements OnDestroy , DoCheck {
296
304
focusTrap : FocusTrap ;
297
305
298
306
/** Whether the focus trap is active. */
@@ -310,8 +318,10 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
310
318
this . focusTrap . destroy ( ) ;
311
319
}
312
320
313
- ngAfterContentInit ( ) {
314
- this . focusTrap . attachAnchors ( ) ;
321
+ ngDoCheck ( ) {
322
+ if ( ! this . focusTrap . hasAttached ( ) ) {
323
+ this . focusTrap . attachAnchors ( ) ;
324
+ }
315
325
}
316
326
}
317
327
@@ -321,7 +331,7 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
321
331
selector : '[cdkTrapFocus]' ,
322
332
exportAs : 'cdkTrapFocus' ,
323
333
} )
324
- export class FocusTrapDirective implements OnDestroy , AfterContentInit {
334
+ export class FocusTrapDirective implements OnDestroy , DoCheck {
325
335
focusTrap : FocusTrap ;
326
336
327
337
/** Whether the focus trap is active. */
@@ -337,7 +347,11 @@ export class FocusTrapDirective implements OnDestroy, AfterContentInit {
337
347
this . focusTrap . destroy ( ) ;
338
348
}
339
349
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
+ }
342
356
}
343
357
}
0 commit comments