Compare commits

..

3 Commits

Author SHA1 Message Date
Wendelin
4b72e2d7a5 Add cli to package.json 2025-12-17 16:10:20 +01:00
Wendelin
542be12673 Code more like cli 2025-12-17 16:10:20 +01:00
Wendelin
dba6841a93 Add first auto doc test 2025-12-17 16:10:20 +01:00
50 changed files with 1982 additions and 681 deletions

View File

@@ -3,6 +3,9 @@ import { glob } from "glob";
import gulp from "gulp"; import gulp from "gulp";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { marked } from "marked"; import { marked } from "marked";
import ts from "typescript";
import { create } from "@custom-elements-manifest/analyzer";
import { litPlugin } from "@custom-elements-manifest/analyzer/src/features/framework-plugins/lit/lit.js";
import path from "path"; import path from "path";
import paths from "../paths.cjs"; import paths from "../paths.cjs";
import "./clean.js"; import "./clean.js";
@@ -13,6 +16,28 @@ import "./service-worker.js";
import "./translations.js"; import "./translations.js";
import "./rspack.js"; import "./rspack.js";
gulp.task("generate-component-docs", async function generateComponentDocs() {
const filePaths = ["src/components/ha-alert.ts"];
const modules = await Promise.all(
filePaths.map(async (file) => {
const filePath = path.resolve(file);
console.log(`Reading ${file} -> ${filePath}`);
const source = fs.readFileSync(filePath).toString();
return ts.createSourceFile(file, source, ts.ScriptTarget.ES2015, true);
})
);
const manifest = create({
modules,
plugins: litPlugin(),
context: { dev: true },
});
console.log(manifest);
});
gulp.task("gather-gallery-pages", async function gatherPages() { gulp.task("gather-gallery-pages", async function gatherPages() {
const pageDir = path.resolve(paths.gallery_dir, "src/pages"); const pageDir = path.resolve(paths.gallery_dir, "src/pages");
const files = await glob(path.resolve(pageDir, "**/*")); const files = await glob(path.resolve(pageDir, "**/*"));

186
custom-elements.json Normal file
View File

@@ -0,0 +1,186 @@
{
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/components/ha-alert.ts",
"declarations": [
{
"kind": "class",
"description": "A custom alert component for displaying messages with various alert types.",
"name": "HaAlert",
"cssProperties": [
{
"description": "The color used for \"info\" alerts.",
"name": "--info-color"
},
{
"description": "The color used for \"warning\" alerts.",
"name": "--warning-color"
},
{
"description": "The color used for \"error\" alerts.",
"name": "--error-color"
},
{
"description": "The color used for \"success\" alerts.",
"name": "--success-color"
},
{
"description": "The primary text color used in the alert.",
"name": "--primary-text-color"
}
],
"cssParts": [
{
"description": "The container for the alert.",
"name": "issue-type"
},
{
"description": "The container for the alert icon.",
"name": "icon"
},
{
"description": "The container for the alert content.",
"name": "content"
},
{
"description": "The container for the alert actions.",
"name": "action"
},
{
"description": "The container for the alert title.",
"name": "title"
}
],
"slots": [
{
"description": "The main content of the alert.",
"name": ""
},
{
"description": "Slot for providing a custom icon for the alert.",
"name": "icon"
},
{
"description": "Slot for providing custom actions or buttons for the alert.",
"name": "action"
}
],
"members": [
{
"kind": "field",
"name": "title",
"type": {
"text": "string"
},
"privacy": "public",
"default": "\"\"",
"description": "The title of the alert. Defaults to an empty string.",
"attribute": "title"
},
{
"kind": "field",
"name": "alertType",
"type": {
"text": "\"info\" | \"warning\" | \"error\" | \"success\""
},
"privacy": "public",
"default": "\"info\"",
"description": "The type of alert to display. Defaults to \"info\". Determines the styling and icon used.",
"attribute": "alert-type"
},
{
"kind": "field",
"name": "dismissable",
"type": {
"text": "boolean"
},
"privacy": "public",
"default": "false",
"description": "Whether the alert can be dismissed. Defaults to `false`. If `true`, a dismiss button is displayed.",
"attribute": "dismissable"
},
{
"kind": "field",
"name": "narrow",
"type": {
"text": "boolean"
},
"privacy": "public",
"default": "false",
"description": "Whether the alert should use a narrow layout. Defaults to `false`.",
"attribute": "narrow"
},
{
"kind": "method",
"name": "_dismissClicked",
"privacy": "private"
}
],
"events": [
{
"description": "Fired when the dismiss button is clicked.",
"name": "alert-dismissed-clicked"
}
],
"attributes": [
{
"name": "title",
"type": {
"text": "string"
},
"default": "\"\"",
"description": "The title of the alert. Defaults to an empty string.",
"fieldName": "title"
},
{
"name": "alert-type",
"type": {
"text": "\"info\" | \"warning\" | \"error\" | \"success\""
},
"default": "\"info\"",
"description": "The type of alert to display. Defaults to \"info\". Determines the styling and icon used.",
"fieldName": "alertType"
},
{
"name": "dismissable",
"type": {
"text": "boolean"
},
"default": "false",
"description": "Whether the alert can be dismissed. Defaults to `false`. If `true`, a dismiss button is displayed.",
"fieldName": "dismissable"
},
{
"name": "narrow",
"type": {
"text": "boolean"
},
"default": "false",
"description": "Whether the alert should use a narrow layout. Defaults to `false`.",
"fieldName": "narrow"
}
],
"superclass": {
"name": "LitElement",
"package": "lit"
},
"tagName": "ha-alert",
"customElement": true
}
],
"exports": [
{
"kind": "custom-element-definition",
"name": "ha-alert",
"declaration": {
"name": "HaAlert",
"module": "src/components/ha-alert.ts"
}
}
]
}
]
}

View File

@@ -20,7 +20,9 @@
"prepack": "pinst --disable", "prepack": "pinst --disable",
"postpack": "pinst --enable", "postpack": "pinst --enable",
"test": "vitest run --config test/vitest.config.ts", "test": "vitest run --config test/vitest.config.ts",
"test:coverage": "vitest run --config test/vitest.config.ts --coverage" "test:coverage": "vitest run --config test/vitest.config.ts --coverage",
"analyze": "cem analyze --litelement --globs \"src/components/ha-alert.ts\" --dev",
"doc": "gulp generate-component-docs"
}, },
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)", "author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -153,6 +155,8 @@
"@babel/plugin-transform-runtime": "7.28.5", "@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5", "@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.7", "@bundle-stats/plugin-webpack-filter": "4.21.7",
"@custom-elements-manifest/analyzer": "0.10.4",
"@custom-elements-manifest/to-markdown": "0.1.0",
"@lokalise/node-api": "15.4.0", "@lokalise/node-api": "15.4.0",
"@octokit/auth-oauth-device": "8.0.3", "@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3", "@octokit/plugin-retry": "8.0.3",
@@ -218,7 +222,7 @@
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.49.0", "typescript-eslint": "8.49.0",
"vite-tsconfig-paths": "6.0.1", "vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.15", "vitest": "4.0.15",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0", "webpackbar": "7.0.0",

View File

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

View File

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

View File

