1
- import { Directive , ElementRef , Input , NgZone , AfterViewInit , OnDestroy } from '@angular/core' ;
1
+ import {
2
+ Directive ,
3
+ ElementRef ,
4
+ Input ,
5
+ NgZone ,
6
+ OnDestroy ,
7
+ AfterContentInit ,
8
+ Injectable ,
9
+ } from '@angular/core' ;
2
10
import { InteractivityChecker } from './interactivity-checker' ;
3
11
import { coerceBooleanProperty } from '../coercion/boolean-property' ;
4
12
5
13
6
14
/**
7
- * Directive for trapping focus within a region .
15
+ * Class that allows for trapping focus within a DOM element .
8
16
*
9
- * NOTE: This directive currently uses a very simple (naive) approach to focus trapping.
17
+ * NOTE: This class currently uses a very simple (naive) approach to focus trapping.
10
18
* It assumes that the tab order is the same as DOM order, which is not necessarily true.
11
19
* Things like tabIndex > 0, flex `order`, and shadow roots can cause to two to misalign.
12
20
* This will be replaced with a more intelligent solution before the library is considered stable.
13
21
*/
14
- @Directive ( {
15
- selector : 'cdk-focus-trap, focus-trap, [cdk-focus-trap], [focus-trap]' ,
16
- } )
17
- export class FocusTrap implements AfterViewInit , OnDestroy {
22
+ export class FocusTrap {
18
23
private _startAnchor : HTMLElement = this . _createAnchor ( ) ;
19
24
private _endAnchor : HTMLElement = this . _createAnchor ( ) ;
20
25
21
26
/** Whether the focus trap is active. */
22
- @Input ( )
23
27
get disabled ( ) : boolean { return this . _disabled ; }
24
28
set disabled ( val : boolean ) {
25
- this . _disabled = coerceBooleanProperty ( val ) ;
29
+ this . _disabled = val ;
26
30
this . _startAnchor . tabIndex = this . _endAnchor . tabIndex = this . _disabled ? - 1 : 0 ;
27
31
}
28
32
private _disabled : boolean = false ;
29
33
34
+ /** Element to which the focus trap is attached. */
35
+ get element ( ) : HTMLElement {
36
+ return this . _element ;
37
+ }
38
+
30
39
constructor (
40
+ private _element : HTMLElement ,
31
41
private _checker : InteractivityChecker ,
32
42
private _ngZone : NgZone ,
33
- private _elementRef : ElementRef ) { }
34
-
35
- ngAfterViewInit ( ) {
36
- this . _ngZone . runOutsideAngular ( ( ) => {
37
- this . _elementRef . nativeElement
38
- . insertAdjacentElement ( 'beforebegin' , this . _startAnchor )
39
- . addEventListener ( 'focus' , ( ) => this . focusLastTabbableElement ( ) ) ;
43
+ deferAnchors = false ) {
40
44
41
- this . _elementRef . nativeElement
42
- . insertAdjacentElement ( 'afterend' , this . _endAnchor )
43
- . addEventListener ( 'focus' , ( ) => this . focusFirstTabbableElement ( ) ) ;
44
- } ) ;
45
+ if ( ! deferAnchors ) {
46
+ this . attachAnchors ( ) ;
47
+ }
45
48
}
46
49
47
- ngOnDestroy ( ) {
50
+ /**
51
+ * Destroys the focus trap by cleaning up the anchors.
52
+ */
53
+ destroy ( ) {
48
54
if ( this . _startAnchor . parentNode ) {
49
55
this . _startAnchor . parentNode . removeChild ( this . _startAnchor ) ;
50
56
}
@@ -56,6 +62,22 @@ export class FocusTrap implements AfterViewInit, OnDestroy {
56
62
this . _startAnchor = this . _endAnchor = null ;
57
63
}
58
64
65
+ /**
66
+ * Inserts the anchors into the DOM. This is usually done automatically
67
+ * in the constructor, but can be deferred for cases like directives with `*ngIf`.
68
+ */
69
+ attachAnchors ( ) : void {
70
+ this . _ngZone . runOutsideAngular ( ( ) => {
71
+ this . _element
72
+ . insertAdjacentElement ( 'beforebegin' , this . _startAnchor )
73
+ . addEventListener ( 'focus' , ( ) => this . focusLastTabbableElement ( ) ) ;
74
+
75
+ this . _element
76
+ . insertAdjacentElement ( 'afterend' , this . _endAnchor )
77
+ . addEventListener ( 'focus' , ( ) => this . focusFirstTabbableElement ( ) ) ;
78
+ } ) ;
79
+ }
80
+
59
81
/**
60
82
* Waits for microtask queue to empty, then focuses the first tabbable element within the focus
61
83
* trap region.
@@ -76,9 +98,8 @@ export class FocusTrap implements AfterViewInit, OnDestroy {
76
98
* Focuses the first tabbable element within the focus trap region.
77
99
*/
78
100
focusFirstTabbableElement ( ) {
79
- let rootElement = this . _elementRef . nativeElement ;
80
- let redirectToElement = rootElement . querySelector ( '[cdk-focus-start]' ) as HTMLElement ||
81
- this . _getFirstTabbableElement ( rootElement ) ;
101
+ let redirectToElement = this . _element . querySelector ( '[cdk-focus-start]' ) as HTMLElement ||
102
+ this . _getFirstTabbableElement ( this . _element ) ;
82
103
83
104
if ( redirectToElement ) {
84
105
redirectToElement . focus ( ) ;
@@ -89,13 +110,13 @@ export class FocusTrap implements AfterViewInit, OnDestroy {
89
110
* Focuses the last tabbable element within the focus trap region.
90
111
*/
91
112
focusLastTabbableElement ( ) {
92
- let focusTargets = this . _elementRef . nativeElement . querySelectorAll ( '[cdk-focus-end]' ) ;
113
+ let focusTargets = this . _element . querySelectorAll ( '[cdk-focus-end]' ) ;
93
114
let redirectToElement : HTMLElement = null ;
94
115
95
116
if ( focusTargets . length ) {
96
117
redirectToElement = focusTargets [ focusTargets . length - 1 ] as HTMLElement ;
97
118
} else {
98
- redirectToElement = this . _getLastTabbableElement ( this . _elementRef . nativeElement ) ;
119
+ redirectToElement = this . _getLastTabbableElement ( this . _element ) ;
99
120
}
100
121
101
122
if ( redirectToElement ) {
@@ -142,6 +163,50 @@ export class FocusTrap implements AfterViewInit, OnDestroy {
142
163
let anchor = document . createElement ( 'div' ) ;
143
164
anchor . tabIndex = 0 ;
144
165
anchor . classList . add ( 'cdk-visually-hidden' ) ;
166
+ anchor . classList . add ( 'cdk-focus-trap-anchor' ) ;
145
167
return anchor ;
146
168
}
147
169
}
170
+
171
+
172
+ /**
173
+ * Service that allows easy instantiation of focus traps.
174
+ */
175
+ @Injectable ( )
176
+ export class FocusTrapService {
177
+ constructor ( private _checker : InteractivityChecker , private _ngZone : NgZone ) { }
178
+
179
+ attach ( element : HTMLElement , deferAnchors = false ) : FocusTrap {
180
+ return new FocusTrap ( element , this . _checker , this . _ngZone , deferAnchors ) ;
181
+ }
182
+ }
183
+
184
+
185
+ /**
186
+ * Directive for trapping focus within a region.
187
+ */
188
+ @Directive ( {
189
+ selector : 'cdk-focus-trap, [cdkFocusTrap]' ,
190
+ } )
191
+ export class FocusTrapDirective implements OnDestroy , AfterContentInit {
192
+ focusTrap : FocusTrap ;
193
+
194
+ /** Whether the focus trap is active. */
195
+ @Input ( )
196
+ get disabled ( ) : boolean { return this . focusTrap . disabled ; }
197
+ set disabled ( val : boolean ) {
198
+ this . focusTrap . disabled = coerceBooleanProperty ( val ) ;
199
+ }
200
+
201
+ constructor ( private _elementRef : ElementRef , private _focusTrapService : FocusTrapService ) {
202
+ this . focusTrap = this . _focusTrapService . attach ( this . _elementRef . nativeElement , true ) ;
203
+ }
204
+
205
+ ngOnDestroy ( ) {
206
+ this . focusTrap . destroy ( ) ;
207
+ }
208
+
209
+ ngAfterContentInit ( ) {
210
+ this . focusTrap . attachAnchors ( ) ;
211
+ }
212
+ }
0 commit comments