diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index bf633e938c..baa3161ded 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -216,6 +216,9 @@ export class HaDevicePicker extends LitElement { .getItems=${this._getItems} .hideClearIcon=${this.hideClearIcon} .valueRenderer=${valueRenderer} + .unknownItemText=${this.hass.localize( + "ui.components.device-picker.unknown" + )} @value-changed=${this._valueChanged} > diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 36cc96415c..ce107026e9 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -288,10 +288,13 @@ export class HaEntityPicker extends LitElement { .hideClearIcon=${this.hideClearIcon} .searchFn=${this._searchFn} .valueRenderer=${this._valueRenderer} - @value-changed=${this._valueChanged} .addButtonLabel=${this.addButton ? this.hass.localize("ui.components.entity.entity-picker.add") : undefined} + .unknownItemText=${this.hass.localize( + "ui.components.entity.entity-picker.unknown" + )} + @value-changed=${this._valueChanged} > `; diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index 9462906287..b57bfc0f3b 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -475,6 +475,9 @@ export class HaStatisticPicker extends LitElement { .searchFn=${this._searchFn} .valueRenderer=${this._valueRenderer} .helper=${this.helper} + .unknownItemText=${this.hass.localize( + "ui.components.statistic-picker.unknown" + )} @value-changed=${this._valueChanged} > diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index e2b4ae6db9..fd0bcd77ce 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -379,6 +379,9 @@ export class HaAreaPicker extends LitElement { .getAdditionalItems=${this._getAdditionalItems} .valueRenderer=${valueRenderer} .addButtonLabel=${this.addButtonLabel} + .unknownItemText=${this.hass.localize( + "ui.components.area-picker.unknown" + )} @value-changed=${this._valueChanged} > diff --git a/src/components/ha-floor-picker.ts b/src/components/ha-floor-picker.ts index 88de1bc9eb..b57c254edc 100644 --- a/src/components/ha-floor-picker.ts +++ b/src/components/ha-floor-picker.ts @@ -393,6 +393,9 @@ export class HaFloorPicker extends LitElement { .getAdditionalItems=${this._getAdditionalItems} .valueRenderer=${valueRenderer} .rowRenderer=${this._rowRenderer} + .unknownItemText=${this.hass.localize( + "ui.components.floor-picker.unknown" + )} @value-changed=${this._valueChanged} > diff --git a/src/components/ha-generic-picker.ts b/src/components/ha-generic-picker.ts index 6f955cb7e2..a198ab49ff 100644 --- a/src/components/ha-generic-picker.ts +++ b/src/components/ha-generic-picker.ts @@ -4,6 +4,7 @@ import { mdiPlaylistPlus } from "@mdi/js"; import { css, html, LitElement, nothing, type CSSResultGroup } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; +import memoizeOne from "memoize-one"; import { tinykeys } from "tinykeys"; import { fireEvent } from "../common/dom/fire_event"; import type { HomeAssistant } from "../types"; @@ -46,8 +47,9 @@ export class HaGenericPicker extends LitElement { @property({ attribute: "hide-clear-icon", type: Boolean }) public hideClearIcon = false; + /** To prevent lags, getItems needs to be memoized */ @property({ attribute: false }) - public getItems?: ( + public getItems!: ( searchString?: string, section?: string ) => (PickerComboBoxItem | string)[]; @@ -107,6 +109,8 @@ export class HaGenericPicker extends LitElement { @property({ attribute: "selected-section" }) public selectedSection?: string; + @property({ attribute: "unknown-item-text" }) public unknownItemText?: string; + @query(".container") private _containerElement?: HTMLDivElement; @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox; @@ -156,6 +160,8 @@ export class HaGenericPicker extends LitElement { type="button" class=${this._opened ? "opened" : ""} compact + .unknown=${this._unknownValue(this.value, this.getItems())} + .unknownItemText=${this.unknownItemText} aria-label=${ifDefined(this.label)} @click=${this.open} @clear=${this._clear} @@ -233,6 +239,18 @@ export class HaGenericPicker extends LitElement { `; } + private _unknownValue = memoizeOne( + (value?: string, items?: (PickerComboBoxItem | string)[]) => { + if (value === undefined || !items) { + return false; + } + + return !items.some( + (item) => typeof item !== "string" && item.id === value + ); + } + ); + private _renderHelper() { return this.helper ? html` (PickerComboBoxItem | string)[]; @@ -264,11 +264,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { this.getAdditionalItems?.(searchString) || []; private _getItems = () => { - let items = [ - ...(this.getItems - ? this.getItems(this._search, this.selectedSection) - : []), - ]; + let items = [...this.getItems(this._search, this.selectedSection)]; if (!this.sections?.length) { items = items.sort((entityA, entityB) => diff --git a/src/components/ha-picker-field.ts b/src/components/ha-picker-field.ts index 37c32a27e2..2e93aed84a 100644 --- a/src/components/ha-picker-field.ts +++ b/src/components/ha-picker-field.ts @@ -1,3 +1,4 @@ +import { consume } from "@lit/context"; import { mdiClose, mdiMenuDown } from "@mdi/js"; import { css, @@ -7,8 +8,10 @@ import { type CSSResultGroup, type TemplateResult, } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; +import { localizeContext } from "../data/context"; +import type { HomeAssistant } from "../types"; import "./ha-combo-box-item"; import type { HaComboBoxItem } from "./ha-combo-box-item"; import "./ha-icon-button"; @@ -33,6 +36,10 @@ export class HaPickerField extends LitElement { @property() public placeholder?: string; + @property({ type: Boolean, reflect: true }) public unknown = false; + + @property({ attribute: "unknown-item-text" }) public unknownItemText?: string; + @property({ attribute: "hide-clear-icon", type: Boolean }) public hideClearIcon = false; @@ -41,6 +48,10 @@ export class HaPickerField extends LitElement { @query("ha-combo-box-item", true) public item!: HaComboBoxItem; + @state() + @consume({ context: localizeContext, subscribe: true }) + private localize!: HomeAssistant["localize"]; + public async focus() { await this.updateComplete; await this.item?.focus(); @@ -61,6 +72,12 @@ export class HaPickerField extends LitElement { ${this.placeholder} `} + ${this.unknown + ? html`
+ ${this.unknownItemText || + this.localize("ui.components.combo-box.unknown_item")} +
` + : nothing} ${showClearIcon ? html` diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts index f990ca0c0b..f0288104e2 100644 --- a/src/components/user/ha-user-picker.ts +++ b/src/components/user/ha-user-picker.ts @@ -134,6 +134,9 @@ class HaUserPicker extends LitElement { .getItems=${this._getItems} .valueRenderer=${this._valueRenderer} .rowRenderer=${this._rowRenderer} + .unknownItemText=${this.hass.localize( + "ui.components.user-picker.unknown" + )} @value-changed=${this._valueChanged} > diff --git a/src/panels/config/category/ha-category-picker.ts b/src/panels/config/category/ha-category-picker.ts index f5bb9d633b..25edfdd30e 100644 --- a/src/panels/config/category/ha-category-picker.ts +++ b/src/panels/config/category/ha-category-picker.ts @@ -203,6 +203,9 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { .getItems=${this._getItems} .getAdditionalItems=${this._getAdditionalItems} .valueRenderer=${valueRenderer} + .unknownItemText=${this.hass.localize( + "ui.components.category-picker.unknown" + )} @value-changed=${this._valueChanged} > diff --git a/src/translations/en.json b/src/translations/en.json index 7e3effd1ae..a1bdcb3b1b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -658,7 +658,8 @@ "show_entities": "Show entities", "new_entity": "Create a new entity", "placeholder": "Select an entity", - "create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n } helper." + "create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n } helper.", + "unknown": "Unknown entity selected" }, "entity-name-picker": { "types": { @@ -779,7 +780,8 @@ "user-picker": { "no_match": "No users found for {term}", "user": "User", - "add_user": "Add user" + "add_user": "Add user", + "unknown": "Unknown user selected" }, "blueprint-picker": { "select_blueprint": "Select a blueprint" @@ -793,7 +795,8 @@ "device": "Device", "unnamed_device": "Unnamed device", "no_area": "No area", - "placeholder": "Select a device" + "placeholder": "Select a device", + "unknown": "Unknown device selected" }, "category-picker": { "clear": "Clear", @@ -805,6 +808,7 @@ "add_new": "Add new category…", "no_categories": "No categories available", "no_match": "No categories found for {term}", + "unknown": "Unknown category selected", "add_dialog": { "title": "Add new category", "text": "Enter the name of the new category.", @@ -831,7 +835,8 @@ "add_new": "Add new area…", "no_areas": "No areas available", "no_match": "No areas found for {term}", - "failed_create_area": "Failed to create area." + "failed_create_area": "Failed to create area.", + "unknown": "Unknown area selected" }, "floor-picker": { "clear": "Clear", @@ -841,7 +846,8 @@ "add_new": "Add new floor…", "no_floors": "No floors available", "no_match": "No floors found for {term}", - "failed_create_floor": "Failed to create floor." + "failed_create_floor": "Failed to create floor.", + "unknown": "Unknown floor selected" }, "area-filter": { "title": "Areas", @@ -858,7 +864,8 @@ "no_match": "No statistics found for {term}", "no_state": "Entity without state", "missing_entity": "Why is my entity not listed?", - "learn_more": "Learn more about statistics" + "learn_more": "Learn more about statistics", + "unknown": "Unknown statistic selected" }, "addon-picker": { "addon": "Add-on", @@ -998,7 +1005,8 @@ }, "service-picker": { "action": "Action", - "no_match": "No matching actions found" + "no_match": "No matching actions found", + "unknown": "Unknown action selected" }, "service-control": { "required": "This field is required", @@ -1296,7 +1304,8 @@ }, "combo-box": { "no_match": "No matching items found", - "no_items": "No items available" + "no_items": "No items available", + "unknown_item": "Unknown item" }, "suggest_with_ai": { "label": "Suggest",