import { css, CSSResultGroup, html, LitElement, nothing, PropertyValues, TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { repeat } from "lit/directives/repeat"; import { fireEvent } from "../common/dom/fire_event"; import "./ha-icon"; import "./ha-svg-icon"; export type ControlSelectOption = { value: string; label?: string; icon?: TemplateResult; path?: string; }; @customElement("ha-control-select") export class HaControlSelect extends LitElement { @property({ type: Boolean, reflect: true }) disabled = false; @property({ attribute: false }) public options?: ControlSelectOption[]; @property() public value?: string; @property({ type: Boolean, reflect: true }) public vertical = false; @property({ type: Boolean, attribute: "hide-label" }) public hideLabel = false; @state() private _activeIndex?: number; protected firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); this.setAttribute("role", "listbox"); if (!this.hasAttribute("tabindex")) { this.setAttribute("tabindex", "0"); } } protected updated(changedProps: PropertyValues) { super.updated(changedProps); if (changedProps.has("_activeIndex")) { const activeValue = this._activeIndex != null ? this.options?.[this._activeIndex]?.value : undefined; const activedescendant = activeValue != null ? `option-${activeValue}` : undefined; this.setAttribute("aria-activedescendant", activedescendant ?? ""); } if (changedProps.has("vertical")) { const orientation = this.vertical ? "vertical" : "horizontal"; this.setAttribute("aria-orientation", orientation); } } public connectedCallback(): void { super.connectedCallback(); this._setupListeners(); } public disconnectedCallback(): void { super.disconnectedCallback(); this._destroyListeners(); } private _setupListeners() { this.addEventListener("focus", this._handleFocus); this.addEventListener("blur", this._handleBlur); this.addEventListener("keydown", this._handleKeydown); } private _destroyListeners() { this.removeEventListener("focus", this._handleFocus); this.removeEventListener("blur", this._handleBlur); this.removeEventListener("keydown", this._handleKeydown); } private _handleFocus() { if (this.disabled) return; this._activeIndex = (this.value != null ? this.options?.findIndex((option) => option.value === this.value) : undefined) ?? 0; } private _handleBlur() { this._activeIndex = undefined; } private _handleKeydown(ev: KeyboardEvent) { if (!this.options || this._activeIndex == null || this.disabled) return; const value = this.options[this._activeIndex].value; switch (ev.key) { case " ": this.value = value; fireEvent(this, "value-changed", { value }); break; case "ArrowUp": case "ArrowLeft": this._activeIndex = this._activeIndex <= 0 ? this.options.length - 1 : this._activeIndex - 1; break; case "ArrowDown": case "ArrowRight": this._activeIndex = (this._activeIndex + 1) % this.options.length; break; case "Home": this._activeIndex = 0; break; case "End": this._activeIndex = this.options.length - 1; break; default: return; } ev.preventDefault(); } private _handleOptionClick(ev: MouseEvent) { if (this.disabled) return; const value = (ev.target as any).value; this.value = value; fireEvent(this, "value-changed", { value }); } private _handleOptionMouseDown(ev: MouseEvent) { if (this.disabled) return; ev.preventDefault(); const value = (ev.target as any).value; this._activeIndex = this.options?.findIndex( (option) => option.value === value ); } private _handleOptionMouseUp(ev: MouseEvent) { ev.preventDefault(); this._activeIndex = undefined; } protected render() { return html`