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) {