@@ -277,13 +277,12 @@ export class HaEntityPicker extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity} .allowCustomValue=${this.allowCustomEntity}
.required=${this.required}
.label=${this.label} .label=${this.label}
.placeholder=${placeholder}
.helper=${this.helper} .helper=${this.helper}
.value=${this.addButton ? undefined : this.value}
.searchLabel=${this.searchLabel} .searchLabel=${this.searchLabel}
.notFoundLabel=${this._notFoundLabel} .notFoundLabel=${this._notFoundLabel}
.placeholder=${placeholder}
.value=${this.addButton ? undefined : this.value}
.rowRenderer=${this._rowRenderer} .rowRenderer=${this._rowRenderer}
.getItems=${this._getItems} .getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}
@@ -291,7 +290,6 @@ export class HaEntityPicker extends LitElement {
.searchFn=${this._searchFn} .searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer} .valueRenderer=${this._valueRenderer}
.searchKeys=${entityComboBoxKeys} .searchKeys=${entityComboBoxKeys}
use-top-label
.addButtonLabel=${this.addButton .addButtonLabel=${this.addButton
? this.hass.localize("ui.components.entity.entity-picker.add") ? this.hass.localize("ui.components.entity.entity-picker.add")
: undefined} : undefined}

View File

@@ -471,14 +471,14 @@ export class HaStatisticPicker extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity} .allowCustomValue=${this.allowCustomEntity}
.disabled=${this.disabled}
.label=${this.label} .label=${this.label}
.placeholder=${placeholder} .disabled=${this.disabled}
.value=${this.value}
.notFoundLabel=${this._notFoundLabel} .notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize( .emptyLabel=${this.hass.localize(
"ui.components.statistic-picker.no_statistics" "ui.components.statistic-picker.no_statistics"
)} )}
.placeholder=${placeholder}
.value=${this.value}
.rowRenderer=${this._rowRenderer} .rowRenderer=${this._rowRenderer}
.getItems=${this._getItems} .getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}

View File

