diff --git a/src/components/ha-label-picker.ts b/src/components/ha-label-picker.ts index a5360cc34a..4fdf3dc6f9 100644 --- a/src/components/ha-label-picker.ts +++ b/src/components/ha-label-picker.ts @@ -116,23 +116,26 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { } ); - private _valueRenderer: PickerValueRenderer = (value) => { - const label = this._labelMap(this._labels).get(value); + private _computeValueRenderer = memoizeOne( + (labels: LabelRegistryEntry[] | undefined): PickerValueRenderer => + (value) => { + const label = this._labelMap(labels).get(value); - if (!label) { - return html` - - ${value} - `; - } + if (!label) { + return html` + + ${value} + `; + } - return html` - ${label.icon - ? html`` - : html``} - ${label.name} - `; - }; + return html` + ${label.icon + ? html`` + : html``} + ${label.name} + `; + } + ); private _getLabels = memoizeOne( ( @@ -388,6 +391,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { this.placeholder ?? this.hass.localize("ui.components.label-picker.label"); + const valueRenderer = this._computeValueRenderer(this._labels); + return html` diff --git a/src/panels/config/category/ha-category-picker.ts b/src/panels/config/category/ha-category-picker.ts index 9d2247a7af..b4d987b3cb 100644 --- a/src/panels/config/category/ha-category-picker.ts +++ b/src/panels/config/category/ha-category-picker.ts @@ -1,17 +1,14 @@ -import { mdiTag } from "@mdi/js"; -import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; +import { mdiTag, mdiPlus } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; -import type { PropertyValues } from "lit"; -import { html, LitElement, nothing } from "lit"; +import type { TemplateResult } from "lit"; +import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; -import type { ScorableTextItem } from "../../../common/string/filter/sequence-matching"; -import { fuzzyFilterSort } from "../../../common/string/filter/sequence-matching"; -import "../../../components/ha-combo-box"; -import type { HaComboBox } from "../../../components/ha-combo-box"; -import "../../../components/ha-combo-box-item"; -import "../../../components/ha-icon-button"; +import "../../../components/ha-generic-picker"; +import type { HaGenericPicker } from "../../../components/ha-generic-picker"; +import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box"; +import type { PickerValueRenderer } from "../../../components/ha-picker-field"; import "../../../components/ha-svg-icon"; import type { CategoryRegistryEntry } from "../../../data/category_registry"; import { @@ -22,20 +19,8 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail"; -type ScorableCategoryRegistryEntry = ScorableTextItem & CategoryRegistryEntry; - const ADD_NEW_ID = "___ADD_NEW___"; const NO_CATEGORIES_ID = "___NO_CATEGORIES___"; -const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___"; - -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - ${item.icon - ? html`` - : html``} - ${item.name} - -`; @customElement("ha-category-picker") export class HaCategoryPicker extends SubscribeMixin(LitElement) { @@ -58,14 +43,17 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public required = false; - @state() private _opened?: boolean; - @state() private _categories?: CategoryRegistryEntry[]; - @query("ha-combo-box", true) public comboBox!: HaComboBox; + @query("ha-generic-picker") private _picker?: HaGenericPicker; protected hassSubscribeRequiredHostProps = ["scope"]; + public async open() { + await this.updateComplete; + await this._picker?.open(); + } + protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { return [ subscribeCategoryRegistry( @@ -78,186 +66,185 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { ]; } - private _suggestion?: string; - - private _init = false; - - public async open() { - await this.updateComplete; - await this.comboBox?.open(); - } - - public async focus() { - await this.updateComplete; - await this.comboBox?.focus(); - } - - private _getCategories = memoizeOne( + private _categoryMap = memoizeOne( ( - categories: CategoryRegistryEntry[] | undefined, - noAdd: this["noAdd"] - ): CategoryRegistryEntry[] => { - const result = categories ? [...categories] : []; - if (!result?.length) { - result.push({ - category_id: NO_CATEGORIES_ID, - name: this.hass.localize( - "ui.components.category-picker.no_categories" - ), - icon: null, - }); + categories: CategoryRegistryEntry[] | undefined + ): Map => { + if (!categories) { + return new Map(); } - - return noAdd - ? result - : [ - ...result, - { - category_id: ADD_NEW_ID, - name: this.hass.localize("ui.components.category-picker.add_new"), - icon: "mdi:plus", - }, - ]; + return new Map( + categories.map((category) => [category.category_id, category]) + ); } ); - protected updated(changedProps: PropertyValues) { - if ( - (!this._init && this.hass && this._categories) || - (this._init && changedProps.has("_opened") && this._opened) - ) { - this._init = true; - const categories = this._getCategories(this._categories, this.noAdd).map( - (label) => ({ - ...label, - strings: [label.name], - }) - ); - this.comboBox.items = categories; - this.comboBox.filteredItems = categories; - } - } + private _computeValueRenderer = memoizeOne( + (categories: CategoryRegistryEntry[] | undefined): PickerValueRenderer => + (value) => { + const category = this._categoryMap(categories).get(value); - protected render() { - if (!this._categories) { - return nothing; - } - return html` - - - `; - } + if (!category) { + return html` + + ${value} + `; + } - private _filterChanged(ev: CustomEvent): void { - const target = ev.target as HaComboBox; - const filterString = ev.detail.value; - if (!filterString) { - this.comboBox.filteredItems = this.comboBox.items; - return; - } + return html` + ${category.icon + ? html`` + : html``} + ${category.name} + `; + } + ); - const filteredItems = fuzzyFilterSort( - filterString, - target.items?.filter( - (item) => ![NO_CATEGORIES_ID, ADD_NEW_ID].includes(item.category_id) - ) || [] - ); - if (filteredItems?.length === 0) { - if (this.noAdd) { - this.comboBox.filteredItems = [ + private _getCategories = memoizeOne( + (categories: CategoryRegistryEntry[] | undefined): PickerComboBoxItem[] => { + if (!categories || categories.length === 0) { + return [ { - category_id: NO_CATEGORIES_ID, - name: this.hass.localize("ui.components.category-picker.no_match"), - icon: null, - }, - ] as ScorableCategoryRegistryEntry[]; - } else { - this._suggestion = filterString; - this.comboBox.filteredItems = [ - { - category_id: ADD_NEW_SUGGESTION_ID, - name: this.hass.localize( - "ui.components.category-picker.add_new_sugestion", - { name: this._suggestion } + id: NO_CATEGORIES_ID, + primary: this.hass.localize( + "ui.components.category-picker.no_categories" ), - icon: "mdi:plus", + icon_path: mdiTag, }, ]; } - } else { - this.comboBox.filteredItems = filteredItems; + + const items = categories.map((category) => ({ + id: category.category_id, + primary: category.name, + icon: category.icon || undefined, + icon_path: category.icon ? undefined : mdiTag, + sorting_label: category.name, + search_labels: [category.name, category.category_id].filter( + (v): v is string => Boolean(v) + ), + })); + + return items; } - } + ); - private get _value() { - return this.value || ""; - } + private _getItems = () => this._getCategories(this._categories); - private _openedChanged(ev: ValueChangedEvent) { - this._opened = ev.detail.value; - } - - private _categoryChanged(ev: ValueChangedEvent) { - ev.stopPropagation(); - let newValue = ev.detail.value; - - if (newValue === NO_CATEGORIES_ID) { - newValue = ""; - this.comboBox.setInputValue(""); - return; - } - - if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) { - if (newValue !== this._value) { - this._setValue(newValue); + private _allCategoryNames = memoizeOne( + (categories?: CategoryRegistryEntry[]) => { + if (!categories) { + return []; } + return [ + ...new Set( + categories + .map((category) => category.name.toLowerCase()) + .filter(Boolean) as string[] + ), + ]; + } + ); + + private _getAdditionalItems = ( + searchString?: string + ): PickerComboBoxItem[] => { + if (this.noAdd) { + return []; + } + + const allCategoryNames = this._allCategoryNames(this._categories); + + if ( + searchString && + !allCategoryNames.includes(searchString.toLowerCase()) + ) { + return [ + { + id: ADD_NEW_ID + searchString, + primary: this.hass.localize( + "ui.components.category-picker.add_new_sugestion", + { + name: searchString, + } + ), + icon_path: mdiPlus, + }, + ]; + } + + return [ + { + id: ADD_NEW_ID, + primary: this.hass.localize("ui.components.category-picker.add_new"), + icon_path: mdiPlus, + }, + ]; + }; + + protected render(): TemplateResult { + const placeholder = + this.placeholder ?? + this.hass.localize("ui.components.category-picker.category"); + + const valueRenderer = this._computeValueRenderer(this._categories); + + return html` + + + `; + } + + private _valueChanged(ev: ValueChangedEvent) { + ev.stopPropagation(); + + const value = ev.detail.value; + + if (value === NO_CATEGORIES_ID) { return; } - (ev.target as any).value = this._value; + if (!value) { + this._setValue(undefined); + return; + } - this.hass.loadFragmentTranslation("config"); + if (value.startsWith(ADD_NEW_ID)) { + this.hass.loadFragmentTranslation("config"); - showCategoryRegistryDetailDialog(this, { - scope: this.scope!, - suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", - createEntry: async (values) => { - const category = await createCategoryRegistryEntry( - this.hass, - this.scope!, - values - ); - this._categories = [...this._categories!, category]; - this.comboBox.filteredItems = this._getCategories( - this._categories, - this.noAdd - ); - await this.updateComplete; - await this.comboBox.updateComplete; - this._setValue(category.category_id); - return category; - }, - }); + const suggestedName = value.substring(ADD_NEW_ID.length); - this._suggestion = undefined; - this.comboBox.setInputValue(""); + showCategoryRegistryDetailDialog(this, { + scope: this.scope!, + suggestedName: suggestedName, + createEntry: async (values) => { + const category = await createCategoryRegistryEntry( + this.hass, + this.scope!, + values + ); + this._setValue(category.category_id); + return category; + }, + }); + + return; + } + + this._setValue(value); } private _setValue(value?: string) {