From 639c2ce077006e4289678783561528e07d457ba6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 19 Dec 2025 19:52:31 +0100 Subject: [PATCH] Add choose selector (#28624) * Add choose selector * add support for translation * pass required * Add to gallery --- gallery/src/pages/components/ha-selector.ts | 30 +++ .../ha-selector/ha-selector-choose.ts | 202 ++++++++++++++++++ src/components/ha-selector/ha-selector.ts | 1 + src/components/ha-service-control.ts | 1 + src/data/selector.ts | 8 + .../types/ha-automation-condition-platform.ts | 1 + .../types/ha-automation-trigger-platform.ts | 1 + 7 files changed, 244 insertions(+) create mode 100644 src/components/ha-selector/ha-selector-choose.ts diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 9eb13bdee2..2f0fedf1e4 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -40,6 +40,9 @@ const ENTITIES = [ getEntity("switch", "coffee", "off", { friendly_name: "Coffee", }), + getEntity("number", "number", 5, { + friendly_name: "Number", + }), ]; const DEVICES: DeviceRegistryEntry[] = [ @@ -377,6 +380,33 @@ const SCHEMAS: { name: "Constant", selector: { constant: { value: true, label: "Yes!" } }, }, + choose: { + name: "Choose", + selector: { + choose: { + choices: { + number: { + selector: { + number: { + min: 0, + max: 100, + step: 0.1, + }, + }, + }, + entity: { + selector: { + entity: { + filter: { + domain: "number", + }, + }, + }, + }, + }, + }, + }, + }, }, }, { diff --git a/src/components/ha-selector/ha-selector-choose.ts b/src/components/ha-selector/ha-selector-choose.ts new file mode 100644 index 0000000000..5b86712b74 --- /dev/null +++ b/src/components/ha-selector/ha-selector-choose.ts @@ -0,0 +1,202 @@ +import type { PropertyValues } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../common/dom/fire_event"; +import { isTemplate } from "../../common/string/has-template"; +import type { ChooseSelector, Selector } from "../../data/selector"; +import type { HomeAssistant } from "../../types"; +import "../ha-button-toggle-group"; +import "./ha-selector"; + +@customElement("ha-selector-choose") +export class HaChooseSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: ChooseSelector; + + @property() public value?: any; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ attribute: false }) + public localizeValue?: (key: string) => string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + @state() public _activeChoice?: string; + + protected willUpdate(changedProperties: PropertyValues): void { + if ( + changedProperties.has("selector") && + (!this._activeChoice || + !(this._activeChoice in this.selector.choose.choices)) + ) { + this._setActiveChoice(); + } + } + + protected render() { + if (!this._activeChoice) { + return nothing; + } + + const selector = this._selector(this._activeChoice); + const value = this._value(this._activeChoice); + + return html`
+ ${this.label}${this.required ? "*" : ""} + +
+ `; + } + + private _toggleButtons = memoizeOne( + (choices: ChooseSelector["choose"]["choices"], translationKey?: string) => + Object.keys(choices).map((choice) => ({ + label: + this.localizeValue && translationKey + ? this.localizeValue(`${translationKey}.choices.${choice}`) + : choice, + value: choice, + })) + ); + + private _choiceChanged(ev) { + ev.stopPropagation(); + const value = + typeof this.value === "object" + ? this.value + : { + [this._activeChoice!]: this.value, + }; + this._activeChoice = ev.detail?.value || ev.target.value; + fireEvent(this, "value-changed", { + value: { + ...value, + active_choice: this._activeChoice, + }, + }); + } + + private _handleValueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const value = typeof this.value === "object" ? this.value : {}; + fireEvent(this, "value-changed", { + value: { + ...value, + [this._activeChoice!]: ev.detail.value, + active_choice: this._activeChoice, + }, + }); + } + + private _selector(choice?: string): Selector { + const choices = this.selector.choose.choices; + + choice = choice || this.value?.active_choice; + + if (choice && choice in choices) { + return choices[choice].selector; + } + + return choices[Object.keys(choices)[0]].selector; + } + + private _value(choice?: string): any { + if (!this.value) { + return undefined; + } + return typeof this.value === "object" + ? this.value[choice || this.value.active_choice] + : this.value; + } + + private _setActiveChoice() { + if (this.value) { + if (typeof this.value === "object") { + if (this.value.active_choice in this.selector.choose.choices) { + this._activeChoice = this.value.active_choice; + return; + } + } else { + const typeofValue = typeof this.value; + const selectorTypes = Object.values(this.selector.choose.choices).map( + (choice) => Object.keys(choice.selector)[0] + ); + if (typeofValue === "number" && selectorTypes.includes("number")) { + this._activeChoice = Object.keys(this.selector.choose.choices)[ + selectorTypes.indexOf("number") + ]; + return; + } + if ( + typeofValue === "string" && + isTemplate(this.value) && + selectorTypes.includes("template") + ) { + this._activeChoice = Object.keys(this.selector.choose.choices)[ + selectorTypes.indexOf("template") + ]; + return; + } + if ( + typeofValue === "string" && + this.value.includes(".") && + selectorTypes.includes("entity") + ) { + this._activeChoice = Object.keys(this.selector.choose.choices)[ + selectorTypes.indexOf("entity") + ]; + return; + } + if (typeofValue === "string" && selectorTypes.includes("text")) { + this._activeChoice = Object.keys(this.selector.choose.choices)[ + selectorTypes.indexOf("text") + ]; + return; + } + } + } + this._activeChoice = Object.keys(this.selector.choose.choices)[0]; + } + + static styles = css` + .multi-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--ha-space-2); + } + ha-button-toggle-group { + display: block; + justify-self: end; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-choose": HaChooseSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 6fb5f56344..3487a6b21d 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -18,6 +18,7 @@ const LOAD_ELEMENTS = { attribute: () => import("./ha-selector-attribute"), assist_pipeline: () => import("./ha-selector-assist-pipeline"), boolean: () => import("./ha-selector-boolean"), + choose: () => import("./ha-selector-choose"), color_rgb: () => import("./ha-selector-color-rgb"), condition: () => import("./ha-selector-condition"), config_entry: () => import("./ha-selector-config-entry"), diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index 67733abf38..f5a51a6dbf 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -726,6 +726,7 @@ export class HaServiceControl extends LitElement { : undefined} .placeholder=${dataField.default} .localizeValue=${this._localizeValueCallback} + .required=${dataField.required} > ` : ""; diff --git a/src/data/selector.ts b/src/data/selector.ts index cf1398b1fd..4a662490e9 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -27,6 +27,7 @@ export type Selector = | AttributeSelector | BooleanSelector | ButtonToggleSelector + | ChooseSelector | ColorRGBSelector | ColorTempSelector | ConditionSelector @@ -116,6 +117,13 @@ export interface ButtonToggleSelector { } | null; } +export interface ChooseSelector { + choose: { + choices: Record; + translation_key?: string; + }; +} + export interface ColorRGBSelector { color_rgb: {} | null; } diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-platform.ts b/src/panels/config/automation/condition/types/ha-automation-condition-platform.ts index c1e15b3a16..d425048f18 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-platform.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-platform.ts @@ -256,6 +256,7 @@ export class HaPlatformCondition extends LitElement { : undefined} .placeholder=${dataField.default} .localizeValue=${this._localizeValueCallback} + .required=${dataField.required} > ` : nothing; diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-platform.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-platform.ts index 58b6287c73..c61ad3a704 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-platform.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-platform.ts @@ -292,6 +292,7 @@ export class HaPlatformTrigger extends LitElement { : undefined} .placeholder=${dataField.default} .localizeValue=${this._localizeValueCallback} + .required=${dataField.required} > ` : nothing;