import { LitElement, html, css } from "lit"; import { state, property, query, queryAssignedElements, } from "lit/decorators.js"; import { SlInput, SlMenu, SlMenuItem, SlPopup, SlTag, } from "@shoelace-style/shoelace"; import { dropdown } from "../utils/css"; /** * Input that opens a popup of autocomplete options * * Usage: * ```ts * ``` * * @event request-close */ export class Combobox extends LitElement { static styles = [ dropdown, css` :host { position: relative; z-index: 2; } `, ]; @property({ type: Boolean }) open = false; @state() isActive = true; @query("#dropdown") private dropdown?: HTMLDivElement; @query("sl-menu") private menu?: SlMenu; @query("sl-popup") private combobox?: SlPopup; @queryAssignedElements({ flatten: true, }) private anchor?: HTMLElement[]; @queryAssignedElements({ slot: "menu-item", selector: "sl-menu-item:not([disabled])", }) private menuItems?: SlMenuItem[]; protected willUpdate(changedProperties: Map) { if (changedProperties.has("open")) { if (this.open) { this.openDropdown(); } else { this.closeDropdown(); } } } render() { return html`
`; } private async onFocusout(e: FocusEvent) { const relatedTarget = e.relatedTarget as HTMLElement; if (!this.open) { return; } if ( !relatedTarget || (!this.anchor?.some((item) => item === relatedTarget) && !this.menuItems?.some((item) => item === relatedTarget)) ) { await this.updateComplete; this.dispatchEvent(new CustomEvent("request-close")); } } private onKeydown(e: KeyboardEvent) { if (this.open && e.key === "ArrowDown") { if (this.menu && this.menuItems?.length && !this.menu.getCurrentItem()) { // Focus on first menu item e.preventDefault(); const menuItem = this.menuItems[0]; this.menu!.setCurrentItem(menuItem); menuItem.focus(); } } } private async onKeyup(e: KeyboardEvent) { if (this.open && e.key === "Escape") { await this.updateComplete; this.dispatchEvent(new CustomEvent("request-close")); } } private async openDropdown() { this.isActive = true; await this.combobox?.updateComplete; // Manually sync dropdown width instead of using `sync="width"` // to get around ResizeObserver loop error if (this.anchor?.length && this.dropdown) { const anchorWidth = this.anchor[0].clientWidth; if (anchorWidth) { this.dropdown.style.width = `${anchorWidth}px`; } } this.dropdown?.classList.add("animateShow"); this.dropdown?.classList.remove("hidden"); } private closeDropdown() { this.dropdown?.classList.add("animateHide"); } }