@@ -12,7 +12,7 @@ import {
1212 Input ,
1313 NgZone ,
1414 OnDestroy ,
15- AfterContentInit ,
15+ DoCheck ,
1616 Injectable ,
1717} from '@angular/core' ;
1818import { coerceBooleanProperty } from '@angular/cdk/coercion' ;
@@ -32,6 +32,8 @@ import {InteractivityChecker} from './interactivity-checker';
3232export class FocusTrap {
3333 private _startAnchor : HTMLElement | null ;
3434 private _endAnchor : HTMLElement | null ;
35+ private _element : HTMLElement | null ;
36+ private _hasAttached = false ;
3537
3638 /** Whether the focus trap is active. */
3739 get enabled ( ) : boolean { return this . _enabled ; }
@@ -45,12 +47,14 @@ export class FocusTrap {
4547 private _enabled : boolean = true ;
4648
4749 constructor (
48- private _element : HTMLElement ,
50+ element : HTMLElement ,
4951 private _platform : Platform ,
5052 private _checker : InteractivityChecker ,
5153 private _ngZone : NgZone ,
5254 deferAnchors = false ) {
5355
56+ this . _element = element ;
57+
5458 if ( ! deferAnchors ) {
5559 this . attachAnchors ( ) ;
5660 }
@@ -66,41 +70,41 @@ export class FocusTrap {
6670 this . _endAnchor . parentNode . removeChild ( this . _endAnchor ) ;
6771 }
6872
69- this . _startAnchor = this . _endAnchor = null ;
73+ this . _element = this . _startAnchor = this . _endAnchor = null ;
7074 }
7175
7276 /**
7377 * Inserts the anchors into the DOM. This is usually done automatically
7478 * 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.
7581 */
76- attachAnchors ( ) : void {
82+ attachAnchors ( ) : boolean {
7783 // 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 ;
8887 }
8988
9089 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+ }
9894
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 ( ) ) ;
10298 }
10399 } ) ;
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 ;
104108 }
105109
106110 /**
@@ -146,7 +150,7 @@ export class FocusTrap {
146150 */
147151 private _getRegionBoundary ( bound : 'start' | 'end' ) : HTMLElement | null {
148152 // 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 } ], ` +
150154 `[cdk-focus-${ bound } ]` ) as NodeListOf < HTMLElement > ;
151155
152156 for ( let i = 0 ; i < markers . length ; i ++ ) {
@@ -157,18 +161,18 @@ export class FocusTrap {
157161 }
158162
159163 if ( bound == 'start' ) {
160- return markers . length ? markers [ 0 ] : this . _getFirstTabbableElement ( this . _element ) ;
164+ return markers . length ? markers [ 0 ] : this . _getFirstTabbableElement ( this . _element ! ) ;
161165 }
162166 return markers . length ?
163- markers [ markers . length - 1 ] : this . _getLastTabbableElement ( this . _element ) ;
167+ markers [ markers . length - 1 ] : this . _getLastTabbableElement ( this . _element ! ) ;
164168 }
165169
166170 /**
167171 * Focuses the element that should be focused when the focus trap is initialized.
168172 * @returns Returns whether focus was moved successfuly.
169173 */
170174 focusInitialElement ( ) : boolean {
171- const redirectToElement = this . _element . querySelector ( '[cdk-focus-initial]' ) as HTMLElement ;
175+ const redirectToElement = this . _element ! . querySelector ( '[cdk-focus-initial]' ) as HTMLElement ;
172176
173177 if ( redirectToElement ) {
174178 redirectToElement . focus ( ) ;
@@ -206,6 +210,13 @@ export class FocusTrap {
206210 return ! ! redirectToElement ;
207211 }
208212
213+ /**
214+ * Checks whether the focus trap has successfuly been attached.
215+ */
216+ hasAttached ( ) : boolean {
217+ return this . _hasAttached ;
218+ }
219+
209220 /** Get the first tabbable element from a DOM subtree (inclusive). */
210221 private _getFirstTabbableElement ( root : HTMLElement ) : HTMLElement | null {
211222 if ( this . _checker . isFocusable ( root ) && this . _checker . isTabbable ( root ) ) {
@@ -287,12 +298,12 @@ export class FocusTrapFactory {
287298
288299/**
289300 * Directive for trapping focus within a region.
290- * @deprecated
301+ * @deprecated Use `cdkTrapFocus` instead.
291302 */
292303@Directive ( {
293304 selector : 'cdk-focus-trap' ,
294305} )
295- export class FocusTrapDeprecatedDirective implements OnDestroy , AfterContentInit {
306+ export class FocusTrapDeprecatedDirective implements OnDestroy , DoCheck {
296307 focusTrap : FocusTrap ;
297308
298309 /** Whether the focus trap is active. */
@@ -310,8 +321,10 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
310321 this . focusTrap . destroy ( ) ;
311322 }
312323
313- ngAfterContentInit ( ) {
314- this . focusTrap . attachAnchors ( ) ;
324+ ngDoCheck ( ) {
325+ if ( ! this . focusTrap . hasAttached ( ) ) {
326+ this . focusTrap . attachAnchors ( ) ;
327+ }
315328 }
316329}
317330
@@ -321,7 +334,7 @@ export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit
321334 selector : '[cdkTrapFocus]' ,
322335 exportAs : 'cdkTrapFocus' ,
323336} )
324- export class FocusTrapDirective implements OnDestroy , AfterContentInit {
337+ export class FocusTrapDirective implements OnDestroy , DoCheck {
325338 focusTrap : FocusTrap ;
326339
327340 /** Whether the focus trap is active. */
@@ -337,7 +350,11 @@ export class FocusTrapDirective implements OnDestroy, AfterContentInit {
337350 this . focusTrap . destroy ( ) ;
338351 }
339352
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+ }
342359 }
343360}
0 commit comments