@@ -1,29 +1,29 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded"; import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import type { HassioAddonInfo } from "../data/hassio/addon";
import { fetchHassioAddonsInfo } from "../data/hassio/addon"; import { fetchHassioAddonsInfo } from "../data/hassio/addon";
import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-alert"; import "./ha-alert";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
const SEARCH_KEYS = [ const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`
{ name: "primary", weight: 10 },
{ name: "secondary", weight: 8 },
{ name: "search_labels.description", weight: 6 },
{ name: "search_labels.repository", weight: 5 },
];
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
<ha-combo-box-item type="button"> <ha-combo-box-item type="button">
<span slot="headline">${item.primary}</span> <span slot="headline">${item.name}</span>
<span slot="supporting-text">${item.secondary}</span> <span slot="supporting-text">${item.slug}</span>
${item.icon ${item.icon
? html` <img alt="" slot="start" .src=${item.icon} /> ` ? html`
<img
alt=""
slot="start"
.src="/api/hassio/addons/${item.slug}/icon"
/>
`
: nothing} : nothing}
</ha-combo-box-item> </ha-combo-box-item>
`; `;
@@ -38,22 +38,22 @@ class HaAddonPicker extends LitElement {
@property() public helper?: string; @property() public helper?: string;
@state() private _addons?: PickerComboBoxItem[]; @state() private _addons?: HassioAddonInfo[];
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@query("ha-generic-picker") private _genericPicker!: HaGenericPicker; @query("ha-combo-box") private _comboBox!: HaComboBox;
@state() private _error?: string; @state() private _error?: string;
public open() { public open() {
this._genericPicker?.open(); this._comboBox?.open();
} }
public focus() { public focus() {
this._genericPicker?.focus(); this._comboBox?.focus();
} }
protected firstUpdated() { protected firstUpdated() {
@@ -61,34 +61,29 @@ class HaAddonPicker extends LitElement {
} }
protected render() { protected render() {
const label =
this.label === undefined && this.hass
? this.hass.localize("ui.components.addon-picker.addon")
: this.label;
if (this._error) { if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`; return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
} }
if (!this._addons) { if (!this._addons) {
return nothing; return nothing;
} }
return html` return html`
<ha-generic-picker <ha-combo-box
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .label=${this.label === undefined && this.hass
.label=${label} ? this.hass.localize("ui.components.addon-picker.addon")
.valueRenderer=${this._valueRenderer} : this.label}
.helper=${this.helper} .value=${this._value}
.disabled=${this.disabled}
.required=${this.required} .required=${this.required}
.value=${this.value} .disabled=${this.disabled}
.getItems=${this._getItems} .helper=${this.helper}
.searchKeys=${SEARCH_KEYS} .renderer=${rowRenderer}
.rowRenderer=${rowRenderer} .items=${this._addons}
item-value-path="slug"
item-id-path="slug"
item-label-path="name"
@value-changed=${this._addonChanged} @value-changed=${this._addonChanged}
> ></ha-combo-box>
</ha-generic-picker>
`; `;
} }
@@ -98,19 +93,9 @@ class HaAddonPicker extends LitElement {
const addonsInfo = await fetchHassioAddonsInfo(this.hass); const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = addonsInfo.addons this._addons = addonsInfo.addons
.filter((addon) => addon.version) .filter((addon) => addon.version)
.map((addon) => ({ .sort((a, b) =>
id: addon.slug, stringCompare(a.name, b.name, this.hass.locale.language)
primary: addon.name, );
secondary: addon.slug,
icon: addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined,
search_labels: {
description: addon.description || null,
repository: addon.repository || null,
},
sorting_label: [addon.name, addon.slug].filter(Boolean).join("_"),
}));
} else { } else {
this._error = this.hass.localize( this._error = this.hass.localize(
"ui.components.addon-picker.error.no_supervisor" "ui.components.addon-picker.error.no_supervisor"
@@ -123,8 +108,6 @@ class HaAddonPicker extends LitElement {
} }
} }
private _getItems = () => this._addons!;
private get _value() { private get _value() {
return this.value || ""; return this.value || "";
} }
@@ -145,17 +128,6 @@ class HaAddonPicker extends LitElement {
fireEvent(this, "change"); fireEvent(this, "change");
}, 0); }, 0);
} }
private _valueRenderer = (itemId: string) => {
const item = this._addons!.find((addon) => addon.id === itemId);
return html`${item?.icon
? html`<img
slot="start"
alt=${item.primary ?? "Unknown"}
.src=${item.icon}
/>`
: nothing}<span slot="headline">${item?.primary || "Unknown"}</span>`;
};
} }
declare global { declare global {

View File

@@ -25,6 +25,36 @@ declare global {
} }
} }
/**
* A custom alert component for displaying messages with various alert types.
*
* @element ha-alert
*
* @property {string} title - The title of the alert. Defaults to an empty string.
* @property {"info" | "warning" | "error" | "success"} alertType - The type of alert to display.
* Defaults to "info". Determines the styling and icon used.
* @property {boolean} dismissable - Whether the alert can be dismissed. Defaults to `false`.
* If `true`, a dismiss button is displayed.
* @property {boolean} narrow - Whether the alert should use a narrow layout. Defaults to `false`.
*
* @slot - The main content of the alert.
* @slot icon - Slot for providing a custom icon for the alert.
* @slot action - Slot for providing custom actions or buttons for the alert.
*
* @fires alert-dismissed-clicked - Fired when the dismiss button is clicked.
*
* @csspart issue-type - The container for the alert.
* @csspart icon - The container for the alert icon.
* @csspart content - The container for the alert content.
* @csspart action - The container for the alert actions.
* @csspart title - The container for the alert title.
*
* @cssprop --info-color - The color used for "info" alerts.
* @cssprop --warning-color - The color used for "warning" alerts.
* @cssprop --error-color - The color used for "error" alerts.
* @cssprop --success-color - The color used for "success" alerts.
* @cssprop --primary-text-color - The primary text color used in the alert.
*/
@customElement("ha-alert") @customElement("ha-alert")
class HaAlert extends LitElement { class HaAlert extends LitElement {
// eslint-disable-next-line lit/no-native-attributes // eslint-disable-next-line lit/no-native-attributes

View File

@@ -51,6 +51,9 @@ export class HaAreaPicker extends LitElement {
@property({ type: Boolean, attribute: "no-add" }) @property({ type: Boolean, attribute: "no-add" })
public noAdd = false; public noAdd = false;
@property({ type: Boolean, attribute: "show-label" })
public showLabel = false;
/** /**
* Show only areas with entities from specific domains. * Show only areas with entities from specific domains.
* @type {Array} * @type {Array}
@@ -363,19 +366,16 @@ export class HaAreaPicker extends LitElement {
}; };
protected render(): TemplateResult { protected render(): TemplateResult {
const baseLabel = const placeholder =
this.label ?? this.hass.localize("ui.components.area-picker.area"); this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
const valueRenderer = this._computeValueRenderer(this.hass.areas); const valueRenderer = this._computeValueRenderer(this.hass.areas);
// Only show label if there's no floor let showLabel = this.showLabel;
let label: string | undefined = baseLabel; if (this.value) {
if (this.value && baseLabel) {
const area = this.hass.areas[this.value]; const area = this.hass.areas[this.value];
if (area) { if (area) {
const { floor } = getAreaContext(area, this.hass.floors); const { floor } = getAreaContext(area, this.hass.floors);
if (floor) { showLabel = !floor && this.showLabel;
label = undefined;
}
} }
} }
@@ -383,12 +383,14 @@ export class HaAreaPicker extends LitElement {
<ha-generic-picker <ha-generic-picker
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${label} .label=${this.label}
.helper=${this.helper} .helper=${this.helper}
.notFoundLabel=${this._notFoundLabel} .notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")} .emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
.placeholder=${placeholder}
.showLabel=${showLabel}
.value=${this.value} .value=${this.value}
.getItems=${this._getItems} .getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}

View File

@@ -1,23 +1,25 @@
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js"; import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import { html, LitElement } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { LocalizeKeys } from "../common/translations/localize"; import type { LocalizeKeys } from "../common/translations/localize";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-generic-picker"; import "./ha-list-item";
import type { PickerComboBoxItem } from "./ha-picker-combo-box"; import "./ha-md-divider";
import type { PickerValueRenderer } from "./ha-picker-field"; import "./ha-select";
import type { HaSelect } from "./ha-select";
@customElement("ha-color-picker") @customElement("ha-color-picker")
export class HaColorPicker extends LitElement { export class HaColorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@property() public helper?: string; @property() public helper?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: string; @property() public value?: string;
@property({ type: String, attribute: "default_color" }) @property({ type: String, attribute: "default_color" })
@@ -31,178 +33,137 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false; @query("ha-select") private _select?: HaSelect;
connectedCallback(): void {
super.connectedCallback();
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
this._select?.layoutOptions();
}
private _valueSelected(ev) {
ev.stopPropagation();
if (!this.isConnected) return;
const value = ev.target.value;
this.value = value === this.defaultColor ? undefined : value;
fireEvent(this, "value-changed", {
value: this.value,
});
}
render() { render() {
const effectiveValue = this.value ?? this.defaultColor ?? ""; const value = this.value || this.defaultColor || "";
const isCustom = !(
THEME_COLORS.has(value) ||
value === "none" ||
value === "state"
);
return html` return html`
<ha-generic-picker <ha-select
.hass=${this.hass} .icon=${Boolean(value)}
.disabled=${this.disabled}
.required=${this.required}
.hideClearIcon=${!this.value && !!this.defaultColor}
.label=${this.label} .label=${this.label}
.value=${value}
.helper=${this.helper} .helper=${this.helper}
.value=${effectiveValue} .disabled=${this.disabled}
.getItems=${this._getItems} @closed=${stopPropagation}
.rowRenderer=${this._rowRenderer} @selected=${this._valueSelected}
.valueRenderer=${this._valueRenderer} fixedMenuPosition
@value-changed=${this._valueChanged} naturalMenuWidth
.clearable=${!this.defaultColor}
> >
</ha-generic-picker> ${value
`; ? html`
} <span slot="icon">
${value === "none"
private _getItems = () => ? html`
this._getColors( <ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
this.includeNone, `
this.includeState, : value === "state"
this.defaultColor, ? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
this.value : this._renderColorCircle(value || "grey")}
); </span>
`
private _getColors = ( : nothing}
includeNone: boolean, ${this.includeNone
includeState: boolean, ? html`
defaultColor: string | undefined, <ha-list-item value="none" graphic="icon">
currentValue: string | undefined
): PickerComboBoxItem[] => {
const items: PickerComboBoxItem[] = [];
const defaultSuffix = this.hass.localize(
"ui.components.color-picker.default"
);
const addDefaultSuffix = (label: string, isDefault: boolean) =>
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
if (includeNone) {
const noneLabel =
this.hass.localize("ui.components.color-picker.none") || "None";
items.push({
id: "none",
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
icon_path: mdiInvertColorsOff,
sorting_label: noneLabel,
});
}
if (includeState) {
const stateLabel =
this.hass.localize("ui.components.color-picker.state") || "State";
items.push({
id: "state",
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
icon_path: mdiPalette,
sorting_label: stateLabel,
});
}
Array.from(THEME_COLORS).forEach((color) => {
const themeLabel =
this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color;
items.push({
id: color,
primary: addDefaultSuffix(themeLabel, defaultColor === color),
sorting_label: themeLabel,
});
});
const isSpecial =
currentValue === "none" ||
currentValue === "state" ||
THEME_COLORS.has(currentValue || "");
const hasValue = currentValue && currentValue.length > 0;
if (hasValue && !isSpecial) {
items.push({
id: currentValue!,
primary: currentValue!,
sorting_label: currentValue!,
});
}
return items;
};
private _rowRenderer: (
item: PickerComboBoxItem,
index?: number
) => ReturnType<typeof html> = (item) => html`
<ha-combo-box-item type="button" compact>
${item.id === "none"
? html`<ha-svg-icon
slot="start"
.path=${mdiInvertColorsOff}
></ha-svg-icon>`
: item.id === "state"
? html`<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>`
: html`<span slot="start">
${this._renderColorCircle(item.id)}
</span>`}
<span slot="headline">${item.primary}</span>
</ha-combo-box-item>
`;
private _valueRenderer: PickerValueRenderer = (value: string) => {
if (value === "none") {
return html`
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.components.color-picker.none")} ${this.hass.localize("ui.components.color-picker.none")}
</span> ${this.defaultColor === "none"
`; ? ` (${this.hass.localize("ui.components.color-picker.default")})`
} : nothing}
if (value === "state") { <ha-svg-icon
return html` slot="graphic"
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon> path=${mdiInvertColorsOff}
<span slot="headline"> ></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState
? html`
<ha-list-item value="state" graphic="icon">
${this.hass.localize("ui.components.color-picker.state")} ${this.hass.localize("ui.components.color-picker.state")}
</span> ${this.defaultColor === "state"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState || this.includeNone
? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing}
${Array.from(THEME_COLORS).map(
(color) => html`
<ha-list-item .value=${color} graphic="icon">
${this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color}
${this.defaultColor === color
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<span slot="graphic">${this._renderColorCircle(color)}</span>
</ha-list-item>
`
)}
${isCustom
? html`
<ha-list-item .value=${value} graphic="icon">
${value}
<span slot="graphic">${this._renderColorCircle(value)}</span>
</ha-list-item>
`
: nothing}
</ha-select>
`; `;
} }
return html`
<span slot="start">${this._renderColorCircle(value)}</span>
<span slot="headline">
${this.hass.localize(
`ui.components.color-picker.colors.${value}` as LocalizeKeys
) || value}
</span>
`;
};
private _renderColorCircle(color: string) { private _renderColorCircle(color: string) {
return html` return html`
<span <span
class="circle-color"
style=${styleMap({ style=${styleMap({
"--circle-color": computeCssColor(color), "--circle-color": computeCssColor(color),
display: "block",
"background-color": "var(--circle-color, var(--divider-color))",
border: "1px solid var(--outline-color)",
"border-radius": "var(--ha-border-radius-pill)",
width: "20px",
height: "20px",
"box-sizing": "border-box",
})} })}
></span> ></span>
`; `;
} }
private _valueChanged(ev: CustomEvent<{ value?: string }>) { static styles = css`
ev.stopPropagation(); .circle-color {
const selected = ev.detail.value; display: block;
const normalized = background-color: var(--circle-color, var(--divider-color));
selected && selected === this.defaultColor border: 1px solid var(--outline-color);
? undefined border-radius: var(--ha-border-radius-pill);
: (selected ?? undefined); width: 20px;
this.value = normalized; height: 20px;
fireEvent(this, "value-changed", { value: this.value }); box-sizing: border-box;
} }
ha-select {
width: 100%;
}
`;
} }
declare global { declare global {

View File

@@ -57,9 +57,10 @@ class HaConfigEntryPicker extends LitElement {
return html` return html`
<ha-generic-picker <ha-generic-picker
.hass=${this.hass} .hass=${this.hass}
.label=${this.label === undefined && this.hass .placeholder=${this.label === undefined && this.hass
? this.hass.localize("ui.components.config-entry-picker.config_entry") ? this.hass.localize("ui.components.config-entry-picker.config_entry")
: this.label} : this.label}
show-label
.value=${this.value} .value=${this.value}
.required=${this.required} .required=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}

View File

@@ -389,14 +389,14 @@ export class HaFloorPicker extends LitElement {
<ha-generic-picker <ha-generic-picker
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.disabled=${this.disabled}
.label=${this.label} .label=${this.label}
.helper=${this.helper} .helper=${this.helper}
.placeholder=${placeholder} .disabled=${this.disabled}
.notFoundLabel=${this._notFoundLabel} .notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize( .emptyLabel=${this.hass.localize(
"ui.components.floor-picker.no_floors" "ui.components.floor-picker.no_floors"
)} )}
.placeholder=${placeholder}
.value=${this.value} .value=${this.value}
.getItems=${this._getItems} .getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}

View File

@@ -7,10 +7,8 @@ import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys"; import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { PickerMixin } from "../mixins/picker-mixin";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm"; import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { isIosApp } from "../util/is_ios";
import "./ha-bottom-sheet"; import "./ha-bottom-sheet";
import "./ha-button"; import "./ha-button";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
@@ -22,18 +20,39 @@ import type {
PickerComboBoxSearchFn, PickerComboBoxSearchFn,
} from "./ha-picker-combo-box"; } from "./ha-picker-combo-box";
import "./ha-picker-field"; import "./ha-picker-field";
import type { PickerValueRenderer } from "./ha-picker-field";
import "./ha-svg-icon"; import "./ha-svg-icon";
@customElement("ha-generic-picker") @customElement("ha-generic-picker")
export class HaGenericPicker extends PickerMixin(LitElement) { export class HaGenericPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "allow-custom-value" }) @property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue; public allowCustomValue;
@property() public value?: string;
@property() public icon?: string;
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: String, attribute: "search-label" }) @property({ type: String, attribute: "search-label" })
public searchLabel?: string; public searchLabel?: string;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "show-label", type: Boolean })
public showLabel = false;
/** To prevent lags, getItems needs to be memoized */ /** To prevent lags, getItems needs to be memoized */
@property({ attribute: false }) @property({ attribute: false })
public getItems!: ( public getItems!: (
@@ -47,6 +66,9 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@property({ attribute: false }) @property({ attribute: false })
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>; public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
@property({ attribute: false })
public valueRenderer?: PickerValueRenderer;
@property({ attribute: false }) @property({ attribute: false })
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>; public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
@@ -96,11 +118,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@property({ attribute: "selected-section" }) public selectedSection?: string; @property({ attribute: "selected-section" }) public selectedSection?: string;
@property({ type: Boolean, attribute: "use-top-label" }) @property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
public useTopLabel = false;
@property({ attribute: "custom-value-label" })
public customValueLabel?: string;
@query(".container") private _containerElement?: HTMLDivElement; @query(".container") private _containerElement?: HTMLDivElement;
@@ -131,13 +149,11 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
private _unsubscribeTinyKeys?: () => void; private _unsubscribeTinyKeys?: () => void;
protected render() { protected render() {
// Only show label if it's not a top label and there is a value. return html`
const label = this.useTopLabel && this.value ? undefined : this.label; ${this.label
return html`<div class="container">
${this.useTopLabel && this.label
? html`<label ?disabled=${this.disabled}>${this.label}</label>` ? html`<label ?disabled=${this.disabled}>${this.label}</label>`
: nothing} : nothing}
<div class="container">
<div id="picker"> <div id="picker">
<slot name="field"> <slot name="field">
${this.addButtonLabel && !this.value ${this.addButtonLabel && !this.value
@@ -157,20 +173,14 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
type="button" type="button"
class=${this._opened ? "opened" : ""} class=${this._opened ? "opened" : ""}
compact compact
.unknown=${this._unknownValue( .unknown=${this._unknownValue(this.value, this.getItems())}
this.allowCustomValue,
this.value,
this.getItems()
)}
.unknownItemText=${this.unknownItemText} .unknownItemText=${this.unknownItemText}
aria-label=${ifDefined(this.label)} aria-label=${ifDefined(this.label)}
@click=${this.open} @click=${this.open}
@clear=${this._clear} @clear=${this._clear}
.icon=${this.icon} .icon=${this.icon}
.image=${this.image} .showLabel=${this.showLabel}
.label=${label}
.placeholder=${this.placeholder} .placeholder=${this.placeholder}
.helper=${this.helper}
.value=${this.value} .value=${this.value}
.valueRenderer=${this.valueRenderer} .valueRenderer=${this.valueRenderer}
.required=${this.required} .required=${this.required}
@@ -178,7 +188,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
.invalid=${this.invalid} .invalid=${this.invalid}
.hideClearIcon=${this.hideClearIcon} .hideClearIcon=${this.hideClearIcon}
> >
<slot name="start"></slot>
</ha-picker-field>`} </ha-picker-field>`}
</slot> </slot>
</div> </div>
@@ -217,7 +226,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
</ha-bottom-sheet>` </ha-bottom-sheet>`
: nothing} : nothing}
</div> </div>
${this._renderHelper()}`; ${this._renderHelper()}
`;
} }
private _renderComboBox(dialogMode = false) { private _renderComboBox(dialogMode = false) {
@@ -226,7 +236,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
} }
return html` return html`
<ha-picker-combo-box <ha-picker-combo-box
id="combo-box"
.hass=${this.hass} .hass=${this.hass}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel} .label=${this.searchLabel}
@@ -243,24 +252,13 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
.sectionTitleFunction=${this.sectionTitleFunction} .sectionTitleFunction=${this.sectionTitleFunction}
.selectedSection=${this.selectedSection} .selectedSection=${this.selectedSection}
.searchKeys=${this.searchKeys} .searchKeys=${this.searchKeys}
.customValueLabel=${this.customValueLabel}
></ha-picker-combo-box> ></ha-picker-combo-box>
`; `;
} }
private _unknownValue = memoizeOne( private _unknownValue = memoizeOne(
( (value?: string, items?: (PickerComboBoxItem | string)[]) => {
allowCustomValue: boolean, if (value === undefined || value === null || value === "" || !items) {
value?: string,
items?: (PickerComboBoxItem | string)[]
) => {
if (
allowCustomValue ||
value === undefined ||
value === null ||
value === "" ||
!items
) {
return false; return false;
} }
@@ -286,15 +284,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
private _dialogOpened = () => { private _dialogOpened = () => {
this._opened = true; this._opened = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (this.hass && isIosApp(this.hass)) {
this.hass.auth.external!.fireMessage({
type: "focus_element",
payload: {
element_id: "combo-box",
},
});
return;
}
this._comboBox?.focus(); this._comboBox?.focus();
}); });
}; };
@@ -321,7 +310,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
this._newValue = value; this._newValue = value;
} }
private _clear(e: CustomEvent) { private _clear(e) {
e.stopPropagation(); e.stopPropagation();
this._setValue(undefined); this._setValue(undefined);
} }

View File

@@ -113,6 +113,7 @@ export class HaIconPicker extends LitElement {
<ha-generic-picker <ha-generic-picker
.hass=${this.hass} .hass=${this.hass}
allow-custom-value allow-custom-value
show-label
.getItems=${this._getIconPickerItems} .getItems=${this._getIconPickerItems}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
@@ -121,7 +122,7 @@ export class HaIconPicker extends LitElement {
.invalid=${this.invalid} .invalid=${this.invalid}
.rowRenderer=${rowRenderer} .rowRenderer=${rowRenderer}
.icon=${this._icon} .icon=${this._icon}
.label=${this.label} .placeholder=${this.label}
.value=${this._value} .value=${this._value}
.searchFn=${this._filterIcons} .searchFn=${this._filterIcons}
.notFoundLabel=${this.hass?.localize( .notFoundLabel=${this.hass?.localize(
@@ -130,7 +131,6 @@ export class HaIconPicker extends LitElement {
popover-placement="bottom-start" popover-placement="bottom-start"
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
<slot name="start"></slot>
</ha-generic-picker> </ha-generic-picker>
`; `;
} }

View File

@@ -116,11 +116,6 @@ export class HaLanguagePicker extends LitElement {
> `; > `;
protected render() { protected render() {
const label =
this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language");
const value = const value =
this.value ?? this.value ??
(this.required && !this.disabled ? this._getItems()[0].id : this.value); (this.required && !this.disabled ? this._getItems()[0].id : this.value);
@@ -134,7 +129,10 @@ export class HaLanguagePicker extends LitElement {
.emptyLabel=${this.hass?.localize( .emptyLabel=${this.hass?.localize(
"ui.components.language-picker.no_languages" "ui.components.language-picker.no_languages"
) || "No languages available"} ) || "No languages available"}
.label=${label} .placeholder=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language")}
show-label
.value=${value} .value=${value}
.valueRenderer=${this._valueRenderer} .valueRenderer=${this._valueRenderer}
.disabled=${this.disabled} .disabled=${this.disabled}

View File

@@ -1,17 +1,55 @@
import { html, LitElement, nothing } from "lit"; import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, state } from "lit/decorators"; import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { titleCase } from "../common/string/title-case"; import { titleCase } from "../common/string/title-case";
import { fetchConfig } from "../data/lovelace/config/types"; import { fetchConfig } from "../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
import { getPanelIcon, getPanelTitle } from "../data/panel"; import { getPanelIcon, getPanelTitle } from "../data/panel";
import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
import "./ha-generic-picker"; import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon"; import "./ha-icon";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
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) || "",
});
@customElement("ha-navigation-picker") @customElement("ha-navigation-picker")
export class HaNavigationPicker extends LitElement { export class HaNavigationPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@@ -23,51 +61,46 @@ export class HaNavigationPicker extends LitElement {
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@state() private _loading = true; @state() private _opened = false;
protected firstUpdated() { private navigationItemsLoaded = false;
private navigationItems: NavigationItem[] = DEFAULT_ITEMS;
@query("ha-combo-box", true) private comboBox!: HaComboBox;
protected render(): TemplateResult {
return html`
<ha-combo-box
.hass=${this.hass}
item-value-path="path"
item-label-path="path"
.value=${this._value}
allow-custom-value
.filteredItems=${this.navigationItems}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
`;
}
private async _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
if (this._opened && !this.navigationItemsLoaded) {
this._loadNavigationItems(); this._loadNavigationItems();
} }
private _navigationItems: PickerComboBoxItem[] = [];
protected render() {
return html`
<ha-generic-picker
.hass=${this.hass}
.value=${this._loading ? undefined : this.value}
allow-custom-value
.placeholder=${this.label}
.helper=${this.helper}
.disabled=${this._loading || this.disabled}
.required=${this.required}
.getItems=${this._getItems}
.valueRenderer=${this._valueRenderer}
.customValueLabel=${this.hass.localize(
"ui.components.navigation-picker.add_custom_path"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
} }
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() { private async _loadNavigationItems() {
this.navigationItemsLoaded = true;
const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({ const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({
id, id,
...panel, ...panel,
@@ -91,47 +124,27 @@ export class HaNavigationPicker extends LitElement {
const panelViewConfig = new Map(viewConfigs); const panelViewConfig = new Map(viewConfigs);
this._navigationItems = []; this.navigationItems = [];
for (const panel of panels) { for (const panel of panels) {
const path = `/${panel.url_path}`; this.navigationItems.push(createPanelNavigationItem(this.hass!, panel));
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); const config = panelViewConfig.get(panel.id);
if (!config || !("views" in config)) continue; if (!config || !("views" in config)) continue;
config.views.forEach((view, index) => { config.views.forEach((view, index) =>
const viewPath = `/${panel.url_path}/${view.path ?? index}`; this.navigationItems.push(
const viewPrimary = createViewNavigationItem(panel.url_path, view, index)
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._loading = false; this.comboBox.filteredItems = this.navigationItems;
}
protected shouldUpdate(changedProps: PropertyValues) {
return !this._opened || changedProps.has("_opened");
} }
private _valueChanged(ev: ValueChangedEvent<string>) { private _valueChanged(ev: ValueChangedEvent<string>) {
@@ -139,18 +152,61 @@ export class HaNavigationPicker extends LitElement {
this._setValue(ev.detail.value); this._setValue(ev.detail.value);
} }
private _setValue(value = "") { private _setValue(value: string) {
this.value = value; this.value = value;
fireEvent( fireEvent(
this, this,
"value-changed", "value-changed",
{ value: this.value }, { value: this._value },
{ {
bubbles: false, bubbles: false,
composed: 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 { declare global {

View File

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

View File

@@ -9,9 +9,7 @@ import {
type TemplateResult, type TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { PickerMixin } from "../mixins/picker-mixin";
import { localizeContext } from "../data/context"; import { localizeContext } from "../data/context";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
@@ -28,7 +26,32 @@ declare global {
export type PickerValueRenderer = (value: string) => TemplateResult<1>; export type PickerValueRenderer = (value: string) => TemplateResult<1>;
@customElement("ha-picker-field") @customElement("ha-picker-field")
export class HaPickerField extends PickerMixin(LitElement) { export class HaPickerField extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public value?: string;
@property() public icon?: string;
@property() public helper?: string;
@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;
@property({ attribute: "show-label", type: Boolean })
public showLabel = false;
@property({ attribute: false })
public valueRenderer?: PickerValueRenderer;
@property({ type: Boolean, reflect: true }) public invalid = false; @property({ type: Boolean, reflect: true }) public invalid = false;
@query("ha-combo-box-item", true) public item!: HaComboBoxItem; @query("ha-combo-box-item", true) public item!: HaComboBoxItem;
@@ -43,48 +66,31 @@ export class HaPickerField extends PickerMixin(LitElement) {
} }
protected render() { protected render() {
const hasValue = !!this.value; const hasValue = !!this.value?.length;
const showClearIcon = const showClearIcon =
!!this.value && !this.required && !this.disabled && !this.hideClearIcon; !!this.value && !this.required && !this.disabled && !this.hideClearIcon;
const placeholderText = this.placeholder ?? this.label;
const overlineLabel = const overlineLabel =
this.label && hasValue this.showLabel && hasValue && this.placeholder
? html`<span slot="overline" ? html`<span slot="overline">${this.placeholder}</span>`
>${this.label}${this.required ? " *" : ""}</span
>`
: nothing; : nothing;
const headlineContent = hasValue const headlineContent = hasValue
? this.valueRenderer ? this.valueRenderer
? this.valueRenderer(this.value ?? "") ? this.valueRenderer(this.value ?? "")
: html`<span slot="headline">${this.value}</span>` : html`<span slot="headline">${this.value}</span>`
: placeholderText : this.placeholder
? html`<span slot="headline" class="placeholder"> ? html`<span slot="headline" class="placeholder">
${placeholderText}${this.required ? " *" : ""} ${this.placeholder}
</span>` </span>`
: nothing; : nothing;
return html` return html`
<ha-combo-box-item <ha-combo-box-item .disabled=${this.disabled} type="button" compact>
aria-label=${ifDefined(this.label || this.placeholder)} ${this.icon
.disabled=${this.disabled}
type="button"
compact
>
${this.image
? html`<img
alt=${this.label ?? ""}
slot="start"
.src=${this.image}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`
: this.icon
? html`<ha-icon slot="start" .icon=${this.icon}></ha-icon>` ? html`<ha-icon slot="start" .icon=${this.icon}></ha-icon>`
: html`<slot name="start"></slot>`} : nothing}
${overlineLabel}${headlineContent} ${overlineLabel}${headlineContent}
${this.unknown ${this.unknown
? html`<div slot="supporting-text" class="unknown"> ? html`<div slot="supporting-text" class="unknown">
@@ -111,7 +117,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
`; `;
} }
private _clear(e: CustomEvent) { private _clear(e) {
e.stopPropagation(); e.stopPropagation();
fireEvent(this, "clear"); fireEvent(this, "clear");
} }

View File

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

View File

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

View File

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

View File

@@ -66,14 +66,15 @@ export class HaEntitySelector extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.value=${this.value} .value=${this.value}
.label=${this.label} .label=${this.label}
.placeholder=${this.placeholder}
.helper=${this.helper} .helper=${this.helper}
.includeEntities=${this.selector.entity?.include_entities} .includeEntities=${this.selector.entity?.include_entities}
.excludeEntities=${this.selector.entity?.exclude_entities} .excludeEntities=${this.selector.entity?.exclude_entities}
.entityFilter=${this._filterEntities} .entityFilter=${this._filterEntities}
.createDomains=${this._createDomains} .createDomains=${this._createDomains}
.placeholder=${this.placeholder}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
allow-custom-entity
></ha-entity-picker>`; ></ha-entity-picker>`;
} }
@@ -82,13 +83,13 @@ export class HaEntitySelector extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.value=${this.value} .value=${this.value}
.label=${this.label} .label=${this.label}
.placeholder=${this.placeholder}
.helper=${this.helper} .helper=${this.helper}
.includeEntities=${this.selector.entity.include_entities} .includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities} .excludeEntities=${this.selector.entity.exclude_entities}
.reorder=${this.selector.entity.reorder ?? false} .reorder=${this.selector.entity.reorder ?? false}
.entityFilter=${this._filterEntities} .entityFilter=${this._filterEntities}
.createDomains=${this._createDomains} .createDomains=${this._createDomains}
.placeholder=${this.placeholder}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
></ha-entities-picker> ></ha-entities-picker>

View File

@@ -52,7 +52,7 @@ export class HaIconSelector extends LitElement {
${!placeholder && stateObj ${!placeholder && stateObj
? html` ? html`
<ha-state-icon <ha-state-icon
slot="start" slot="fallback"
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .stateObj=${stateObj}
></ha-state-icon> ></ha-state-icon>

View File

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

View File

@@ -13,7 +13,6 @@ import { fireEvent } from "../common/dom/fire_event";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin"; import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { isIosApp } from "../util/is_ios";
import "./ha-dialog-header"; import "./ha-dialog-header";
import "./ha-icon-button"; import "./ha-icon-button";
@@ -185,21 +184,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
await this.updateComplete; await this.updateComplete;
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (isIosApp(this.hass)) {
const element = this.querySelector("[autofocus]");
if (element !== null) {
if (!element.id) {
element.id = "ha-wa-dialog-autofocus";
}
this.hass.auth.external!.fireMessage({
type: "focus_element",
payload: {
element_id: element.id,
},
});
}
return;
}
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus(); (this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
}); });
}; };
@@ -251,12 +235,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
); );
max-width: var(--ha-dialog-max-width, var(--safe-width)); max-width: var(--ha-dialog-max-width, var(--safe-width));
} }
@media (prefers-reduced-motion: reduce) {
wa-dialog {
--show-duration: 0ms;
--hide-duration: 0ms;
}
}
:host([width="small"]) wa-dialog { :host([width="small"]) wa-dialog {
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width)); --width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));

View File

@@ -132,9 +132,9 @@ class HaUserPicker extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label} .label=${this.label}
.notFoundLabel=${this._notFoundLabel}
.placeholder=${placeholder} .placeholder=${placeholder}
.value=${this.value} .value=${this.value}
.notFoundLabel=${this._notFoundLabel}
.getItems=${this._getItems} .getItems=${this._getItems}
.valueRenderer=${this._valueRenderer} .valueRenderer=${this._valueRenderer}
.rowRenderer=${this._rowRenderer} .rowRenderer=${this._rowRenderer}

View File

@@ -88,6 +88,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"select", "select",
"text", "text",
"update", "update",
"event",
]; ];
/** Domains that should have the history hidden in the more info dialog. */ /** Domains that should have the history hidden in the more info dialog. */

View File

@@ -176,13 +176,6 @@ interface EMOutgoingMessageAddEntityTo extends EMMessage {
}; };
} }
interface EMOutgoingMessageFocusElement extends EMMessage {
type: "focus_element";
payload: {
element_id: string;
};
}
type EMOutgoingMessageWithoutAnswer = type EMOutgoingMessageWithoutAnswer =
| EMMessageResultError | EMMessageResultError
| EMMessageResultSuccess | EMMessageResultSuccess
@@ -204,8 +197,7 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageThreadStoreInPlatformKeychain | EMOutgoingMessageThreadStoreInPlatformKeychain
| EMOutgoingMessageImprovScan | EMOutgoingMessageImprovScan
| EMOutgoingMessageImprovConfigureDevice | EMOutgoingMessageImprovConfigureDevice
| EMOutgoingMessageAddEntityTo | EMOutgoingMessageAddEntityTo;
| EMOutgoingMessageFocusElement;
export interface EMIncomingMessageRestart { export interface EMIncomingMessageRestart {
id: number; id: number;

View File

@@ -1,38 +0,0 @@
import type { ReactiveElement } from "lit";
import { property } from "lit/decorators";
import type { Constructor } from "../types";
import type { PickerValueRenderer } from "../components/ha-picker-field";
export const PickerMixin = <T extends Constructor<ReactiveElement>>(
superClass: T
) => {
class PickerFieldClass extends superClass {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public icon?: string;
@property() public image?: string;
@property() public label?: string;
@property() public placeholder?: string;
@property() public helper?: string;
@property() public value?: 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;
@property({ attribute: false })
public valueRenderer?: PickerValueRenderer;
}
return PickerFieldClass;
};

View File

@@ -160,7 +160,7 @@ class DialogFloorDetail extends LitElement {
${!this._icon ${!this._icon
? html` ? html`
<ha-floor-icon <ha-floor-icon
slot="start" slot="fallback"
.floor=${{ level: this._level }} .floor=${{ level: this._level }}
></ha-floor-icon> ></ha-floor-icon>
` `

View File

@@ -148,7 +148,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
@value-changed=${this._iconChanged} @value-changed=${this._iconChanged}
> >
<ha-domain-icon <ha-domain-icon
slot="start" slot="fallback"
domain=${this._params.domain} domain=${this._params.domain}
.hass=${this.hass} .hass=${this.hass}
> >
@@ -176,10 +176,8 @@ class DialogAutomationSave extends LitElement implements HassDialog {
id="category" id="category"
.hass=${this.hass} .hass=${this.hass}
.scope=${this._params.domain} .scope=${this._params.domain}
.label=${this.hass.localize(
"ui.components.category-picker.category"
)}
.value=${this._entryUpdates.category} .value=${this._entryUpdates.category}
show-label
@value-changed=${this._registryEntryChanged} @value-changed=${this._registryEntryChanged}
></ha-category-picker>` ></ha-category-picker>`
: nothing} : nothing}
@@ -196,6 +194,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
id="area" id="area"
.hass=${this.hass} .hass=${this.hass}
.value=${this._entryUpdates.area} .value=${this._entryUpdates.area}
show-label
@value-changed=${this._registryEntryChanged} @value-changed=${this._registryEntryChanged}
></ha-area-picker>` ></ha-area-picker>`
: nothing} : nothing}

View File

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

View File

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

View File

@@ -65,9 +65,6 @@ class DialogAssignCategory extends LitElement {
<ha-category-picker <ha-category-picker
.hass=${this.hass} .hass=${this.hass}
.scope=${this._scope} .scope=${this._scope}
.label=${this.hass.localize(
"ui.components.category-picker.category"
)}
.value=${this._category} .value=${this._category}
@value-changed=${this._categoryChanged} @value-changed=${this._categoryChanged}
></ha-category-picker> ></ha-category-picker>

View File

@@ -39,6 +39,9 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean, attribute: "no-add" }) @property({ type: Boolean, attribute: "no-add" })
public noAdd = false; public noAdd = false;
@property({ type: Boolean, attribute: "show-label" })
public showLabel = false;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@@ -180,6 +183,10 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
}; };
protected render(): TemplateResult { protected render(): TemplateResult {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.category-picker.category");
const valueRenderer = this._computeValueRenderer(this._categories); const valueRenderer = this._computeValueRenderer(this._categories);
return html` return html`
@@ -187,12 +194,13 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass} .hass=${this.hass}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label} .label=${this.label}
.placeholder=${this.placeholder}
.value=${this.value}
.notFoundLabel=${this._notFoundLabel} .notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize( .emptyLabel=${this.hass.localize(
"ui.components.category-picker.no_categories" "ui.components.category-picker.no_categories"
)} )}
.placeholder=${placeholder}
.showLabel=${this.showLabel}
.value=${this.value}
.getItems=${this._getItems} .getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer} .valueRenderer=${valueRenderer}

View File

@@ -80,6 +80,7 @@ class DialogDeviceRegistryDetail extends LitElement {
<ha-area-picker <ha-area-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._areaId} .value=${this._areaId}
show-label
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker> ></ha-area-picker>
<ha-labels-picker <ha-labels-picker

View File

@@ -403,7 +403,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
${!this._icon && !stateObj?.attributes.icon && stateObj ${!this._icon && !stateObj?.attributes.icon && stateObj
? html` ? html`
<ha-state-icon <ha-state-icon
slot="start" slot="fallback"
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .stateObj=${stateObj}
></ha-state-icon> ></ha-state-icon>
@@ -778,6 +778,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.value=${this._areaId} .value=${this._areaId}
.disabled=${this.disabled} .disabled=${this.disabled}
show-label
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker>` ></ha-area-picker>`
: ""} : ""}
@@ -1012,6 +1013,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
? html`<ha-area-picker ? html`<ha-area-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._areaId} .value=${this._areaId}
show-label
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker>` ></ha-area-picker>`
@@ -1543,12 +1545,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
margin-inline-end: 0; margin-inline-end: 0;
margin-inline-start: initial; margin-inline-start: initial;
} }
ha-settings-row {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--ha-space-4);
align-items: start;
}
ha-textfield, ha-textfield,
ha-icon-picker, ha-icon-picker,
ha-select, ha-select,

View File

@@ -16,14 +16,13 @@ import "../../../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button"; import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert"; import "../../../../../components/ha-alert";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../../../components/ha-generic-picker";
import "../../../../../components/ha-list-item"; import "../../../../../components/ha-list-item";
import type { PickerComboBoxItem } from "../../../../../components/ha-picker-combo-box";
import "../../../../../components/ha-select"; import "../../../../../components/ha-select";
import "../../../../../components/ha-selector/ha-selector-boolean"; import "../../../../../components/ha-selector/ha-selector-boolean";
import "../../../../../components/ha-settings-row"; import "../../../../../components/ha-settings-row";
import "../../../../../components/ha-svg-icon"; import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-textfield"; import "../../../../../components/ha-textfield";
import "../../../../../components/ha-combo-box";
import type { import type {
ZWaveJSNodeCapabilities, ZWaveJSNodeCapabilities,
ZWaveJSNodeConfigParam, ZWaveJSNodeConfigParam,
@@ -330,22 +329,19 @@ class ZWaveJSNodeConfig extends LitElement {
) { ) {
return html` return html`
${labelAndDescription} ${labelAndDescription}
<ha-generic-picker <ha-combo-box
.hass=${this.hass} .hass=${this.hass}
.value=${item.value?.toString()} .value=${item.value?.toString()}
allow-custom-value allow-custom-value
hide-clear-icon hide-clear-icon
.getItems=${this._getManualEntryItems(item.metadata.states)} .items=${this._getComboBoxOptions(item.metadata.states)}
.disabled=${!item.metadata.writeable} .disabled=${!item.metadata.writeable}
.invalid=${result?.status === "error"} .invalid=${result?.status === "error"}
.placeholder=${item.metadata.unit} .placeholder=${item.metadata.unit}
.helper=${`${this.hass.localize("ui.panel.config.zwave_js.node_config.between_min_max", { min: item.metadata.min, max: item.metadata.max })}${defaultLabel ? `, ${defaultLabel}` : ""}`} .helper=${`${this.hass.localize("ui.panel.config.zwave_js.node_config.between_min_max", { min: item.metadata.min, max: item.metadata.max })}${defaultLabel ? `, ${defaultLabel}` : ""}`}
.valueRenderer=${this._enumeratedPickerValueRenderer(
item.metadata.states
)}
@value-changed=${this._getComboBoxValueChangedCallback(id, item)} @value-changed=${this._getComboBoxValueChangedCallback(id, item)}
> >
</ha-generic-picker> </ha-combo-box>
`; `;
} }
return html`${labelAndDescription} return html`${labelAndDescription}
@@ -367,10 +363,7 @@ class ZWaveJSNodeConfig extends LitElement {
</ha-textfield>`; </ha-textfield>`;
} }
if ( if (item.configuration_value_type === "enumerated") {
item.configuration_value_type === "enumerated" &&
Object.keys(item.metadata.states).length < 5
) {
return html` return html`
${labelAndDescription} ${labelAndDescription}
<ha-select <ha-select
@@ -392,28 +385,6 @@ class ZWaveJSNodeConfig extends LitElement {
</ha-select> </ha-select>
`; `;
} }
if (item.configuration_value_type === "enumerated") {
return html`
${labelAndDescription}
<ha-generic-picker
.hass=${this.hass}
.disabled=${!item.metadata.writeable}
.value=${item.value?.toString()}
.key=${id}
hide-clear-icon
@value-changed=${this._pickerValueChanged}
.helper=${defaultLabel}
.getItems=${this._getEnumeratedPickerItems(item.metadata.states!)}
.valueRenderer=${this._enumeratedPickerValueRenderer(
item.metadata.states!
)}
.property=${item.property}
.endpoint=${item.endpoint}
.propertyKey=${item.property_key}
>
</ha-generic-picker>
`;
}
return html`${labelAndDescription} return html`${labelAndDescription}
<p>${item.value}</p>`; <p>${item.value}</p>`;
@@ -458,23 +429,15 @@ class ZWaveJSNodeConfig extends LitElement {
} }
private _dropdownSelected(ev) { private _dropdownSelected(ev) {
this._handleEnumeratedPickerValueChanged(ev, ev.target.value);
}
private _pickerValueChanged(ev) {
this._handleEnumeratedPickerValueChanged(ev, ev.detail.value);
}
private _handleEnumeratedPickerValueChanged(ev, value: string) {
if (ev.target === undefined || this._config![ev.target.key] === undefined) { if (ev.target === undefined || this._config![ev.target.key] === undefined) {
return; return;
} }
if (this._config![ev.target.key].value?.toString() === value) { if (this._config![ev.target.key].value?.toString() === ev.target.value) {
return; return;
} }
this._setResult(ev.target.key, undefined); this._setResult(ev.target.key, undefined);
this._updateConfigParameter(ev.target, Number(value)); this._updateConfigParameter(ev.target, Number(ev.target.value));
} }
private _numericInputChanged(ev) { private _numericInputChanged(ev) {
@@ -511,36 +474,11 @@ class ZWaveJSNodeConfig extends LitElement {
this._updateConfigParameter(ev.target, value); this._updateConfigParameter(ev.target, value);
} }
private _getEnumeratedPickerItems = memoizeOne( private _getComboBoxOptions = memoizeOne((states: Record<string, string>) =>
(states: Record<string, string>) => { Object.entries(states).map(([value, label]) => ({
const items: PickerComboBoxItem[] = Object.entries(states).map( value,
([value, label]) => ({ label: `${value} - ${label}`,
id: value, }))
primary: label,
sorting_label: `${label}_${value}`,
})
);
return () => items;
}
);
private _enumeratedPickerValueRenderer = memoizeOne(
(states: Record<string, string>) => (value: string) =>
html`<span slot="headline">${states[value] || value}</span>`
);
private _getManualEntryItems = memoizeOne(
(states: Record<string, string>) => {
const items: PickerComboBoxItem[] = Object.entries(states).map(
([value, label]) => ({
id: value,
primary: `${label}`,
secondary: value,
sorting_label: `${label}_${value}`,
})
);
return () => items;
}
); );
private _getComboBoxValueChangedCallback( private _getComboBoxValueChangedCallback(

View File

@@ -5,10 +5,9 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-color-picker"; import "../../../components/ha-color-picker";
import "../../../components/ha-dialog-footer"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-icon-picker"; import "../../../components/ha-icon-picker";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-textarea"; import "../../../components/ha-textarea";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import type { LabelRegistryEntryMutableParams } from "../../../data/label/label_registry"; import type { LabelRegistryEntryMutableParams } from "../../../data/label/label_registry";
@@ -38,8 +37,6 @@ class DialogLabelDetail
@state() private _submitting = false; @state() private _submitting = false;
@state() private _open = false;
public showDialog(params: LabelDetailDialogParams): void { public showDialog(params: LabelDetailDialogParams): void {
this._params = params; this._params = params;
this._error = undefined; this._error = undefined;
@@ -54,17 +51,20 @@ class DialogLabelDetail
this._color = ""; this._color = "";
this._description = ""; this._description = "";
} }
this._open = true; document.body.addEventListener("keydown", this._handleKeyPress);
} }
private _handleKeyPress = (ev: KeyboardEvent) => {
if (ev.key === "Escape") {
ev.stopPropagation();
}
};
public closeDialog() { public closeDialog() {
this._open = false;
return true;
}
private _dialogClosed(): void {
this._params = undefined; this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
document.body.removeEventListener("keydown", this._handleKeyPress);
return true;
} }
protected render() { protected render() {
@@ -73,13 +73,17 @@ class DialogLabelDetail
} }
return html` return html`
<ha-wa-dialog <ha-dialog
.hass=${this.hass} open
.open=${this._open} @closed=${this.closeDialog}
header-title=${this._params.entry scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this._params.entry
? this._params.entry.name || this._params.entry.label_id ? this._params.entry.name || this._params.entry.label_id
: this.hass!.localize("ui.dialogs.label-detail.new_label")} : this.hass!.localize("ui.dialogs.label-detail.new_label")
@closed=${this._dialogClosed} )}
> >
<div> <div>
${this._error ${this._error
@@ -87,7 +91,7 @@ class DialogLabelDetail
: ""} : ""}
<div class="form"> <div class="form">
<ha-textfield <ha-textfield
autofocus dialogInitialFocus
.value=${this._name} .value=${this._name}
.configValue=${"name"} .configValue=${"name"}
@input=${this._input} @input=${this._input}
@@ -121,8 +125,6 @@ class DialogLabelDetail
></ha-textarea> ></ha-textarea>
</div> </div>
</div> </div>
<ha-dialog-footer slot="footer">
${this._params.entry && this._params.removeEntry ${this._params.entry && this._params.removeEntry
? html` ? html`
<ha-button <ha-button
@@ -145,8 +147,7 @@ class DialogLabelDetail
? this.hass!.localize("ui.common.update") ? this.hass!.localize("ui.common.update")
: this.hass!.localize("ui.common.create")} : this.hass!.localize("ui.common.create")}
</ha-button> </ha-button>
</ha-dialog-footer> </ha-dialog>
</ha-wa-dialog>
`; `;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1308,9 +1308,7 @@
"combo-box": { "combo-box": {
"no_match": "No matching items found", "no_match": "No matching items found",
"no_items": "No items available", "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": { "suggest_with_ai": {
"label": "Suggest", "label": "Suggest",
@@ -1319,9 +1317,6 @@
"suggesting_3": "Enchanting…", "suggesting_3": "Enchanting…",
"done": "Done!", "done": "Done!",
"error": "Fail!" "error": "Fail!"
},
"navigation-picker": {
"add_custom_path": "Add custom path"
} }
}, },
"dialogs": { "dialogs": {

1281
yarn.lock

File diff suppressed because it is too large Load Diff