license const FOCUS_SENTINEL_CLASS = 'mdc-dom-focus-sentinel' ; /**
 * Utility to trap focus in a given root element, e.g. for modal components such
 * as dialogs. The root should have at least one focusable child element,
 * for setting initial focus when trapping focus.
 * Also tracks the previously focused element, and restores focus to that
 * element when releasing focus.
 */ export class FocusTrap { private elFocusedBeforeTrapFocus : HTMLElement | null = null ; constructor ( private readonly root , private readonly options = { } ) { } /**
     * Traps focus in `root`. Also focuses on either `initialFocusEl` if set;
     * otherwises sets initial focus to the first focusable child element.
     */ trapFocus ( ) { const focusableEls = this . getFocusableElements ( this . root ) ; if ( focusableEls . length === 0 ) { throw new Error ( 'FocusTrap: Element must have at least one focusable child.' ) ; } this . elFocusedBeforeTrapFocus = document . activeElement instanceof HTMLElement ? document . activeElement : null ; this . wrapTabFocus ( this . root ) ; if ( ! this . options . skipInitialFocus ) { this . focusInitialElement ( focusableEls , this . options . initialFocusEl ) ; } } /**
     * Releases focus from `root`. Also restores focus to the previously focused
     * element.
     */ releaseFocus ( ) { [ ] . slice . call ( this . root . querySelectorAll ( `.${ FOCUS_SENTINEL_CLASS }` ) ) . forEach ( ( sentinelEl ) => { sentinelEl . parentElement ! . removeChild ( sentinelEl ) ; } ) ; if ( ! this . options . skipRestoreFocus && this . elFocusedBeforeTrapFocus ) { this . elFocusedBeforeTrapFocus . focus ( ) ; } } /**
     * Wraps tab focus within `el` by adding two hidden sentinel divs which are
     * used to mark the beginning and the end of the tabbable region. When
     * focused, these sentinel elements redirect focus to the first/last
     * children elements of the tabbable region, ensuring that focus is trapped
     * within that region.
     */ private wrapTabFocus ( el ) { const sentinelStart = this . createSentinel ( ) ; const sentinelEnd = this . createSentinel ( ) ; sentinelStart . addEventListener ( 'focus' , ( ) => { const focusableEls = this . getFocusableElements ( el ) ; if ( focusableEls . length > 0 ) { focusableEls [ focusableEls . length - 1 ] . focus ( ) ; } } ) ; sentinelEnd . addEventListener ( 'focus' , ( ) => { const focusableEls = this . getFocusableElements ( el ) ; if ( focusableEls . length > 0 ) { focusableEls [ 0 ] . focus ( ) ; } } ) ; el . insertBefore ( sentinelStart , el . children [ 0 ] ) ; el . appendChild ( sentinelEnd ) ; } /**
     * Focuses on `initialFocusEl` if defined and a child of the root element.
     * Otherwise, focuses on the first focusable child element of the root.
     */ private focusInitialElement ( focusableEls , initialFocusEl ? ) { let focusIndex = 0 ; if ( initialFocusEl ) { focusIndex = Math . max ( focusableEls . indexOf ( initialFocusEl ) , 0 ) ; } focusableEls [ focusIndex ] . focus ( ) ; } private getFocusableElements ( root ) { const focusableEls = [ ] . slice . call ( root . querySelectorAll ( '[autofocus], [tabindex], a, input, textarea, select, button' ) ) as HTMLElement [ ] ; return focusableEls . filter ( ( el ) => { const isDisabledOrHidden = el . getAttribute ( 'aria-disabled' ) === 'true' || el . getAttribute ( 'disabled' ) != null || el . getAttribute ( 'hidden' ) != null || el . getAttribute ( 'aria-hidden' ) === 'true' ; const isTabbableAndVisible = el . tabIndex >= 0 && el . getBoundingClientRect ( ) . width > 0 && ! el . classList . contains ( FOCUS_SENTINEL_CLASS ) && ! isDisabledOrHidden ; let isProgrammaticallyHidden = false ; if ( isTabbableAndVisible ) { const style = getComputedStyle ( el ) ; isProgrammaticallyHidden = style . display === 'none' || style . visibility === 'hidden' ; } return isTabbableAndVisible && ! isProgrammaticallyHidden ; } ) ; } private createSentinel ( ) { const sentinel = document . createElement ( 'div' ) ; sentinel . setAttribute ( 'tabindex' , '0' ) ; sentinel . setAttribute ( 'aria-hidden' , 'true' ) ; sentinel . classList . add ( FOCUS_SENTINEL_CLASS ) ; return sentinel ; } } /** Customization options. */ export interface FocusOptions { initialFocusEl ? : HTMLElement ; skipInitialFocus ? : boolean ; skipRestoreFocus ? : boolean ; }