diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts
index 16625aa81a..68c3ed549f 100644
--- a/src/components/entity/ha-entities-picker.ts
+++ b/src/components/entity/ha-entities-picker.ts
@@ -8,7 +8,7 @@ import type { HaEntityComboBoxEntityFilterFunc } from "./ha-entity-combo-box";
import "./ha-entity-picker";
@customElement("ha-entities-picker")
-class HaEntitiesPickerLight extends LitElement {
+class HaEntitiesPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Array }) public value?: string[];
@@ -17,6 +17,10 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Boolean }) public required = false;
+ @property() public label?: string;
+
+ @property() public placeholder?: string;
+
@property() public helper?: string;
/**
@@ -67,11 +71,6 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
- @property({ attribute: "picked-entity-label" })
- public pickedEntityLabel?: string;
-
- @property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
-
@property({ attribute: false })
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
@@ -84,6 +83,7 @@ class HaEntitiesPickerLight extends LitElement {
const currentEntities = this._currentEntities;
return html`
+ ${this.label ? html`` : nothing}
${currentEntities.map(
(entityId) => html`
@@ -99,7 +99,6 @@ class HaEntitiesPickerLight extends LitElement {
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter}
.value=${entityId}
- .label=${this.pickedEntityLabel}
.disabled=${this.disabled}
.createDomains=${this.createDomains}
@value-changed=${this._entityChanged}
@@ -121,7 +120,7 @@ class HaEntitiesPickerLight extends LitElement {
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter}
- .label=${this.pickEntityLabel}
+ .placeholder=${this.placeholder}
.helper=${this.helper}
.disabled=${this.disabled}
.createDomains=${this.createDomains}
@@ -198,11 +197,15 @@ class HaEntitiesPickerLight extends LitElement {
div {
margin-top: 8px;
}
+ label {
+ display: block;
+ margin: 0 0 8px;
+ }
`;
}
declare global {
interface HTMLElementTagNameMap {
- "ha-entities-picker": HaEntitiesPickerLight;
+ "ha-entities-picker": HaEntitiesPicker;
}
}
diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts
index c8f8b5c378..0cba69c515 100644
--- a/src/components/entity/ha-entity-picker.ts
+++ b/src/components/entity/ha-entity-picker.ts
@@ -180,7 +180,7 @@ export class HaEntityPicker extends LitElement {
protected render() {
return html`
- ${this.label ? html`
${this.label}
` : nothing}
+ ${this.label ? html`
` : nothing}
${!this._opened
? html`
= (
+ item,
+ { index }
+ ) => html`
+
+ ${!item.state
+ ? html`
+
+ `
+ : html`
+
+ `}
+
+ ${item.primary}
+ ${item.secondary
+ ? html`${item.secondary}`
+ : nothing}
+ ${item.id && item.show_entity_id
+ ? html`
+
+ ${item.id}
+
+ `
+ : nothing}
+
+ `;
+
+ private _getItems = memoizeOne(
+ (
+ _opened: boolean,
+ hass: this["hass"],
+ statisticIds: StatisticsMetaData[],
+ includeStatisticsUnitOfMeasurement?: string | string[],
+ includeUnitClass?: string | string[],
+ includeDeviceClass?: string | string[],
+ entitiesOnly?: boolean,
+ excludeStatistics?: string[],
+ value?: string
+ ): StatisticItem[] => {
+ if (!statisticIds.length) {
+ return [
+ {
+ id: "",
+ label: "",
+ primary: this.hass.localize(
+ "ui.components.statistic-picker.no_statistics"
+ ),
+ },
+ ];
+ }
+
+ if (includeStatisticsUnitOfMeasurement) {
+ const includeUnits: (string | null)[] = ensureArray(
+ includeStatisticsUnitOfMeasurement
+ );
+ statisticIds = statisticIds.filter((meta) =>
+ includeUnits.includes(meta.statistics_unit_of_measurement)
+ );
+ }
+ if (includeUnitClass) {
+ const includeUnitClasses: (string | null)[] =
+ ensureArray(includeUnitClass);
+ statisticIds = statisticIds.filter((meta) =>
+ includeUnitClasses.includes(meta.unit_class)
+ );
+ }
+ if (includeDeviceClass) {
+ const includeDeviceClasses: (string | null)[] =
+ ensureArray(includeDeviceClass);
+ statisticIds = statisticIds.filter((meta) => {
+ const stateObj = this.hass.states[meta.statistic_id];
+ if (!stateObj) {
+ return true;
+ }
+ return includeDeviceClasses.includes(
+ stateObj.attributes.device_class || ""
+ );
+ });
+ }
+
+ const isRTL = computeRTL(this.hass);
+
+ const output: StatisticItem[] = [];
+ statisticIds.forEach((meta) => {
+ if (
+ excludeStatistics &&
+ meta.statistic_id !== value &&
+ excludeStatistics.includes(meta.statistic_id)
+ ) {
+ return;
+ }
+ const stateObj = this.hass.states[meta.statistic_id];
+
+ if (!stateObj) {
+ if (!entitiesOnly) {
+ const id = meta.statistic_id;
+ const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
+ const type =
+ meta.statistic_id.includes(":") &&
+ !meta.statistic_id.includes(".")
+ ? "external"
+ : "no_state";
+
+ if (type === "no_state") {
+ output.push({
+ id,
+ primary: label,
+ secondary: this.hass.localize(
+ "ui.components.statistic-picker.no_state"
+ ),
+ label: "",
+ type,
+ sorting_label: label,
+ iconPath: mdiShape,
+ });
+ } else if (type === "external") {
+ const domain = id.split(":")[0];
+ const domainName = domainToName(this.hass.localize, domain);
+ output.push({
+ id,
+ primary: label,
+ secondary: domainName,
+ label: "",
+ type,
+ sorting_label: label,
+ iconPath: mdiChartLine,
+ });
+ }
+ }
+ return;
+ }
+ const id = meta.statistic_id;
+
+ const { area, device } = getEntityContext(stateObj, hass);
+
+ const friendlyName = computeStateName(stateObj); // Keep this for search
+ const entityName = computeEntityName(stateObj, hass);
+ const deviceName = device ? computeDeviceName(device) : undefined;
+ const areaName = area ? computeAreaName(area) : undefined;
+
+ const primary = entityName || deviceName || id;
+ const secondary = [areaName, entityName ? deviceName : undefined]
+ .filter(Boolean)
+ .join(isRTL ? " ◂ " : " ▸ ");
+
+ output.push({
+ id,
+ primary,
+ secondary,
+ label: "",
+ state: stateObj,
+ type: "entity",
+ sorting_label: [deviceName, entityName].join("_"),
+ entity_name: entityName || deviceName,
+ area_name: areaName,
+ device_name: deviceName,
+ friendly_name: friendlyName,
+ show_entity_id: hass.userData?.showEntityIdPicker,
+ });
+ });
+
+ if (!output.length) {
+ return [
+ {
+ id: "",
+ primary: this.hass.localize(
+ "ui.components.statistic-picker.no_match"
+ ),
+ label: "",
+ },
+ ];
+ }
+
+ if (output.length > 1) {
+ output.sort((a, b) => {
+ const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
+ const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
+
+ return caseInsensitiveStringCompare(
+ `${aPrefix}_${a.sorting_label || ""}`,
+ `${bPrefix}_${b.sorting_label || ""}`,
+ this.hass.locale.language
+ );
+ });
+ }
+
+ output.push({
+ id: "__missing",
+ primary: this.hass.localize(
+ "ui.components.statistic-picker.missing_entity"
+ ),
+ label: "",
+ });
+
+ return output;
+ }
+ );
+
+ public async open() {
+ await this.updateComplete;
+ await this.comboBox?.open();
+ }
+
+ public async focus() {
+ await this.updateComplete;
+ await this.comboBox?.focus();
+ }
+
+ protected shouldUpdate(changedProps: PropertyValues) {
+ if (
+ changedProps.has("value") ||
+ changedProps.has("label") ||
+ changedProps.has("disabled")
+ ) {
+ return true;
+ }
+ return !(!changedProps.has("_opened") && this._opened);
+ }
+
+ public willUpdate(changedProps: PropertyValues) {
+ if (
+ (!this.hasUpdated && !this.statisticIds) ||
+ changedProps.has("statisticTypes")
+ ) {
+ this._getStatisticIds();
+ }
+
+ if (
+ this.statisticIds &&
+ (!this._initialItems || (changedProps.has("_opened") && this._opened))
+ ) {
+ this._items = this._getItems(
+ this._opened,
+ this.hass,
+ this.statisticIds!,
+ this.includeStatisticsUnitOfMeasurement,
+ this.includeUnitClass,
+ this.includeDeviceClass,
+ this.entitiesOnly,
+ this.excludeStatistics,
+ this.value
+ );
+ if (this._initialItems) {
+ this.comboBox.filteredItems = this._items;
+ }
+ this._initialItems = true;
+ }
+ }
+
+ protected render(): TemplateResult | typeof nothing {
+ if (this._items.length === 0) {
+ return nothing;
+ }
+
+ return html`
+
+ `;
+ }
+
+ private async _getStatisticIds() {
+ this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
+ }
+
+ private get _value() {
+ return this.value || "";
+ }
+
+ private _statisticChanged(ev: ValueChangedEvent) {
+ ev.stopPropagation();
+ let newValue = ev.detail.value;
+ if (newValue === "__missing") {
+ newValue = "";
+ }
+
+ if (newValue !== this._value) {
+ this._setValue(newValue);
+ }
+ }
+
+ private _openedChanged(ev: ValueChangedEvent) {
+ this._opened = ev.detail.value;
+ }
+
+ private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
+ Fuse.createIndex(
+ [
+ "entity_name",
+ "device_name",
+ "area_name",
+ "friendly_name", // for backwards compatibility
+ "id", // for technical search
+ ],
+ states
+ )
+ );
+
+ private _filterChanged(ev: CustomEvent): void {
+ if (!this._opened) return;
+
+ const target = ev.target as HaComboBox;
+ const filterString = ev.detail.value.trim().toLowerCase() as string;
+
+ const index = this._fuseIndex(this._items);
+ const fuse = new HaFuse(this._items, {}, index);
+
+ const results = fuse.multiTermsSearch(filterString);
+
+ if (results) {
+ target.filteredItems = results.map((result) => result.item);
+ } else {
+ target.filteredItems = this._items;
+ }
+ }
+
+ private _setValue(value: string) {
+ this.value = value;
+ setTimeout(() => {
+ fireEvent(this, "value-changed", { value });
+ fireEvent(this, "change");
+ }, 0);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-statistic-combo-box": HaStatisticComboBox;
+ }
+}
diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts
index 760bf202d1..f0dbdd0872 100644
--- a/src/components/entity/ha-statistic-picker.ts
+++ b/src/components/entity/ha-statistic-picker.ts
@@ -1,65 +1,66 @@
-import { mdiChartLine } from "@mdi/js";
-import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
-import Fuse from "fuse.js";
+import { mdiChartLine, mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
+import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import type { HassEntity } from "home-assistant-js-websocket";
-import type { PropertyValues, TemplateResult } from "lit";
-import { html, LitElement, nothing } from "lit";
+import {
+ css,
+ html,
+ LitElement,
+ nothing,
+ type CSSResultGroup,
+ type PropertyValues,
+} from "lit";
import { customElement, property, query, state } from "lit/decorators";
-import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
-import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
+import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeEntityName } from "../../common/entity/compute_entity_name";
-import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
-import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { computeRTL } from "../../common/util/compute_rtl";
+import { debounce } from "../../common/util/debounce";
import { domainToName } from "../../data/integration";
-import type { StatisticsMetaData } from "../../data/recorder";
-import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
-import { HaFuse } from "../../resources/fuse";
-import type { HomeAssistant, ValueChangedEvent } from "../../types";
-import "../ha-combo-box";
-import type { HaComboBox } from "../ha-combo-box";
+import {
+ getStatisticIds,
+ getStatisticLabel,
+ type StatisticsMetaData,
+} from "../../data/recorder";
+import type { HomeAssistant } from "../../types";
import "../ha-combo-box-item";
+import "../ha-icon-button";
+import type { HaMdListItem } from "../ha-md-list-item";
import "../ha-svg-icon";
+import "./ha-entity-combo-box";
+import type { HaEntityComboBox } from "./ha-entity-combo-box";
+import "./ha-statistic-combo-box";
import "./state-badge";
-type StatisticItemType = "entity" | "external" | "no_state";
-
interface StatisticItem {
- id: string;
- label: string;
primary: string;
secondary?: string;
- show_entity_id?: boolean;
- entity_name?: string;
- area_name?: string;
- device_name?: string;
- friendly_name?: string;
- sorting_label?: string;
- state?: HassEntity;
- type?: StatisticItemType;
iconPath?: string;
+ stateObj?: HassEntity;
}
-const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
-
-const ENTITY_ID_STYLE = styleMap({
- fontFamily: "var(--code-font-family, monospace)",
- fontSize: "11px",
-});
-
@customElement("ha-statistic-picker")
export class HaStatisticPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
+ // eslint-disable-next-line lit/no-native-attributes
+ @property({ type: Boolean }) public autofocus = false;
+
+ @property({ type: Boolean }) public disabled = false;
+
+ @property({ type: Boolean }) public required = false;
+
@property() public label?: string;
@property() public value?: string;
+ @property() public helper?: string;
+
+ @property() public placeholder?: string;
+
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@@ -69,8 +70,6 @@ export class HaStatisticPicker extends LitElement {
@property({ attribute: false, type: Array })
public statisticIds?: StatisticsMetaData[];
- @property({ type: Boolean }) public disabled = false;
-
/**
* Show only statistics natively stored with these units of measurements.
* @type {Array}
@@ -112,251 +111,15 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[];
- @property({ attribute: false }) public helpMissingEntityUrl =
- "/more-info/statistics/";
+ @property({ attribute: "hide-clear-icon", type: Boolean })
+ public hideClearIcon = false;
+
+ @query("#anchor") private _anchor?: HaMdListItem;
+
+ @query("#input") private _input?: HaEntityComboBox;
@state() private _opened = false;
- @query("ha-combo-box", true) public comboBox!: HaComboBox;
-
- private _initialItems = false;
-
- private _items: StatisticItem[] = [];
-
- protected firstUpdated(changedProperties: PropertyValues): void {
- super.firstUpdated(changedProperties);
- this.hass.loadBackendTranslation("title");
- }
-
- private _rowRenderer: ComboBoxLitRenderer = (
- item,
- { index }
- ) => html`
-
- ${!item.state
- ? html``
- : html`
-
- `}
-
- ${item.primary}
- ${item.secondary
- ? html`${item.secondary}`
- : nothing}
- ${item.id && item.show_entity_id
- ? html`
-
- ${item.id}
-
- `
- : nothing}
-
- `;
-
- private _getItems = memoizeOne(
- (
- _opened: boolean,
- hass: this["hass"],
- statisticIds: StatisticsMetaData[],
- includeStatisticsUnitOfMeasurement?: string | string[],
- includeUnitClass?: string | string[],
- includeDeviceClass?: string | string[],
- entitiesOnly?: boolean,
- excludeStatistics?: string[],
- value?: string
- ): StatisticItem[] => {
- if (!statisticIds.length) {
- return [
- {
- id: "",
- label: this.hass.localize(
- "ui.components.statistic-picker.no_statistics"
- ),
- primary: this.hass.localize(
- "ui.components.statistic-picker.no_statistics"
- ),
- },
- ];
- }
-
- if (includeStatisticsUnitOfMeasurement) {
- const includeUnits: (string | null)[] = ensureArray(
- includeStatisticsUnitOfMeasurement
- );
- statisticIds = statisticIds.filter((meta) =>
- includeUnits.includes(meta.statistics_unit_of_measurement)
- );
- }
- if (includeUnitClass) {
- const includeUnitClasses: (string | null)[] =
- ensureArray(includeUnitClass);
- statisticIds = statisticIds.filter((meta) =>
- includeUnitClasses.includes(meta.unit_class)
- );
- }
- if (includeDeviceClass) {
- const includeDeviceClasses: (string | null)[] =
- ensureArray(includeDeviceClass);
- statisticIds = statisticIds.filter((meta) => {
- const stateObj = this.hass.states[meta.statistic_id];
- if (!stateObj) {
- return true;
- }
- return includeDeviceClasses.includes(
- stateObj.attributes.device_class || ""
- );
- });
- }
-
- const isRTL = computeRTL(this.hass);
-
- const output: StatisticItem[] = [];
- statisticIds.forEach((meta) => {
- if (
- excludeStatistics &&
- meta.statistic_id !== value &&
- excludeStatistics.includes(meta.statistic_id)
- ) {
- return;
- }
- const entityState = this.hass.states[meta.statistic_id];
- if (!entityState) {
- if (!entitiesOnly) {
- const id = meta.statistic_id;
- const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
- const type =
- meta.statistic_id.includes(":") &&
- !meta.statistic_id.includes(".")
- ? "external"
- : "no_state";
-
- if (type === "no_state") {
- output.push({
- id,
- primary: label,
- secondary: this.hass.localize(
- "ui.components.statistic-picker.no_state"
- ),
- label,
- type,
- sorting_label: label,
- });
- } else if (type === "external") {
- const domain = id.split(":")[0];
- const domainName = domainToName(this.hass.localize, domain);
- output.push({
- id,
- primary: label,
- secondary: domainName,
- label,
- type,
- sorting_label: label,
- iconPath: mdiChartLine,
- });
- }
- }
- return;
- }
- const id = meta.statistic_id;
-
- const { area, device } = getEntityContext(entityState, hass);
-
- const friendlyName = computeStateName(entityState); // Keep this for search
- const entityName = computeEntityName(entityState, hass);
- const deviceName = device ? computeDeviceName(device) : undefined;
- const areaName = area ? computeAreaName(area) : undefined;
-
- const primary = entityName || deviceName || id;
- const secondary = [areaName, entityName ? deviceName : undefined]
- .filter(Boolean)
- .join(isRTL ? " ◂ " : " ▸ ");
-
- output.push({
- id,
- primary,
- secondary,
- label: friendlyName,
- state: entityState,
- type: "entity",
- sorting_label: [deviceName, entityName].join("_"),
- entity_name: entityName || deviceName,
- area_name: areaName,
- device_name: deviceName,
- friendly_name: friendlyName,
- show_entity_id: hass.userData?.showEntityIdPicker,
- });
- });
-
- if (!output.length) {
- return [
- {
- id: "",
- primary: this.hass.localize(
- "ui.components.statistic-picker.no_match"
- ),
- label: this.hass.localize(
- "ui.components.statistic-picker.no_match"
- ),
- },
- ];
- }
-
- if (output.length > 1) {
- output.sort((a, b) => {
- const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
- const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
-
- return caseInsensitiveStringCompare(
- `${aPrefix}_${a.sorting_label || ""}`,
- `${bPrefix}_${b.sorting_label || ""}`,
- this.hass.locale.language
- );
- });
- }
-
- output.push({
- id: "__missing",
- primary: this.hass.localize(
- "ui.components.statistic-picker.missing_entity"
- ),
- label: this.hass.localize(
- "ui.components.statistic-picker.missing_entity"
- ),
- });
-
- return output;
- }
- );
-
- public async open() {
- await this.updateComplete;
- await this.comboBox?.open();
- }
-
- public async focus() {
- await this.updateComplete;
- await this.comboBox?.focus();
- }
-
- protected shouldUpdate(changedProps: PropertyValues) {
- if (
- changedProps.has("value") ||
- changedProps.has("label") ||
- changedProps.has("disabled")
- ) {
- return true;
- }
- return !(!changedProps.has("_opened") && this._opened);
- }
-
public willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && !this.statisticIds) ||
@@ -364,117 +127,278 @@ export class HaStatisticPicker extends LitElement {
) {
this._getStatisticIds();
}
-
- if (
- this.statisticIds &&
- (!this._initialItems || (changedProps.has("_opened") && this._opened))
- ) {
- this._items = this._getItems(
- this._opened,
- this.hass,
- this.statisticIds!,
- this.includeStatisticsUnitOfMeasurement,
- this.includeUnitClass,
- this.includeDeviceClass,
- this.entitiesOnly,
- this.excludeStatistics,
- this.value
- );
- if (this._initialItems) {
- this.comboBox.filteredItems = this._items;
- }
- this._initialItems = true;
- }
- }
-
- protected render(): TemplateResult | typeof nothing {
- if (this._items.length === 0) {
- return nothing;
- }
-
- return html`
-
- `;
}
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
}
- private get _value() {
- return this.value || "";
- }
-
- private _statisticChanged(ev: ValueChangedEvent) {
- ev.stopPropagation();
- let newValue = ev.detail.value;
- if (newValue === "__missing") {
- newValue = "";
+ private _statisticMetaData = memoizeOne(
+ (statisticId: string, statisticIds: StatisticsMetaData[]) => {
+ if (!statisticIds) {
+ return undefined;
+ }
+ return statisticIds.find(
+ (statistic) => statistic.statistic_id === statisticId
+ );
}
-
- if (newValue !== this._value) {
- this._setValue(newValue);
- }
- }
-
- private _openedChanged(ev: ValueChangedEvent) {
- this._opened = ev.detail.value;
- }
-
- private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
- Fuse.createIndex(
- [
- "label",
- "entity_name",
- "device_name",
- "area_name",
- "friendly_name", // for backwards compatibility
- "id", // for technical search
- ],
- states
- )
);
- private _filterChanged(ev: CustomEvent): void {
- if (!this._opened) return;
+ private _renderContent() {
+ const statisticId = this.value || "";
- const target = ev.target as HaComboBox;
- const filterString = ev.detail.value.trim().toLowerCase() as string;
+ if (!this.value) {
+ return html`
+ ${this.placeholder ??
+ this.hass.localize(
+ "ui.components.statistic-picker.placeholder"
+ )}
+
+ `;
+ }
- const index = this._fuseIndex(this._items);
- const fuse = new HaFuse(this._items, {}, index);
+ const item = this._computeItem(statisticId);
- const results = fuse.multiTermsSearch(filterString);
+ const showClearIcon =
+ !this.required && !this.disabled && !this.hideClearIcon;
- if (results) {
- target.filteredItems = results.map((result) => result.item);
- } else {
- target.filteredItems = this._items;
+ return html`
+ ${item.stateObj
+ ? html`
+
+ `
+ : item.iconPath
+ ? html``
+ : nothing}
+ ${item.primary}
+ ${item.secondary
+ ? html`${item.secondary}`
+ : nothing}
+ ${showClearIcon
+ ? html``
+ : nothing}
+
+ `;
+ }
+
+ private _computeItem(statisticId: string): StatisticItem {
+ const stateObj = this.hass.states[statisticId];
+
+ if (stateObj) {
+ const { area, device } = getEntityContext(stateObj, this.hass);
+
+ const entityName = computeEntityName(stateObj, this.hass);
+ const deviceName = device ? computeDeviceName(device) : undefined;
+ const areaName = area ? computeAreaName(area) : undefined;
+
+ const isRTL = computeRTL(this.hass);
+
+ const primary = entityName || deviceName || statisticId;
+ const secondary = [areaName, entityName ? deviceName : undefined]
+ .filter(Boolean)
+ .join(isRTL ? " ◂ " : " ▸ ");
+
+ return {
+ primary,
+ secondary,
+ stateObj,
+ };
+ }
+
+ const statistic = this.statisticIds
+ ? this._statisticMetaData(statisticId, this.statisticIds)
+ : undefined;
+
+ if (statistic) {
+ const type =
+ statisticId.includes(":") && !statisticId.includes(".")
+ ? "external"
+ : "no_state";
+
+ if (type === "external") {
+ const label = getStatisticLabel(this.hass, statisticId, statistic);
+ const domain = statisticId.split(":")[0];
+ const domainName = domainToName(this.hass.localize, domain);
+
+ return {
+ primary: label,
+ secondary: domainName,
+ iconPath: mdiChartLine,
+ };
+ }
+ }
+
+ return {
+ primary: statisticId,
+ iconPath: mdiShape,
+ };
+ }
+
+ protected render() {
+ return html`
+ ${this.label ? html`` : nothing}
+
+ ${!this._opened
+ ? html`
+
+ ${this._renderContent()}
+
+ `
+ : html`
+
+ `}
+ ${this._renderHelper()}
+
+ `;
+ }
+
+ private _renderHelper() {
+ return this.helper
+ ? html`${this.helper}`
+ : nothing;
+ }
+
+ private _clear(e) {
+ e.stopPropagation();
+ this.value = undefined;
+ fireEvent(this, "value-changed", { value: undefined });
+ fireEvent(this, "change");
+ }
+
+ private async _showPicker() {
+ if (this.disabled) {
+ return;
+ }
+ this._opened = true;
+ await this.updateComplete;
+ this._input?.focus();
+ this._input?.open();
+ }
+
+ // Multiple calls to _openedChanged can be triggered in quick succession
+ // when the menu is opened
+ private _debounceOpenedChanged = debounce(
+ (ev) => this._openedChanged(ev),
+ 10
+ );
+
+ private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
+ const opened = ev.detail.value;
+ if (this._opened && !opened) {
+ this._opened = false;
+ await this.updateComplete;
+ this._anchor?.focus();
}
}
- private _setValue(value: string) {
- this.value = value;
- setTimeout(() => {
- fireEvent(this, "value-changed", { value });
- fireEvent(this, "change");
- }, 0);
+ static get styles(): CSSResultGroup {
+ return [
+ css`
+ .container {
+ position: relative;
+ display: block;
+ }
+ ha-combo-box-item {
+ background-color: var(--mdc-text-field-fill-color, whitesmoke);
+ border-radius: 4px;
+ border-end-end-radius: 0;
+ border-end-start-radius: 0;
+ --md-list-item-one-line-container-height: 56px;
+ --md-list-item-two-line-container-height: 56px;
+ --md-list-item-top-space: 8px;
+ --md-list-item-bottom-space: 8px;
+ --md-list-item-leading-space: 8px;
+ --md-list-item-trailing-space: 8px;
+ --ha-md-list-item-gap: 8px;
+ /* Remove the default focus ring */
+ --md-focus-ring-width: 0px;
+ --md-focus-ring-duration: 0s;
+ }
+
+ /* Add Similar focus style as the text field */
+ ha-combo-box-item:after {
+ display: block;
+ content: "";
+ position: absolute;
+ pointer-events: none;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ width: 100%;
+ background-color: var(
+ --mdc-text-field-idle-line-color,
+ rgba(0, 0, 0, 0.42)
+ );
+ transform:
+ height 180ms ease-in-out,
+ background-color 180ms ease-in-out;
+ }
+
+ ha-combo-box-item:focus:after {
+ height: 2px;
+ background-color: var(--mdc-theme-primary);
+ }
+
+ ha-combo-box-item ha-svg-icon[slot="start"] {
+ margin: 0 4px;
+ }
+ .clear {
+ margin: 0 -8px;
+ --mdc-icon-button-size: 32px;
+ --mdc-icon-size: 20px;
+ }
+ .edit {
+ --mdc-icon-size: 20px;
+ width: 32px;
+ }
+ label {
+ display: block;
+ margin: 0 0 8px;
+ }
+ .placeholder {
+ color: var(--secondary-text-color);
+ padding: 0 8px;
+ }
+ `,
+ ];
}
}
diff --git a/src/components/entity/ha-statistics-picker.ts b/src/components/entity/ha-statistics-picker.ts
index d731b2ec20..e259a222e9 100644
--- a/src/components/entity/ha-statistics-picker.ts
+++ b/src/components/entity/ha-statistics-picker.ts
@@ -16,11 +16,11 @@ class HaStatisticsPicker extends LitElement {
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
- @property({ attribute: "picked-statistic-label" })
- public pickedStatisticLabel?: string;
+ @property({ type: String })
+ public label?: string;
- @property({ attribute: "pick-statistic-label" })
- public pickStatisticLabel?: string;
+ @property({ type: String })
+ public placeholder?: string;
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@@ -82,6 +82,7 @@ class HaStatisticsPicker extends LitElement {
: this.statisticTypes;
return html`
+ ${this.label ? html`` : nothing}
${repeat(
this._currentStatistics,
(statisticId) => statisticId,
@@ -96,7 +97,6 @@ class HaStatisticsPicker extends LitElement {
.value=${statisticId}
.statisticTypes=${includeStatisticTypesCurrent}
.statisticIds=${this.statisticIds}
- .label=${this.pickedStatisticLabel}
.excludeStatistics=${this.value}
.allowCustomEntity=${this.allowCustomEntity}
@value-changed=${this._statisticChanged}
@@ -113,7 +113,7 @@ class HaStatisticsPicker extends LitElement {
.includeDeviceClass=${this.includeDeviceClass}
.statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds}
- .label=${this.pickStatisticLabel}
+ .placeholder=${this.placeholder}
.excludeStatistics=${this.value}
.allowCustomEntity=${this.allowCustomEntity}
@value-changed=${this._addStatistic}
@@ -181,6 +181,10 @@ class HaStatisticsPicker extends LitElement {
width: 100%;
margin-top: 8px;
}
+ label {
+ display: block;
+ margin-bottom: 0 0 8px;
+ }
`;
}
diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts
index 1aae4adf78..df3a7eab76 100644
--- a/src/components/ha-selector/ha-selector-entity.ts
+++ b/src/components/ha-selector/ha-selector-entity.ts
@@ -76,10 +76,10 @@ export class HaEntitySelector extends LitElement {
}
return html`
- ${this.label ? html`` : ""}