license import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { DOCUMENT } from '@angular/common' ; import { AfterContentInit , Directive , ElementRef , Inject , Injectable , Input , NgZone , OnDestroy , DoCheck , isDevMode , } from '@angular/core' ; import { take } from 'rxjs/operators' ; import { InteractivityChecker } from '../interactivity-checker/interactivity-checker' ; /**
 * Class that allows for trapping focus within a DOM element.
 *
 * This class currently uses a relatively simple approach to focus trapping.
 * It assumes that the tab order is the same as DOM order, which is not necessarily true.
 * Things like `tabIndex > 0`, flex `order`, and shadow roots can cause to two to misalign.
 */ export class FocusTrap { private _startAnchor : HTMLElement | null ; private _endAnchor : HTMLElement | null ; private _hasAttached = false ; /** Whether the focus trap is active. */ get enabled ( ) : boolean { return this . _enabled ; } set enabled ( value ) { this . _enabled = value ; if ( this . _startAnchor && this . _endAnchor ) { this . _toggleAnchorTabIndex ( value , this . _startAnchor ) ; this . _toggleAnchorTabIndex ( value , this . _endAnchor ) ; } } private _enabled : boolean = true ; constructor ( private _element , private _checker , private _ngZone , private _document , deferAnchors = false ) { if ( ! deferAnchors ) { this . attachAnchors ( ) ; } } /** Destroys the focus trap by cleaning up the anchors. */ destroy ( ) { if ( this . _startAnchor && this . _startAnchor . parentNode ) { this . _startAnchor . parentNode . removeChild ( this . _startAnchor ) ; } if ( this . _endAnchor && this . _endAnchor . parentNode ) { this . _endAnchor . parentNode . removeChild ( this . _endAnchor ) ; } this . _startAnchor = this . _endAnchor = null ; } returns attachAnchors ( ) { if ( this . _hasAttached ) { return true ; } this . _ngZone . runOutsideAngular ( ( ) => { if ( ! this . _startAnchor ) { this . _startAnchor = this . _createAnchor ( ) ; this . _startAnchor ! . addEventListener ( 'focus' , ( ) => this . focusLastTabbableElement ( ) ) ; } if ( ! this . _endAnchor ) { this . _endAnchor = this . _createAnchor ( ) ; this . _endAnchor ! . addEventListener ( 'focus' , ( ) => this . focusFirstTabbableElement ( ) ) ; } } ) ; if ( this . _element . parentNode ) { this . _element . parentNode . insertBefore ( this . _startAnchor ! , this . _element ) ; this . _element . parentNode . insertBefore ( this . _endAnchor ! , this . _element . nextSibling ) ; this . _hasAttached = true ; } return this . _hasAttached ; } returns focusInitialElementWhenReady ( ) { return new Promise < boolean > ( resolve => { this . _executeOnStable ( ( ) => resolve ( this . focusInitialElement ( ) ) ) ; } ) ; } returns focusFirstTabbableElementWhenReady ( ) { return new Promise < boolean > ( resolve => { this . _executeOnStable ( ( ) => resolve ( this . focusFirstTabbableElement ( ) ) ) ; } ) ; } returns focusLastTabbableElementWhenReady ( ) { return new Promise < boolean > ( resolve => { this . _executeOnStable ( ( ) => resolve ( this . focusLastTabbableElement ( ) ) ) ; } ) ; } param bound returns private _getRegionBoundary ( bound ) { let markers = this . _element . querySelectorAll ( `[cdk-focus-region-${ bound }], ` + `[cdkFocusRegion${ bound }], ` + `[cdk-focus-${ bound }]` ) as NodeListOf < HTMLElement > ; for ( let i = 0 ; i < markers . length ; i ++ ) { if ( markers [ i ] . hasAttribute ( `cdk-focus-${ bound }` ) ) { console . warn ( `Found use of deprecated attribute 'cdk-focus-${ bound }', ` + `use 'cdkFocusRegion${ bound }' instead. The deprecated ` + `attribute will be removed in 8.0.0.` , markers [ i ] ) ; } else if ( markers [ i ] . hasAttribute ( `cdk-focus-region-${ bound }` ) ) { console . warn ( `Found use of deprecated attribute 'cdk-focus-region-${ bound }', ` + `use 'cdkFocusRegion${ bound }' instead. The deprecated attribute ` + `will be removed in 8.0.0.` , markers [ i ] ) ; } } if ( bound == 'start' ) { return markers . length ? markers [ 0 ] : this . _getFirstTabbableElement ( this . _element ) ; } return markers . length ? markers [ markers . length - 1 ] : this . _getLastTabbableElement ( this . _element ) ; } returns focusInitialElement ( ) { const redirectToElement = this . _element . querySelector ( `[cdk-focus-initial], ` + `[cdkFocusInitial]` ) as HTMLElement ; if ( redirectToElement ) { if ( redirectToElement . hasAttribute ( `cdk-focus-initial` ) ) { console . warn ( `Found use of deprecated attribute 'cdk-focus-initial', ` + `use 'cdkFocusInitial' instead. The deprecated attribute ` + `will be removed in 8.0.0` , redirectToElement ) ; } if ( isDevMode ( ) && ! this . _checker . isFocusable ( redirectToElement ) ) { console . warn ( `Element matching '[cdkFocusInitial]' is not focusable.` , redirectToElement ) ; } redirectToElement . focus ( ) ; return true ; } return this . focusFirstTabbableElement ( ) ; } returns focusFirstTabbableElement ( ) { const redirectToElement = this . _getRegionBoundary ( 'start' ) ; if ( redirectToElement ) { redirectToElement . focus ( ) ; } return ! ! redirectToElement ; } returns focusLastTabbableElement ( ) { const redirectToElement = this . _getRegionBoundary ( 'end' ) ; if ( redirectToElement ) { redirectToElement . focus ( ) ; } return ! ! redirectToElement ; } /**
     * Checks whether the focus trap has successfuly been attached.
     */ hasAttached ( ) { return this . _hasAttached ; } /** Get the first tabbable element from a DOM subtree (inclusive). */ private _getFirstTabbableElement ( root ) { if ( this . _checker . isFocusable ( root ) && this . _checker . isTabbable ( root ) ) { return root ; } let children = root . children || root . childNodes ; for ( let i = 0 ; i < children . length ; i ++ ) { let tabbableChild = children [ i ] . nodeType === this . _document . ELEMENT_NODE ? this . _getFirstTabbableElement ( children [ i ] as HTMLElement ) : null ; if ( tabbableChild ) { return tabbableChild ; } } return null ; } /** Get the last tabbable element from a DOM subtree (inclusive). */ private _getLastTabbableElement ( root ) { if ( this . _checker . isFocusable ( root ) && this . _checker . isTabbable ( root ) ) { return root ; } let children = root . children || root . childNodes ; for ( let i = children . length - 1 ; i >= 0 ; i -- ) { let tabbableChild = children [ i ] . nodeType === this . _document . ELEMENT_NODE ? this . _getLastTabbableElement ( children [ i ] as HTMLElement ) : null ; if ( tabbableChild ) { return tabbableChild ; } } return null ; } /** Creates an anchor element. */ private _createAnchor ( ) { const anchor = this . _document . createElement ( 'div' ) ; this . _toggleAnchorTabIndex ( this . _enabled , anchor ) ; anchor . classList . add ( 'cdk-visually-hidden' ) ; anchor . classList . add ( 'cdk-focus-trap-anchor' ) ; return anchor ; } param isEnabled param anchor private _toggleAnchorTabIndex ( isEnabled , anchor ) { isEnabled ? anchor . setAttribute ( 'tabindex' , '0' ) : anchor . removeAttribute ( 'tabindex' ) ; } /** Executes a function when the zone is stable. */ private _executeOnStable ( fn ) { if ( this . _ngZone . isStable ) { fn ( ) ; } else { this . _ngZone . onStable . asObservable ( ) . pipe ( take ( 1 ) ) . subscribe ( fn ) ; } } } /** Factory that allows easy instantiation of focus traps. */ @ Injectable ( { providedIn : 'root' } ) export class FocusTrapFactory { private _document : Document ; constructor ( private _checker , private _ngZone , @ Inject ( DOCUMENT ) _document ) { this . _document = _document ; } param element param deferCaptureElements returns create ( element , deferCaptureElements = false ) { return new FocusTrap ( element , this . _checker , this . _ngZone , this . _document , deferCaptureElements ) ; } } /** Directive for trapping focus within a region. */ @ Directive ( { selector : '[cdkTrapFocus]' , exportAs : 'cdkTrapFocus' , } ) export class CdkTrapFocus implements OnDestroy , AfterContentInit , DoCheck { private _document : Document ; /** Underlying FocusTrap instance. */ focusTrap : FocusTrap ; /** Previously focused element to restore focus to upon destroy when using autoCapture. */ private _previouslyFocusedElement : HTMLElement | null = null ; /** Whether the focus trap is active. */ @ Input ( 'cdkTrapFocus' ) get enabled ( ) : boolean { return this . focusTrap . enabled ; } set enabled ( value ) { this . focusTrap . enabled = coerceBooleanProperty ( value ) ; } /**
     * Whether the directive should automatially move focus into the trapped region upon
     * initialization and return focus to the previous activeElement upon destruction.
     */ @ Input ( 'cdkTrapFocusAutoCapture' ) get autoCapture ( ) : boolean { return this . _autoCapture ; } set autoCapture ( value ) { this . _autoCapture = coerceBooleanProperty ( value ) ; } private _autoCapture : boolean ; constructor ( private _elementRef , private _focusTrapFactory , @ Inject ( DOCUMENT ) _document ) { this . _document = _document ; this . focusTrap = this . _focusTrapFactory . create ( this . _elementRef . nativeElement , true ) ; } ngOnDestroy ( ) { this . focusTrap . destroy ( ) ; if ( this . _previouslyFocusedElement ) { this . _previouslyFocusedElement . focus ( ) ; this . _previouslyFocusedElement = null ; } } ngAfterContentInit ( ) { this . focusTrap . attachAnchors ( ) ; if ( this . autoCapture ) { this . _previouslyFocusedElement = this . _document . activeElement as HTMLElement ; this . focusTrap . focusInitialElementWhenReady ( ) ; } } ngDoCheck ( ) { if ( ! this . focusTrap . hasAttached ( ) ) { this . focusTrap . attachAnchors ( ) ; } } }