1
- import { Component , ViewEncapsulation , ViewChild , ElementRef , Input , NgZone } from '@angular/core' ;
1
+ import { Directive , ElementRef , Input , NgZone , AfterViewInit , OnDestroy } from '@angular/core' ;
2
2
import { InteractivityChecker } from './interactivity-checker' ;
3
3
import { coerceBooleanProperty } from '../coercion/boolean-property' ;
4
4
@@ -11,48 +11,72 @@ import {coerceBooleanProperty} from '../coercion/boolean-property';
11
11
* Things like tabIndex > 0, flex `order`, and shadow roots can cause to two to misalign.
12
12
* This will be replaced with a more intelligent solution before the library is considered stable.
13
13
*/
14
- @Component ( {
15
- moduleId : module . id ,
16
- selector : 'cdk-focus-trap, focus-trap' ,
17
- templateUrl : 'focus-trap.html' ,
18
- encapsulation : ViewEncapsulation . None ,
14
+ @Directive ( {
15
+ selector : 'cdk-focus-trap, focus-trap, [cdk-focus-trap], [focus-trap]' ,
19
16
} )
20
- export class FocusTrap {
21
- @ViewChild ( 'trappedContent' ) trappedContent : ElementRef ;
17
+ export class FocusTrap implements AfterViewInit , OnDestroy {
18
+ private _startAnchor : HTMLElement = this . _createAnchor ( ) ;
19
+ private _endAnchor : HTMLElement = this . _createAnchor ( ) ;
22
20
23
21
/** Whether the focus trap is active. */
24
22
@Input ( )
25
23
get disabled ( ) : boolean { return this . _disabled ; }
26
- set disabled ( val : boolean ) { this . _disabled = coerceBooleanProperty ( val ) ; }
24
+ set disabled ( val : boolean ) {
25
+ this . _disabled = coerceBooleanProperty ( val ) ;
26
+ this . _startAnchor . tabIndex = this . _endAnchor . tabIndex = this . _disabled ? - 1 : 0 ;
27
+ }
27
28
private _disabled : boolean = false ;
28
29
29
- constructor ( private _checker : InteractivityChecker , private _ngZone : NgZone ) { }
30
+ constructor (
31
+ private _checker : InteractivityChecker ,
32
+ 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 ( ) ) ;
40
+
41
+ this . _elementRef . nativeElement
42
+ . insertAdjacentElement ( 'afterend' , this . _endAnchor )
43
+ . addEventListener ( 'focus' , ( ) => this . focusFirstTabbableElement ( ) ) ;
44
+ } ) ;
45
+ }
46
+
47
+ ngOnDestroy ( ) {
48
+ if ( this . _startAnchor . parentNode ) {
49
+ this . _startAnchor . parentNode . removeChild ( this . _startAnchor ) ;
50
+ }
51
+
52
+ if ( this . _endAnchor . parentNode ) {
53
+ this . _endAnchor . parentNode . removeChild ( this . _endAnchor ) ;
54
+ }
55
+
56
+ this . _startAnchor = this . _endAnchor = null ;
57
+ }
30
58
31
59
/**
32
60
* Waits for microtask queue to empty, then focuses the first tabbable element within the focus
33
61
* trap region.
34
62
*/
35
63
focusFirstTabbableElementWhenReady ( ) {
36
- this . _ngZone . onMicrotaskEmpty . first ( ) . subscribe ( ( ) => {
37
- this . focusFirstTabbableElement ( ) ;
38
- } ) ;
64
+ this . _ngZone . onMicrotaskEmpty . first ( ) . subscribe ( ( ) => this . focusFirstTabbableElement ( ) ) ;
39
65
}
40
66
41
67
/**
42
68
* Waits for microtask queue to empty, then focuses the last tabbable element within the focus
43
69
* trap region.
44
70
*/
45
71
focusLastTabbableElementWhenReady ( ) {
46
- this . _ngZone . onMicrotaskEmpty . first ( ) . subscribe ( ( ) => {
47
- this . focusLastTabbableElement ( ) ;
48
- } ) ;
72
+ this . _ngZone . onMicrotaskEmpty . first ( ) . subscribe ( ( ) => this . focusLastTabbableElement ( ) ) ;
49
73
}
50
74
51
75
/**
52
76
* Focuses the first tabbable element within the focus trap region.
53
77
*/
54
78
focusFirstTabbableElement ( ) {
55
- let rootElement = this . trappedContent . nativeElement ;
79
+ let rootElement = this . _elementRef . nativeElement ;
56
80
let redirectToElement = rootElement . querySelector ( '[cdk-focus-start]' ) as HTMLElement ||
57
81
this . _getFirstTabbableElement ( rootElement ) ;
58
82
@@ -65,14 +89,13 @@ export class FocusTrap {
65
89
* Focuses the last tabbable element within the focus trap region.
66
90
*/
67
91
focusLastTabbableElement ( ) {
68
- let rootElement = this . trappedContent . nativeElement ;
69
- let focusTargets = rootElement . querySelectorAll ( '[cdk-focus-end]' ) ;
92
+ let focusTargets = this . _elementRef . nativeElement . querySelectorAll ( '[cdk-focus-end]' ) ;
70
93
let redirectToElement : HTMLElement = null ;
71
94
72
95
if ( focusTargets . length ) {
73
96
redirectToElement = focusTargets [ focusTargets . length - 1 ] as HTMLElement ;
74
97
} else {
75
- redirectToElement = this . _getLastTabbableElement ( rootElement ) ;
98
+ redirectToElement = this . _getLastTabbableElement ( this . _elementRef . nativeElement ) ;
76
99
}
77
100
78
101
if ( redirectToElement ) {
@@ -114,4 +137,11 @@ export class FocusTrap {
114
137
115
138
return null ;
116
139
}
140
+
141
+ private _createAnchor ( ) : HTMLElement {
142
+ let anchor = document . createElement ( 'div' ) ;
143
+ anchor . tabIndex = 0 ;
144
+ anchor . classList . add ( 'cdk-visually-hidden' ) ;
145
+ return anchor ;
146
+ }
117
147
}
0 commit comments