1- import { Component , ViewEncapsulation , ViewChild , ElementRef , Input , NgZone } from '@angular/core' ;
1+ import { Directive , ElementRef , Input , NgZone , AfterViewInit , OnDestroy } from '@angular/core' ;
22import { InteractivityChecker } from './interactivity-checker' ;
33import { coerceBooleanProperty } from '../coercion/boolean-property' ;
44
@@ -11,48 +11,72 @@ import {coerceBooleanProperty} from '../coercion/boolean-property';
1111 * Things like tabIndex > 0, flex `order`, and shadow roots can cause to two to misalign.
1212 * This will be replaced with a more intelligent solution before the library is considered stable.
1313 */
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]' ,
1916} )
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 ( ) ;
2220
2321 /** Whether the focus trap is active. */
2422 @Input ( )
2523 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+ }
2728 private _disabled : boolean = false ;
2829
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+ }
3058
3159 /**
3260 * Waits for microtask queue to empty, then focuses the first tabbable element within the focus
3361 * trap region.
3462 */
3563 focusFirstTabbableElementWhenReady ( ) {
36- this . _ngZone . onMicrotaskEmpty . first ( ) . subscribe ( ( ) => {
37- this . focusFirstTabbableElement ( ) ;
38- } ) ;
64+ this . _ngZone . onMicrotaskEmpty . first ( ) . subscribe ( ( ) => this . focusFirstTabbableElement ( ) ) ;
3965 }
4066
4167 /**
4268 * Waits for microtask queue to empty, then focuses the last tabbable element within the focus
4369 * trap region.
4470 */
4571 focusLastTabbableElementWhenReady ( ) {
46- this . _ngZone . onMicrotaskEmpty . first ( ) . subscribe ( ( ) => {
47- this . focusLastTabbableElement ( ) ;
48- } ) ;
72+ this . _ngZone . onMicrotaskEmpty . first ( ) . subscribe ( ( ) => this . focusLastTabbableElement ( ) ) ;
4973 }
5074
5175 /**
5276 * Focuses the first tabbable element within the focus trap region.
5377 */
5478 focusFirstTabbableElement ( ) {
55- let rootElement = this . trappedContent . nativeElement ;
79+ let rootElement = this . _elementRef . nativeElement ;
5680 let redirectToElement = rootElement . querySelector ( '[cdk-focus-start]' ) as HTMLElement ||
5781 this . _getFirstTabbableElement ( rootElement ) ;
5882
@@ -65,14 +89,13 @@ export class FocusTrap {
6589 * Focuses the last tabbable element within the focus trap region.
6690 */
6791 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]' ) ;
7093 let redirectToElement : HTMLElement = null ;
7194
7295 if ( focusTargets . length ) {
7396 redirectToElement = focusTargets [ focusTargets . length - 1 ] as HTMLElement ;
7497 } else {
75- redirectToElement = this . _getLastTabbableElement ( rootElement ) ;
98+ redirectToElement = this . _getLastTabbableElement ( this . _elementRef . nativeElement ) ;
7699 }
77100
78101 if ( redirectToElement ) {
@@ -114,4 +137,11 @@ export class FocusTrap {
114137
115138 return null ;
116139 }
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+ }
117147}
0 commit comments