@@ -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,8 @@ import {InteractivityChecker} from './interactivity-checker';
32
32
export class FocusTrap {
33
33
private _startAnchor : HTMLElement | null ;
34
34
private _endAnchor : HTMLElement | null ;
35
+ private _element : HTMLElement | null ;
36
+ private _hasAttached = false ;
35
37
36
38
/** Whether the focus trap is active. */
37
39
get enabled ( ) : boolean { return this . _enabled ; }
@@ -45,12 +47,14 @@ export class FocusTrap {
45
47
private _enabled : boolean = true ;
46
48
47
49
constructor (
48
- private _element : HTMLElement ,
50
+ element : HTMLElement ,
49
51
private _platform : Platform ,
50
52
private _checker : InteractivityChecker ,
51
53
private _ngZone : NgZone ,
52
54
deferAnchors = false ) {
53
55
56
+ this . _element = element ;
57
+
54
58
if ( ! deferAnchors ) {
55
59
this . attachAnchors ( ) ;
56
60
}
@@ -66,41 +70,41 @@ export class FocusTrap {
66
70
this . _endAnchor . parentNode . removeChild ( this . _endAnchor ) ;
67
71
}
68
72
69
- this . _startAnchor = this . _endAnchor = null ;
73
+ this . _element = this . _startAnchor = this . _endAnchor = null ;
70
74
}
71
75
72
76
/**
73
77
* Inserts the anchors into the DOM. This is usually done automatically
74
78
* in the constructor, but can be deferred for cases like directives with `*ngIf`.
79
+ * @returns Whether the focus trap managed to attach successfuly. This may not be the case
80
+ * if the target element isn't currently in the DOM.
75
81
*/
76
- attachAnchors ( ) : void {
82
+ attachAnchors ( ) : boolean {
77
83
// 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 ( ) ;
84
+ if ( ! this . _platform . isBrowser || this . _hasAttached ) {
85
+ this . _hasAttached = true ;
86
+ return true ;
88
87
}
89
88
90
89
this . _ngZone . runOutsideAngular ( ( ) => {
91
- this . _startAnchor ! . addEventListener ( 'focus' , ( ) => {
92
- this . focusLastTabbableElement ( ) ;
93
- } ) ;
94
-
95
- this . _endAnchor ! . addEventListener ( 'focus' , ( ) => {
96
- this . focusFirstTabbableElement ( ) ;
97
- } ) ;
90
+ if ( ! this . _startAnchor ) {
91
+ this . _startAnchor = this . _createAnchor ( ) ;
92
+ this . _startAnchor ! . addEventListener ( 'focus' , ( ) => this . focusLastTabbableElement ( ) ) ;
93
+ }
98
94
99
- if ( this . _element . parentNode ) {
100
- this . _element . parentNode . insertBefore ( this . _startAnchor ! , this . _element ) ;
101
- this . _element . parentNode . insertBefore ( this . _endAnchor ! , this . _element . nextSibling ) ;
95
+ if ( ! this . _endAnchor ) {
96
+ this . _endAnchor = this . _createAnchor ( ) ;
97
+ this . _endAnchor ! . addEventListener ( 'focus' , ( ) => this . focusFirstTabbableElement ( ) ) ;
102
98
}
103
99
} ) ;
100
+
101
+ if ( this . _element && this . _element . parentNode ) {
102
+ this . _element . parentNode . insertBefore ( this . _startAnchor ! , this . _element ) ;
103
+ this . _element . parentNode . insertBefore ( this . _endAnchor ! , this . _element . nextSibling ) ;
104
+ this . _hasAttached = true ;
105
+ }
106
+
107
+ return this . _hasAttached ;
104
108
}
105
109
106
110
/**
@@ -146,7 +150,7 @@ export class FocusTrap {
146
150
*/
147
151
private _getRegionBoundary ( bound : 'start' | 'end' ) : HTMLElement | null {
148
152
// Contains the deprecated version of selector, for temporary backwards comparability.
149
- let markers = this . _element . querySelectorAll ( `[cdk-focus-region-${ bound } ], ` +
153
+ let markers = this . _element ! . querySelectorAll ( `[cdk-focus-region-${ bound } ], ` +
150
154
`[cdk-focus-${ bound } ]` ) as NodeListOf < HTMLElement > ;
151
155
152
156
for ( let i = 0 ; i < markers . length ; i ++ ) {
@@ -157,18 +161,18 @@ export class FocusTrap {
157
161
}
158
162
159
163
if ( bound == 'start' ) {
160
- return markers . length ? markers [ 0 ] : this . _getFirstTabbableElement ( this . _element ) ;
164
+ return markers . length ? markers [ 0 ] : this . _getFirstTabbableElement ( this . _element ! ) ;
161
165
}
162
166
return markers . length ?
163
- markers [ markers . length - 1 ] : this . _getLastTabbableElement ( this . _element ) ;
167
+ markers [ markers . length - 1 ] : this . _getLastTabbableElement ( this . _element ! ) ;
164
168
}
165
169
166
170
/**
167
171
* Focuses the element that should be focused when the focus trap is initialized.
168
172
* @returns Returns whether focus was moved successfuly.
169
173
*/
170
174
focusInitialElement ( ) : boolean {
171
- const redirectToElement = this . _element . querySelector ( '[cdk-focus-initial]' ) as HTMLElement ;
175
+ const redirectToElement = this . _element ! . querySelector ( '[cdk-focus-initial]' ) as HTMLElement ;
172
176
173
177
if ( redirectToElement ) {
174
178
redirectToElement . focus ( ) ;
@@ -206,6 +210,13 @@ export class FocusTrap {
206
210
return ! ! redirectToElement ;
207
211
}
208
212
213
+ /**
214
+ * Checks whether the focus trap has successfuly been attached.
215
+ */
216
+ hasAttached ( ) : boolean {
217
+ return this . _hasAttached ;
218
+ }
219
+
209
220
/** Get the first tabbable element from a DOM subtree (inclusive). */
210
221
private _getFirstTabbableElement ( root : HTMLElement ) : HTMLElement | null {
211
222
if ( this . _checker . isFocusable ( root ) && this . _checker . isTabbable ( root ) ) {
@@ -287,12 +298,12 @@ export class FocusTrapFactory {
287
298
288
299
/**
289
300
* Directive for trapping focus within a region.
290
- * @deprecated
301
+ * @deprecated Use `cdkTrapFocus` instead.
291
302
*/
292
303
@Directive ( {
293
304
selector : 'cdk-focus-trap' ,
294
305
} )
295
- export class FocusTrapDeprecatedDirective implements OnDestroy , AfterContentInit {
306
+ export class FocusTrapDeprecatedDirective implements OnDestroy , DoCheck {
296
307
focusTrap : FocusTrap ;
297
308
298
309
/** Whether the focus trap is active. */
@@ -310,8 +321,10 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
310
321
this . focusTrap . destroy ( ) ;
311
322
}
312
323
313
- ngAfterContentInit ( ) {
314
- this . focusTrap . attachAnchors ( ) ;
324
+ ngDoCheck ( ) {
325
+ if ( ! this . focusTrap . hasAttached ( ) ) {
326
+ this . focusTrap . attachAnchors ( ) ;
327
+ }
315
328
}
316
329
}
317
330
@@ -321,7 +334,7 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
321
334
selector : '[cdkTrapFocus]' ,
322
335
exportAs : 'cdkTrapFocus' ,
323
336
} )
324
- export class FocusTrapDirective implements OnDestroy , AfterContentInit {
337
+ export class FocusTrapDirective implements OnDestroy , DoCheck {
325
338
focusTrap : FocusTrap ;
326
339
327
340
/** Whether the focus trap is active. */
@@ -337,7 +350,11 @@ export class FocusTrapDirective implements OnDestroy, AfterContentInit {
337
350
this . focusTrap . destroy ( ) ;
338
351
}
339
352
340
- ngAfterContentInit ( ) {
341
- this . focusTrap . attachAnchors ( ) ;
353
+ ngDoCheck ( ) {
354
+ // Since the element may not be attached to the DOM (or another element)
355
+ // immediately, keep trying until it is.
356
+ if ( ! this . focusTrap . hasAttached ( ) ) {
357
+ this . focusTrap . attachAnchors ( ) ;
358
+ }
342
359
}
343
360
}
0 commit comments