import { debounce } from "./debounce" ; import { isFocusable , isHidden } from "./focusable" ; import { queryShadowRoot } from "./shadow" ; export interface IFocusTrap { inactive : boolean ; readonly focused : boolean ; focusFirstElement : ( ( ) => void ) ; focusLastElement : ( ( ) => void ) ; getFocusableElements : ( ( ) => HTMLElement [ ] ) ; } /**
 * Template for the focus trap.
 */ const template = document . createElement ( "template" ) ; template . innerHTML = `
	<div id="start"></div>
	<div id="backup"></div>
	<slot></slot>
	<div id="end"></div>
` ; customElement slot export class FocusTrap extends HTMLElement implements IFocusTrap { static get observedAttributes ( ) { return [ "inactive" ] ; } attr get inactive ( ) { return this . hasAttribute ( "inactive" ) ; } set inactive ( value ) { value ? this . setAttribute ( "inactive" , "" ) : this . removeAttribute ( "inactive" ) ; } private $backup ! : HTMLElement ; private debounceId = Math . random ( ) . toString ( ) ; private $start ! : HTMLElement ; private $end ! : HTMLElement ; private _focused = false ; /**
     * Returns whether the element currently has focus.
     */ get focused ( ) : boolean { return this . _focused ; } /**
     * Attaches the shadow root.
     */ constructor ( ) { super ( ) ; const shadow = this . attachShadow ( { mode : "open" } ) ; shadow . appendChild ( template . content . cloneNode ( true ) ) ; this . $backup = shadow . querySelector < HTMLElement > ( "#backup" ) ! ; this . $start = shadow . querySelector < HTMLElement > ( "#start" ) ! ; this . $end = shadow . querySelector < HTMLElement > ( "#end" ) ! ; this . focusLastElement = this . focusLastElement . bind ( this ) ; this . focusFirstElement = this . focusFirstElement . bind ( this ) ; this . onFocusIn = this . onFocusIn . bind ( this ) ; this . onFocusOut = this . onFocusOut . bind ( this ) ; } /**
     * Hooks up the element.
     */ connectedCallback ( ) { this . $start . addEventListener ( "focus" , this . focusLastElement ) ; this . $end . addEventListener ( "focus" , this . focusFirstElement ) ; this . addEventListener ( "focusin" , this . onFocusIn ) ; this . addEventListener ( "focusout" , this . onFocusOut ) ; this . render ( ) ; } /**
     * Tears down the element.
     */ disconnectedCallback ( ) { this . $start . removeEventListener ( "focus" , this . focusLastElement ) ; this . $end . removeEventListener ( "focus" , this . focusFirstElement ) ; this . removeEventListener ( "focusin" , this . onFocusIn ) ; this . removeEventListener ( "focusout" , this . onFocusOut ) ; } /**
     * When the attributes changes we need to re-render the template.
     */ attributeChangedCallback ( ) { this . render ( ) ; } /**
     * Focuses the first focusable element in the focus trap.
     */ focusFirstElement ( ) { this . trapFocus ( ) ; } /**
     * Focuses the last focusable element in the focus trap.
     */ focusLastElement ( ) { this . trapFocus ( true ) ; } /**
     * Returns a list of the focusable children found within the element.
     */ getFocusableElements ( ) { return queryShadowRoot ( this , isHidden , isFocusable ) ; } param { boolean } trapToEnd protected trapFocus ( trapToEnd ? ) { if ( this . inactive ) return ; let focusableElements = this . getFocusableElements ( ) ; if ( focusableElements . length > 0 ) { if ( trapToEnd ) { focusableElements [ focusableElements . length - 1 ] . focus ( ) ; } else { focusableElements [ 0 ] . focus ( ) ; } this . $backup . setAttribute ( "tabindex" , "-1" ) ; } else { this . $backup . setAttribute ( "tabindex" , "0" ) ; this . $backup . focus ( ) ; } } /**
     * When the element gains focus this function is called.
     */ private onFocusIn ( ) { this . updateFocused ( true ) ; } /**
     * When the element looses its focus this function is called.
     */ private onFocusOut ( ) { this . updateFocused ( false ) ; } param value private updateFocused ( value ) { debounce ( ( ) => { if ( this . focused !== value ) { this . _focused = value ; this . render ( ) ; } } , 0 , this . debounceId ) ; } /**
     * Updates the template.
     */ protected render ( ) { this . $start . setAttribute ( "tabindex" , ! this . focused || this . inactive ? `-1` : `0` ) ; this . $end . setAttribute ( "tabindex" , ! this . focused || this . inactive ? `-1` : `0` ) ; this . focused ? this . setAttribute ( "focused" , "" ) : this . removeAttribute ( "focused" ) ; } } window . customElements . define ( "focus-trap" , FocusTrap ) ;