Compare commits

...

3 Commits

Author SHA1 Message Date
Wendelin
bfabbf626e Fix secondary title 2025-12-17 14:49:54 +01:00
Wendelin
0e149c35a5 Review 2025-12-17 09:20:14 +01:00
Wendelin
c90fa341bb Introduce allowCustomValue and remove usage 2025-12-16 17:24:34 +01:00
21 changed files with 123 additions and 160 deletions

View File

@@ -61,7 +61,6 @@ class HaDevicesPicker extends LitElement {
(entityId) => html`
<div>
<ha-device-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.deviceFilter=${this.deviceFilter}
@@ -79,7 +78,6 @@ class HaDevicesPicker extends LitElement {
)}
<div>
<ha-device-picker
allow-custom-entity
.hass=${this.hass}
.helper=${this.helper}
.deviceFilter=${this.deviceFilter}

View File

@@ -99,7 +99,6 @@ class HaEntitiesPicker extends LitElement {
(entityId) => html`
<div class="entity">
<ha-entity-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.includeDomains=${this.includeDomains}
@@ -129,7 +128,6 @@ class HaEntitiesPicker extends LitElement {
</ha-sortable>
<div>
<ha-entity-picker
allow-custom-entity
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}

View File

@@ -118,6 +118,9 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
@property({ attribute: "custom-value-label" })
public customValueLabel?: string;
@query(".container") private _containerElement?: HTMLDivElement;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
@@ -167,7 +170,11 @@ export class HaGenericPicker extends LitElement {
type="button"
class=${this._opened ? "opened" : ""}
compact
.unknown=${this._unknownValue(this.value, this.getItems())}
.unknown=${this._unknownValue(
this.allowCustomValue,
this.value,
this.getItems()
)}
.unknownItemText=${this.unknownItemText}
aria-label=${ifDefined(this.label)}
@click=${this.open}
@@ -244,13 +251,24 @@ export class HaGenericPicker extends LitElement {
.sectionTitleFunction=${this.sectionTitleFunction}
.selectedSection=${this.selectedSection}
.searchKeys=${this.searchKeys}
.customValueLabel=${this.customValueLabel}
></ha-picker-combo-box>
`;
}
private _unknownValue = memoizeOne(
(value?: string, items?: (PickerComboBoxItem | string)[]) => {
if (value === undefined || value === null || value === "" || !items) {
(
allowCustomValue: boolean,
value?: string,
items?: (PickerComboBoxItem | string)[]
) => {
if (
allowCustomValue ||
value === undefined ||
value === null ||
value === "" ||
!items
) {
return false;
}

View File

@@ -1,55 +1,17 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { titleCase } from "../common/string/title-case";
import { fetchConfig } from "../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
import { getPanelIcon, getPanelTitle } from "../data/panel";
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-generic-picker";
import "./ha-icon";
interface NavigationItem {
path: string;
icon: string;
title: string;
}
const DEFAULT_ITEMS: NavigationItem[] = [];
const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html`
<ha-combo-box-item type="button">
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
<span slot="headline">${item.title || item.path}</span>
${item.title
? html`<span slot="supporting-text">${item.path}</span>`
: nothing}
</ha-combo-box-item>
`;
const createViewNavigationItem = (
prefix: string,
view: LovelaceViewRawConfig,
index: number
) => ({
path: `/${prefix}/${view.path ?? index}`,
icon: view.icon ?? "mdi:view-compact",
title: view.title ?? (view.path ? titleCase(view.path) : `${index}`),
});
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
path: `/${panel.url_path}`,
icon: getPanelIcon(panel) || "mdi:view-dashboard",
title: getPanelTitle(hass, panel) || "",
});
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
@customElement("ha-navigation-picker")
export class HaNavigationPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -61,46 +23,51 @@ export class HaNavigationPicker extends LitElement {
@property({ type: Boolean }) public required = false;
@state() private _opened = false;
@state() private _loading = true;
private navigationItemsLoaded = false;
protected firstUpdated() {
this._loadNavigationItems();
}
private navigationItems: NavigationItem[] = DEFAULT_ITEMS;
private _navigationItems: PickerComboBoxItem[] = [];
@query("ha-combo-box", true) private comboBox!: HaComboBox;
protected render(): TemplateResult {
protected render() {
return html`
<ha-combo-box
<ha-generic-picker
.hass=${this.hass}
item-value-path="path"
item-label-path="path"
.value=${this._value}
.value=${this._loading ? undefined : this.value}
allow-custom-value
.filteredItems=${this.navigationItems}
.label=${this.label}
.placeholder=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.disabled=${this._loading || this.disabled}
.required=${this.required}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
.getItems=${this._getItems}
.valueRenderer=${this._valueRenderer}
.customValueLabel=${this.hass.localize(
"ui.components.navigation-picker.add_custom_path"
)}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
</ha-generic-picker>
`;
}
private async _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
if (this._opened && !this.navigationItemsLoaded) {
this._loadNavigationItems();
}
}
private _valueRenderer = (itemId: string) => {
const item = this._navigationItems.find((navItem) => navItem.id === itemId);
return html`
${item?.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: nothing}
<span slot="headline">${item?.primary || itemId}</span>
${item?.primary
? html`<span slot="supporting-text">${itemId}</span>`
: nothing}
`;
};
private _getItems = () => this._navigationItems;
private async _loadNavigationItems() {
this.navigationItemsLoaded = true;
const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({
id,
...panel,
@@ -124,27 +91,47 @@ export class HaNavigationPicker extends LitElement {
const panelViewConfig = new Map(viewConfigs);
this.navigationItems = [];
this._navigationItems = [];
for (const panel of panels) {
this.navigationItems.push(createPanelNavigationItem(this.hass!, panel));
const path = `/${panel.url_path}`;
const panelTitle = getPanelTitle(this.hass, panel);
const primary = panelTitle || path;
this._navigationItems.push({
id: path,
primary,
secondary: panelTitle ? path : undefined,
icon: getPanelIcon(panel) || "mdi:view-dashboard",
sorting_label: [
primary.startsWith("/") ? `zzz${primary}` : primary,
path,
]
.filter(Boolean)
.join("_"),
});
const config = panelViewConfig.get(panel.id);
if (!config || !("views" in config)) continue;
config.views.forEach((view, index) =>
this.navigationItems.push(
createViewNavigationItem(panel.url_path, view, index)
)
);
config.views.forEach((view, index) => {
const viewPath = `/${panel.url_path}/${view.path ?? index}`;
const viewPrimary =
view.title ?? (view.path ? titleCase(view.path) : `${index}`);
this._navigationItems.push({
id: viewPath,
secondary: viewPath,
icon: view.icon ?? "mdi:view-compact",
primary: viewPrimary,
sorting_label: [
viewPrimary.startsWith("/") ? `zzz${viewPrimary}` : viewPrimary,
viewPath,
].join("_"),
});
});
}
this.comboBox.filteredItems = this.navigationItems;
}
protected shouldUpdate(changedProps: PropertyValues) {
return !this._opened || changedProps.has("_opened");
this._loading = false;
}
private _valueChanged(ev: ValueChangedEvent<string>) {
@@ -152,61 +139,18 @@ export class HaNavigationPicker extends LitElement {
this._setValue(ev.detail.value);
}
private _setValue(value: string) {
private _setValue(value = "") {
this.value = value;
fireEvent(
this,
"value-changed",
{ value: this._value },
{ value: this.value },
{
bubbles: false,
composed: false,
}
);
}
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
const characterCount = filterString.length;
if (characterCount >= 2) {
const filteredItems: NavigationItem[] = [];
this.navigationItems.forEach((item) => {
if (
item.path.toLowerCase().includes(filterString) ||
item.title.toLowerCase().includes(filterString)
) {
filteredItems.push(item);
}
});
if (filteredItems.length > 0) {
this.comboBox.filteredItems = filteredItems;
} else {
this.comboBox.filteredItems = [];
}
} else {
this.comboBox.filteredItems = this.navigationItems;
}
}
private get _value() {
return this.value || "";
}
static styles = css`
ha-icon,
ha-svg-icon {
color: var(--primary-text-color);
position: relative;
bottom: 0px;
}
*[slot="prefix"] {
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
}
`;
}
declare global {

View File

@@ -1,6 +1,6 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiMagnify, mdiMinusBoxOutline } from "@mdi/js";
import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit";
import {
@@ -91,6 +91,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@property({ attribute: "custom-value-label" })
public customValueLabel?: string;
@property() public label?: string;
@property() public value?: string;
@@ -187,10 +190,15 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}
protected render() {
const searchLabel =
this.label ??
(this.allowCustomValue
? (this.hass?.localize("ui.components.combo-box.search_or_custom") ??
"Search | Add custom value")
: (this.hass?.localize("ui.common.search") ?? "Search"));
return html`<ha-textfield
.label=${this.label ??
this.hass?.localize("ui.common.search") ??
"Search"}
.label=${searchLabel}
@input=${this._filterChanged}
></ha-textfield>
${this._renderSectionButtons()}
@@ -438,13 +446,23 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
);
}
if (this.allowCustomValue && searchString) {
filteredItems.push({
id: searchString,
primary:
this.customValueLabel ??
this.hass?.localize("ui.components.combo-box.add_custom_item") ??
"Add custom item",
secondary: `"${searchString}"`,
icon_path: mdiPlus,
});
}
this._items = filteredItems as PickerComboBoxItem[];
}
this._selectedItemIndex = -1;
if (this._virtualizerElement) {
this._virtualizerElement.scrollTo(0, 0);
}
this._valuePinned = true;
};
private _toggleSection(ev: Event) {
@@ -639,7 +657,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
typeof item === "string" ? item : item?.id;
private _getInitialSelectedIndex() {
if (!this._virtualizerElement || !this.value) {
if (!this._virtualizerElement || this._search || !this.value) {
return 0;
}

View File

@@ -28,7 +28,6 @@ export class HaAddonSelector extends LitElement {
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-addon-picker>`;
}

View File

@@ -29,7 +29,6 @@ export class HaConfigEntrySelector extends LitElement {
.disabled=${this.disabled}
.required=${this.required}
.integration=${this.selector.config_entry?.integration}
allow-custom-entity
></ha-config-entry-picker>`;
}

View File

@@ -107,7 +107,6 @@ export class HaDeviceSelector extends LitElement {
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-device-picker>
`;
}

View File

@@ -74,7 +74,6 @@ export class HaEntitySelector extends LitElement {
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-entity-picker>`;
}

View File

@@ -135,7 +135,6 @@ class HaServicePicker extends LitElement {
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
allow-custom-value
.notFoundLabel=${this.hass.localize(
"ui.components.service-picker.no_match"
)}

View File

@@ -40,7 +40,6 @@ export class HaZoneCondition extends LitElement {
@value-changed=${this._entityPicked}
.hass=${this.hass}
.disabled=${this.disabled}
allow-custom-entity
.entityFilter=${zoneAndLocationFilter}
></ha-entity-picker>
<ha-entity-picker
@@ -51,7 +50,6 @@ export class HaZoneCondition extends LitElement {
@value-changed=${this._zonePicked}
.hass=${this.hass}
.disabled=${this.disabled}
allow-custom-entity
.includeDomains=${includeDomains}
></ha-entity-picker>
`;

View File

@@ -43,7 +43,6 @@ export class HaZoneTrigger extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._entityPicked}
.hass=${this.hass}
allow-custom-entity
.entityFilter=${zoneAndLocationFilter}
></ha-entity-picker>
<ha-entity-picker
@@ -54,7 +53,6 @@ export class HaZoneTrigger extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._zonePicked}
.hass=${this.hass}
allow-custom-entity
.includeDomains=${includeDomains}
></ha-entity-picker>

View File

@@ -144,7 +144,6 @@ class HaPanelDevState extends LitElement {
.hass=${this.hass}
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
allow-custom-entity
show-entity-id
></ha-entity-picker>
${this._entityId

View File

@@ -75,7 +75,6 @@ export class DialogEditHome
"ui.panel.home.editor.favorite_entities_helper"
)}
reorder
allow-custom-entity
@value-changed=${this._favoriteEntitiesChanged}
></ha-entities-picker>

View File

@@ -167,7 +167,6 @@ export class HuiEntityEditor extends LitElement {
.index=${index}
.entityFilter=${this.entityFilter}
@value-changed=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
</div>
`

View File

@@ -51,7 +51,6 @@ export class HuiGraphFooterEditor
return html`
<div class="card-config">
<ha-entity-picker
allow-custom-entity
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)}

View File

@@ -78,7 +78,6 @@ export class HuiHeadingBadgesEditor extends LitElement {
${isEntityBadge && entityBadge
? html`
<ha-entity-picker
allow-custom-entity
hide-clear-icon
.hass=${this.hass}
.value=${entityBadge.entity ?? ""}
@@ -131,7 +130,6 @@ export class HuiHeadingBadgesEditor extends LitElement {
@value-changed=${this._entityPicked}
.value=${undefined}
@click=${preventDefault}
allow-custom-entity
add-button
></ha-entity-picker>
</div>

View File

@@ -316,7 +316,6 @@ export class HuiStatisticsGraphCardEditor
@value-changed=${this._valueChanged}
></ha-form>
<ha-statistics-picker
allow-custom-entity
.hass=${this.hass}
.placeholder=${this.hass!.localize(
"ui.panel.lovelace.editor.card.statistics-graph.pick_statistic"

View File

@@ -80,7 +80,6 @@ export class HuiEntitiesCardRowEditor extends LitElement {
`
: html`
<ha-entity-picker
allow-custom-entity
hide-clear-icon
.hass=${this.hass}
.value=${(entityConf as EntityConfig).entity}

View File

@@ -36,7 +36,6 @@ export class HuiHomeDashboardStrategyEditor
"ui.panel.lovelace.editor.strategy.home.add_favorite_entity"
)}
reorder
allow-custom-entity
@value-changed=${this._valueChanged}
>
</ha-entities-picker>

View File

@@ -1305,7 +1305,9 @@
"combo-box": {
"no_match": "No matching items found",
"no_items": "No items available",
"unknown_item": "Unknown item"
"unknown_item": "Unknown item",
"search_or_custom": "Search | Add custom item",
"add_custom_item": "Add custom item"
},
"suggest_with_ai": {
"label": "Suggest",
@@ -1314,6 +1316,9 @@
"suggesting_3": "Enchanting…",
"done": "Done!",
"error": "Fail!"
},
"navigation-picker": {
"add_custom_path": "Add custom path"
}
},
"dialogs": {