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 = `
`;
/**
* 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