| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 | import { debounce } from "./debounce";import { isFocusable, isHidden } from "./focusable";import { queryShadowRoot } from "./shadow";/** * 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>`;/** * Focus trap web component. * @customElement focus-trap * @slot - Default content. */export class FocusTrap extends HTMLElement {    /**     * Attaches the shadow root.     */    constructor() {        super();        // The debounce id is used to distinguish this focus trap from others when debouncing        this.debounceId = Math.random().toString();        this._focused = false;        const shadow = this.attachShadow({ mode: "open" });        shadow.appendChild(template.content.cloneNode(true));        this.$backup = shadow.querySelector("#backup");        this.$start = shadow.querySelector("#start");        this.$end = shadow.querySelector("#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);    }    // Whenever one of these attributes changes we need to render the template again.    static get observedAttributes() {        return [            "inactive"        ];    }    /**     * Determines whether the focus trap is active or not.     * @attr     */    get inactive() {        return this.hasAttribute("inactive");    }    set inactive(value) {        value ? this.setAttribute("inactive", "") : this.removeAttribute("inactive");    }    /**     * Returns whether the element currently has focus.     */    get focused() {        return this._focused;    }    /**     * Hooks up the element.     */    connectedCallback() {        this.$start.addEventListener("focus", this.focusLastElement);        this.$end.addEventListener("focus", this.focusFirstElement);        // Focus out is called every time the user tabs around inside the element        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);    }    /**     * Focuses on either the last or first focusable element.     * @param {boolean} trapToEnd     */    trapFocus(trapToEnd) {        if (this.inactive)            return;        let focusableChildren = this.getFocusableElements();        if (focusableChildren.length > 0) {            if (trapToEnd) {                focusableChildren[focusableChildren.length - 1].focus();            }            else {                focusableChildren[0].focus();            }            this.$backup.setAttribute("tabindex", "-1");        }        else {            // If there are no focusable children we need to focus on the backup            // to trap the focus. This is a useful behavior if the focus trap is            // for example used in a dialog and we don't want the user to tab            // outside the dialog even though there are no focusable children            // in the dialog.            this.$backup.setAttribute("tabindex", "0");            this.$backup.focus();        }    }    /**     * When the element gains focus this function is called.     */    onFocusIn() {        this.updateFocused(true);    }    /**     * When the element looses its focus this function is called.     */    onFocusOut() {        this.updateFocused(false);    }    /**     * Updates the focused property and updates the view.     * The update is debounced because the focusin and focusout out     * might fire multiple times in a row. We only want to render     * the element once, therefore waiting until the focus is "stable".     * @param value     */    updateFocused(value) {        debounce(() => {            if (this.focused !== value) {                this._focused = value;                this.render();            }        }, 0, this.debounceId);    }    /**     * Updates the template.     */    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);//# sourceMappingURL=focus-trap.js.map
 |