mirror of
https://github.com/home-assistant/frontend.git
synced 2025-10-24 02:59:47 +00:00
Compare commits
10 Commits
dev
...
combo-box-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b04a9a7ae0 | ||
|
|
141e5fb273 | ||
|
|
958d1ecb21 | ||
|
|
b2a615ba76 | ||
|
|
1ba43a2fbb | ||
|
|
589645e70e | ||
|
|
dc94c1271f | ||
|
|
ab35cfa742 | ||
|
|
74e5e24f95 | ||
|
|
5df982095b |
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@home-assistant/webawesome": "3.0.0-beta.6.ha.5",
|
||||
"@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
|
||||
"@lezer/highlight": "1.2.2",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
@@ -203,7 +203,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "27.0.1",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.5",
|
||||
"lint-staged": "16.2.4",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -217,7 +217,7 @@
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.46.2",
|
||||
"typescript-eslint": "8.46.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -88,19 +88,9 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _lastTapTime?: number;
|
||||
|
||||
private _shouldResizeChart = false;
|
||||
|
||||
// @ts-ignore
|
||||
private _resizeController = new ResizeController(this, {
|
||||
callback: () => {
|
||||
if (this.chart) {
|
||||
if (!this.chart.getZr().animation.isFinished()) {
|
||||
this._shouldResizeChart = true;
|
||||
} else {
|
||||
this.chart.resize();
|
||||
}
|
||||
}
|
||||
},
|
||||
callback: () => this.chart?.resize(),
|
||||
});
|
||||
|
||||
private _loading = false;
|
||||
@@ -376,7 +366,6 @@ export class HaChartBase extends LitElement {
|
||||
if (!this.options?.dataZoom) {
|
||||
this.chart.getZr().on("dblclick", this._handleClickZoom);
|
||||
}
|
||||
this.chart.on("finished", this._handleChartRenderFinished);
|
||||
if (this._isTouchDevice) {
|
||||
this.chart.getZr().on("click", (e: ECElementEvent) => {
|
||||
if (!e.zrByTouch) {
|
||||
@@ -956,13 +945,6 @@ export class HaChartBase extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleChartRenderFinished = () => {
|
||||
if (this._shouldResizeChart) {
|
||||
this.chart?.resize();
|
||||
this._shouldResizeChart = false;
|
||||
}
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
@@ -147,6 +147,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
.createDomains=${this.createDomains}
|
||||
.required=${this.required && !currentEntities.length}
|
||||
@value-changed=${this._addEntity}
|
||||
add-button
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -113,6 +113,9 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@property({ attribute: "add-button", type: Boolean })
|
||||
public addButton = false;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
@@ -281,7 +284,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${notFoundLabel}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.value=${this.addButton ? undefined : this.value}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
@@ -289,6 +292,9 @@ export class HaEntityPicker extends LitElement {
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
.addButtonLabel=${this.addButton
|
||||
? this.hass.localize("ui.components.entity.entity-picker.add")
|
||||
: undefined}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
|
||||
@@ -6,9 +6,6 @@ export class HaDialogHeader extends LitElement {
|
||||
@property({ type: String, attribute: "subtitle-position" })
|
||||
public subtitlePosition: "above" | "below" = "below";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "show-border" })
|
||||
public showBorder = false;
|
||||
|
||||
protected render() {
|
||||
const titleSlot = html`<div class="header-title">
|
||||
<slot name="title"></slot>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
@@ -15,7 +19,7 @@ import type {
|
||||
PickerComboBoxSearchFn,
|
||||
} from "./ha-picker-combo-box";
|
||||
import "./ha-picker-field";
|
||||
import type { HaPickerField, PickerValueRenderer } from "./ha-picker-field";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-generic-picker")
|
||||
@@ -53,7 +57,7 @@ export class HaGenericPicker extends LitElement {
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public rowRenderer?: ComboBoxLitRenderer<PickerComboBoxItem>;
|
||||
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public valueRenderer?: PickerValueRenderer;
|
||||
@@ -64,59 +68,130 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: "not-found-label", type: String })
|
||||
public notFoundLabel?: string;
|
||||
|
||||
@query("ha-picker-field") private _field?: HaPickerField;
|
||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
@query(".container") private _containerElement?: HTMLDivElement;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _pickerWrapperOpen = false;
|
||||
|
||||
@state() private _popoverWidth = 0;
|
||||
|
||||
@state() private _openedNarrow = false;
|
||||
|
||||
private _narrow = false;
|
||||
|
||||
// helper to set new value after closing picker, to avoid flicker
|
||||
private _newValue?: string;
|
||||
|
||||
private _unsubscribeTinyKeys?: () => void;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label
|
||||
? html`<label ?disabled=${this.disabled}>${this.label}</label>`
|
||||
: nothing}
|
||||
<div class="container">
|
||||
${!this._opened
|
||||
<div id="picker">
|
||||
<slot name="field">
|
||||
${this.addButtonLabel && !this.value
|
||||
? html`<ha-button
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this.open}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiPlaylistPlus}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.addButtonLabel}
|
||||
</ha-button>`
|
||||
: html`<ha-picker-field
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@click=${this.open}
|
||||
@clear=${this._clear}
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
>
|
||||
</ha-picker-field>`}
|
||||
</slot>
|
||||
</div>
|
||||
${!this._openedNarrow && (this._pickerWrapperOpen || this._opened)
|
||||
? html`
|
||||
<ha-picker-field
|
||||
id="picker"
|
||||
type="button"
|
||||
compact
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@click=${this.open}
|
||||
@clear=${this._clear}
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
<wa-popover
|
||||
.open=${this._pickerWrapperOpen}
|
||||
style="--body-width: ${this._popoverWidth}px;"
|
||||
without-arrow
|
||||
distance="-4"
|
||||
placement="bottom-start"
|
||||
for="picker"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@wa-after-hide=${this._hidePicker}
|
||||
trap-focus
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_target"
|
||||
)}
|
||||
>
|
||||
</ha-picker-field>
|
||||
${this._renderComboBox()}
|
||||
</wa-popover>
|
||||
`
|
||||
: html`
|
||||
<ha-picker-combo-box
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.searchLabel ??
|
||||
this.hass.localize("ui.common.search")}
|
||||
.value=${this.value}
|
||||
hide-clear-icon
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
.rowRenderer=${this.rowRenderer}
|
||||
.notFoundLabel=${this.notFoundLabel}
|
||||
.getItems=${this.getItems}
|
||||
.getAdditionalItems=${this.getAdditionalItems}
|
||||
.searchFn=${this.searchFn}
|
||||
></ha-picker-combo-box>
|
||||
`}
|
||||
: this._pickerWrapperOpen || this._opened
|
||||
? html`<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._pickerWrapperOpen}
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@closed=${this._hidePicker}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_target"
|
||||
)}
|
||||
>
|
||||
${this._renderComboBox(true)}
|
||||
</ha-bottom-sheet>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderComboBox(dialogMode = false) {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-picker-combo-box
|
||||
.hass=${this.hass}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.searchLabel ?? this.hass.localize("ui.common.search")}
|
||||
.value=${this.value}
|
||||
@value-changed=${this._valueChanged}
|
||||
.rowRenderer=${this.rowRenderer}
|
||||
.notFoundLabel=${this.notFoundLabel}
|
||||
.getItems=${this.getItems}
|
||||
.getAdditionalItems=${this.getAdditionalItems}
|
||||
.searchFn=${this.searchFn}
|
||||
.mode=${dialogMode ? "dialog" : "popover"}
|
||||
></ha-picker-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
@@ -125,13 +200,33 @@ export class HaGenericPicker extends LitElement {
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _dialogOpened = () => {
|
||||
this._opened = true;
|
||||
requestAnimationFrame(() => {
|
||||
this._comboBox?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
private _hidePicker(ev) {
|
||||
ev.stopPropagation();
|
||||
if (this._newValue) {
|
||||
fireEvent(this, "value-changed", { value: this._newValue });
|
||||
this._newValue = undefined;
|
||||
}
|
||||
|
||||
this._opened = false;
|
||||
this._pickerWrapperOpen = false;
|
||||
this._unsubscribeTinyKeys?.();
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
this._pickerWrapperOpen = false;
|
||||
this._newValue = value;
|
||||
}
|
||||
|
||||
private _clear(e) {
|
||||
@@ -144,25 +239,45 @@ export class HaGenericPicker extends LitElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
public async open() {
|
||||
public async open(ev?: Event) {
|
||||
ev?.stopPropagation();
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this._opened = true;
|
||||
await this.updateComplete;
|
||||
this._comboBox?.focus();
|
||||
this._comboBox?.open();
|
||||
this._openedNarrow = this._narrow;
|
||||
this._popoverWidth = this._containerElement?.offsetWidth || 250;
|
||||
this._pickerWrapperOpen = true;
|
||||
this._unsubscribeTinyKeys = tinykeys(this, {
|
||||
Escape: this._handleEscClose,
|
||||
});
|
||||
}
|
||||
|
||||
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||
const opened = ev.detail.value;
|
||||
if (this._opened && !opened) {
|
||||
this._opened = false;
|
||||
await this.updateComplete;
|
||||
this._field?.focus();
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._handleResize();
|
||||
window.addEventListener("resize", this._handleResize);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("resize", this._handleResize);
|
||||
this._unsubscribeTinyKeys?.();
|
||||
}
|
||||
|
||||
private _handleResize = () => {
|
||||
this._narrow =
|
||||
window.matchMedia("(max-width: 870px)").matches ||
|
||||
window.matchMedia("(max-height: 500px)").matches;
|
||||
|
||||
if (!this._openedNarrow && this._pickerWrapperOpen) {
|
||||
this._popoverWidth = this._containerElement?.offsetWidth || 250;
|
||||
}
|
||||
};
|
||||
|
||||
private _handleEscClose = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
@@ -181,6 +296,44 @@ export class HaGenericPicker extends LitElement {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
|
||||
wa-popover {
|
||||
--wa-space-l: var(--ha-space-0);
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
width: max(var(--body-width), 250px);
|
||||
max-width: max(var(--body-width), 250px);
|
||||
max-height: 500px;
|
||||
height: 70vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-height: 1000px) {
|
||||
wa-popover::part(body) {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 1000px) {
|
||||
wa-popover::part(body) {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-bottom-sheet {
|
||||
--ha-bottom-sheet-height: 90vh;
|
||||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
||||
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
|
||||
--ha-bottom-sheet-max-width: 600px;
|
||||
--ha-bottom-sheet-padding: var(--ha-space-0);
|
||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||||
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
|
||||
}
|
||||
|
||||
ha-picker-field.opened {
|
||||
--mdc-text-field-idle-line-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,13 @@ import { mdiLabel, mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAssignedElements,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LabelRegistryEntry } from "../data/label_registry";
|
||||
@@ -84,6 +90,9 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _labels?: LabelRegistryEntry[];
|
||||
|
||||
@queryAssignedElements({ flatten: true })
|
||||
private _slotNodes?: NodeListOf<HTMLElement>;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
public async open() {
|
||||
@@ -211,12 +220,14 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.label-picker.no_match"
|
||||
)}
|
||||
.addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
@@ -224,6 +235,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.valueRenderer=${valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -123,36 +124,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
${labels?.length
|
||||
? html`<ha-chip-set>
|
||||
${repeat(
|
||||
labels,
|
||||
(label) => label?.label_id,
|
||||
(label) => {
|
||||
const color = label?.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.item=${label}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._openDetail}
|
||||
.label=${label?.name}
|
||||
selected
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
>
|
||||
${label?.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${label.icon}
|
||||
></ha-icon>`
|
||||
: nothing}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>`
|
||||
: nothing}
|
||||
<ha-label-picker
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
@@ -162,6 +133,47 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
.excludeLabels=${this.value}
|
||||
@value-changed=${this._labelChanged}
|
||||
>
|
||||
<ha-chip-set>
|
||||
${labels?.length
|
||||
? repeat(
|
||||
labels,
|
||||
(label) => label?.label_id,
|
||||
(label) => {
|
||||
const color = label?.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.item=${label}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._openDetail}
|
||||
.disabled=${this.disabled}
|
||||
.label=${label?.name}
|
||||
selected
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
>
|
||||
${label?.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${label.icon}
|
||||
></ha-icon>`
|
||||
: nothing}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)
|
||||
: nothing}
|
||||
<ha-button
|
||||
id="picker"
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this._openPicker}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize("ui.components.label-picker.add")}
|
||||
</ha-button>
|
||||
</ha-chip-set>
|
||||
</ha-label-picker>
|
||||
`;
|
||||
}
|
||||
@@ -203,9 +215,25 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this.labelPicker.open();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-chip-set {
|
||||
margin-bottom: 8px;
|
||||
background-color: var(--mdc-text-field-fill-color);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
.placeholder {
|
||||
color: var(--mdc-text-field-label-ink-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--ha-space-8);
|
||||
}
|
||||
ha-input-chip {
|
||||
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import Fuse from "fuse.js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HaFuse } from "../resources/fuse";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
export interface PickerComboBoxItem {
|
||||
id: string;
|
||||
@@ -33,10 +42,13 @@ export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
|
||||
|
||||
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
|
||||
|
||||
const DEFAULT_ROW_RENDERER: ComboBoxLitRenderer<PickerComboBoxItem> = (
|
||||
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
||||
item
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
<ha-combo-box-item
|
||||
.type=${item.id === NO_MATCHING_ITEMS_FOUND_ID ? "text" : "button"}
|
||||
compact
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: item.icon_path
|
||||
@@ -73,7 +85,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
@state() private _listScrolled = false;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getItems?: () => PickerComboBoxItem[];
|
||||
@@ -82,10 +94,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public rowRenderer?: ComboBoxLitRenderer<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: "not-found-label", type: String })
|
||||
public notFoundLabel?: string;
|
||||
@@ -93,23 +102,59 @@ export class HaPickerComboBox extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||
|
||||
@state() private _opened = false;
|
||||
@property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@state() private _items: PickerComboBoxItemWithLabel[] = [];
|
||||
|
||||
private _allItems: PickerComboBoxItemWithLabel[] = [];
|
||||
|
||||
private _selectedItemIndex = -1;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
private _removeKeyboardShortcuts?: () => void;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._registerKeyboardShortcuts();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.focus();
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
this._allItems = this._getItems();
|
||||
this._items = this._allItems;
|
||||
}
|
||||
}
|
||||
|
||||
private _initialItems = false;
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._removeKeyboardShortcuts?.();
|
||||
}
|
||||
|
||||
private _items: PickerComboBoxItemWithLabel[] = [];
|
||||
protected render() {
|
||||
return html`<ha-textfield
|
||||
.label=${this.label ?? this.hass.localize("ui.common.search")}
|
||||
@input=${this._filterChanged}
|
||||
></ha-textfield>
|
||||
<lit-virtualizer
|
||||
@scroll=${this._onScrollList}
|
||||
tabindex="0"
|
||||
scroller
|
||||
.items=${this._items}
|
||||
.renderItem=${this._renderItem}
|
||||
style="min-height: 36px;"
|
||||
class=${this._listScrolled ? "scrolled" : ""}
|
||||
@focus=${this._focusList}
|
||||
>
|
||||
</lit-virtualizer> `;
|
||||
}
|
||||
|
||||
private _defaultNotFoundItem = memoizeOne(
|
||||
(
|
||||
@@ -159,94 +204,56 @@ export class HaPickerComboBox extends LitElement {
|
||||
return sortedItems;
|
||||
};
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
changedProps.has("value") ||
|
||||
changedProps.has("label") ||
|
||||
changedProps.has("disabled")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return !(!changedProps.has("_opened") && this._opened);
|
||||
}
|
||||
private _renderItem = (item: PickerComboBoxItem, index: number) => {
|
||||
const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
|
||||
return html`<div
|
||||
id=${`list-item-${index}`}
|
||||
class="combo-box-row ${this._value === item.id ? "current-value" : ""}"
|
||||
.value=${item.id}
|
||||
.index=${index}
|
||||
@click=${this._valueSelected}
|
||||
>
|
||||
${item.id === NO_MATCHING_ITEMS_FOUND_ID
|
||||
? DEFAULT_ROW_RENDERER(item, index)
|
||||
: renderer(item, index)}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (changedProps.has("_opened") && this._opened) {
|
||||
this._items = this._getItems();
|
||||
if (this._initialItems) {
|
||||
this.comboBox.filteredItems = this._items;
|
||||
}
|
||||
this._initialItems = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
item-id-path="id"
|
||||
item-value-path="id"
|
||||
item-label-path="a11y_label"
|
||||
clear-initial-value
|
||||
.hass=${this.hass}
|
||||
.value=${this._value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.filteredItems=${this._items}
|
||||
.renderer=${this.rowRenderer || DEFAULT_ROW_RENDERER}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
`;
|
||||
@eventOptions({ passive: true })
|
||||
private _onScrollList(ev) {
|
||||
const top = ev.target.scrollTop ?? 0;
|
||||
this._listScrolled = top > 0;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
private _valueSelected = (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.value !== this._opened) {
|
||||
this._opened = ev.detail.value;
|
||||
fireEvent(this, "opened-changed", { value: this._opened });
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||
ev.stopPropagation();
|
||||
// Clear the input field to prevent showing the old value next time
|
||||
this.comboBox.setTextFieldValue("");
|
||||
const newValue = ev.detail.value?.trim();
|
||||
const value = (ev.currentTarget as any).value as string;
|
||||
const newValue = value?.trim();
|
||||
|
||||
if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
};
|
||||
|
||||
private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
Fuse.createIndex(["search_labels"], states)
|
||||
);
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
if (!this._opened) return;
|
||||
private _filterChanged = (ev: Event) => {
|
||||
const textfield = ev.target as HaTextField;
|
||||
const searchString = textfield.value.trim();
|
||||
|
||||
const target = ev.target as HaComboBox;
|
||||
const searchString = ev.detail.value.trim() as string;
|
||||
|
||||
const index = this._fuseIndex(this._items);
|
||||
const fuse = new HaFuse(this._items, { shouldSort: false }, index);
|
||||
const index = this._fuseIndex(this._allItems);
|
||||
const fuse = new HaFuse(this._allItems, { shouldSort: false }, index);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchString);
|
||||
let filteredItems = this._items as PickerComboBoxItem[];
|
||||
let filteredItems = this._allItems as PickerComboBoxItem[];
|
||||
if (results) {
|
||||
const items = results.map((result) => result.item);
|
||||
if (items.length === 0) {
|
||||
@@ -260,17 +267,266 @@ export class HaPickerComboBox extends LitElement {
|
||||
}
|
||||
|
||||
if (this.searchFn) {
|
||||
filteredItems = this.searchFn(searchString, filteredItems, this._items);
|
||||
filteredItems = this.searchFn(
|
||||
searchString,
|
||||
filteredItems,
|
||||
this._allItems
|
||||
);
|
||||
}
|
||||
|
||||
target.filteredItems = filteredItems;
|
||||
this._items = filteredItems as PickerComboBoxItemWithLabel[];
|
||||
this._selectedItemIndex = -1;
|
||||
if (this._virtualizerElement) {
|
||||
this._virtualizerElement.scrollTo(0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
private _registerKeyboardShortcuts() {
|
||||
this._removeKeyboardShortcuts = tinykeys(this, {
|
||||
ArrowUp: this._selectPreviousItem,
|
||||
ArrowDown: this._selectNextItem,
|
||||
Home: this._selectFirstItem,
|
||||
End: this._selectLastItem,
|
||||
Enter: this._pickSelectedItem,
|
||||
});
|
||||
}
|
||||
|
||||
private _setValue(value: string | undefined) {
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}, 0);
|
||||
private _focusList() {
|
||||
if (this._selectedItemIndex === -1) {
|
||||
this._selectNextItem();
|
||||
}
|
||||
}
|
||||
|
||||
private _selectNextItem = (ev?: KeyboardEvent) => {
|
||||
ev?.stopPropagation();
|
||||
ev?.preventDefault();
|
||||
if (!this._virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._searchFieldElement?.focus();
|
||||
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
const maxItems = items.length - 1;
|
||||
|
||||
if (maxItems === -1) {
|
||||
this._resetSelectedItem();
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex =
|
||||
maxItems === this._selectedItemIndex
|
||||
? this._selectedItemIndex
|
||||
: this._selectedItemIndex + 1;
|
||||
|
||||
if (!items[nextIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (items[nextIndex].id === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
// Skip titles, padding and empty search
|
||||
if (nextIndex === maxItems) {
|
||||
return;
|
||||
}
|
||||
this._selectedItemIndex = nextIndex + 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
}
|
||||
|
||||
this._scrollToSelectedItem();
|
||||
};
|
||||
|
||||
private _selectPreviousItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!this._virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex > 0) {
|
||||
const nextIndex = this._selectedItemIndex - 1;
|
||||
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
if (!items[nextIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (items[nextIndex]?.id === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
// Skip titles, padding and empty search
|
||||
if (nextIndex === 0) {
|
||||
return;
|
||||
}
|
||||
this._selectedItemIndex = nextIndex - 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
}
|
||||
|
||||
this._scrollToSelectedItem();
|
||||
}
|
||||
};
|
||||
|
||||
private _selectFirstItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = 0;
|
||||
|
||||
if (
|
||||
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
|
||||
NO_MATCHING_ITEMS_FOUND_ID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex + 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
}
|
||||
|
||||
this._scrollToSelectedItem();
|
||||
};
|
||||
|
||||
private _selectLastItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = this._virtualizerElement.items.length - 1;
|
||||
|
||||
if (
|
||||
(this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
|
||||
NO_MATCHING_ITEMS_FOUND_ID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex - 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
}
|
||||
|
||||
this._scrollToSelectedItem();
|
||||
};
|
||||
|
||||
private _scrollToSelectedItem = () => {
|
||||
this._virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
|
||||
this._virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this._virtualizerElement
|
||||
?.querySelector(`#list-item-${this._selectedItemIndex}`)
|
||||
?.classList.add("selected");
|
||||
});
|
||||
};
|
||||
|
||||
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (this._selectedItemIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if filter button is focused
|
||||
ev.preventDefault();
|
||||
|
||||
const item: any = this._virtualizerElement?.items[this._selectedItemIndex];
|
||||
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
|
||||
fireEvent(this, "value-changed", { value: item.id });
|
||||
}
|
||||
};
|
||||
|
||||
private _resetSelectedItem() {
|
||||
this._virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
this._selectedItemIndex = -1;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: var(--ha-space-3);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
padding: 0 var(--ha-space-3);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-textfield {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-combo-box-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-combo-box-item.selected {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-hover);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
ha-combo-box-item.selected {
|
||||
background-color: var(--ha-color-fill-neutral-normal-hover);
|
||||
}
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
lit-virtualizer:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
lit-virtualizer.scrolled {
|
||||
border-top: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
|
||||
.bottom-padding {
|
||||
height: max(var(--safe-area-inset-bottom, 0px), var(--ha-space-8));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.combo-box-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
}
|
||||
.combo-box-row.current-value {
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
}
|
||||
|
||||
.combo-box-row.selected {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-hover);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.combo-box-row.selected {
|
||||
background-color: var(--ha-color-fill-neutral-normal-hover);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -53,7 +53,7 @@ class HaServicePicker extends LitElement {
|
||||
item,
|
||||
{ index }
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" border-top .borderTop=${index !== 0}>
|
||||
<ha-combo-box-item type="button" .borderTop=${index !== 0}>
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
@@ -162,9 +162,7 @@ class HaServicePicker extends LitElement {
|
||||
const description =
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${service}.description`
|
||||
) ||
|
||||
services[domain][service].description ||
|
||||
"";
|
||||
) || services[domain][service].description;
|
||||
|
||||
items.push({
|
||||
id: serviceId,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
|
||||
export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
|
||||
@@ -90,9 +90,6 @@ export class HaWaDialog extends LitElement {
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
protected updated(
|
||||
changedProperties: Map<string | number | symbol, unknown>
|
||||
): void {
|
||||
@@ -113,10 +110,7 @@ export class HaWaDialog extends LitElement {
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
>
|
||||
<slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<ha-dialog-header .subtitlePosition=${this.headerSubtitlePosition}>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
@@ -135,7 +129,7 @@ export class HaWaDialog extends LitElement {
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
|
||||
<div class="body ha-scrollbar">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
@@ -162,11 +156,6 @@ export class HaWaDialog extends LitElement {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleBodyScroll(ev: Event) {
|
||||
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../ha-dialog-header";
|
||||
import "../../ha-icon-button";
|
||||
import "../../ha-icon-next";
|
||||
import "../../ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../ha-md-dialog";
|
||||
import "../../ha-md-list";
|
||||
import "../../ha-md-list-item";
|
||||
import "../../ha-svg-icon";
|
||||
import "../../ha-wa-dialog";
|
||||
import "../ha-target-picker-item-row";
|
||||
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
|
||||
|
||||
@@ -19,15 +21,14 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _params?: TargetDetailsDialogParams;
|
||||
|
||||
@state() private _opened = false;
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public showDialog(params: TargetDetailsDialogParams): void {
|
||||
this._params = params;
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._opened = false;
|
||||
this._dialog?.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -42,31 +43,58 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._opened}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.target-picker.target_details"
|
||||
)}
|
||||
header-subtitle=${`${this.hass.localize(
|
||||
`ui.components.target-picker.type.${this._params.type}`
|
||||
)}:
|
||||
${this._params.title}`}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${this._params.type}
|
||||
.itemId=${this._params.itemId}
|
||||
.deviceFilter=${this._params.deviceFilter}
|
||||
.entityFilter=${this._params.entityFilter}
|
||||
.includeDomains=${this._params.includeDomains}
|
||||
.includeDeviceClasses=${this._params.includeDeviceClasses}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
</ha-wa-dialog>
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.target_details"
|
||||
)}</span
|
||||
>
|
||||
<span slot="subtitle"
|
||||
>${this.hass.localize(
|
||||
`ui.components.target-picker.type.${this._params.type}`
|
||||
)}:
|
||||
${this._params.title}</span
|
||||
>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${this._params.type}
|
||||
.itemId=${this._params.itemId}
|
||||
.deviceFilter=${this._params.deviceFilter}
|
||||
.entityFilter=${this._params.entityFilter}
|
||||
.includeDomains=${this._params.includeDomains}
|
||||
.includeDeviceClasses=${this._params.includeDeviceClasses}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-md-dialog {
|
||||
min-width: 400px;
|
||||
max-height: 90%;
|
||||
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6)
|
||||
max(var(--safe-area-inset-bottom, var(--ha-space-0)), var(--ha-space-8));
|
||||
}
|
||||
|
||||
@media all and (max-width: 600px), all and (max-height: 500px) {
|
||||
ha-md-dialog {
|
||||
--md-dialog-container-shape: var(--ha-space-0);
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -162,12 +162,11 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
<div slot="headline">${name}</div>
|
||||
${context && !this.hideContext
|
||||
? html`<span slot="supporting-text">${context}</span>`
|
||||
: nothing}
|
||||
${this._domainName && this.subEntry
|
||||
? html`<span slot="supporting-text" class="domain"
|
||||
>${this._domainName}</span
|
||||
>`
|
||||
: nothing}
|
||||
: this._domainName && this.subEntry
|
||||
? html`<span slot="supporting-text" class="domain"
|
||||
>${this._domainName}</span
|
||||
>`
|
||||
: nothing}
|
||||
${!this.subEntry && entries && showEntities
|
||||
? html`
|
||||
<div slot="end" class="summary">
|
||||
@@ -232,11 +231,9 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
const rows1 =
|
||||
(nextType === "area"
|
||||
? entries?.referenced_areas
|
||||
: nextType === "device" && this.type !== "label"
|
||||
: nextType === "device"
|
||||
? entries?.referenced_devices
|
||||
: this.type !== "label"
|
||||
? entries?.referenced_entities
|
||||
: []) || [];
|
||||
: entries?.referenced_entities) || [];
|
||||
|
||||
const devicesInAreas = [] as string[];
|
||||
|
||||
@@ -287,13 +284,9 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
const entityRows =
|
||||
this.type === "label" && entries
|
||||
? entries.referenced_entities.filter((entity_id) => {
|
||||
const entity = this.hass.entities[entity_id];
|
||||
return (
|
||||
entity.labels.includes(this.itemId) &&
|
||||
!entries.referenced_devices.includes(entity.device_id || "")
|
||||
);
|
||||
})
|
||||
? entries.referenced_entities.filter((entity_id) =>
|
||||
this.hass.entities[entity_id].labels.includes(this.itemId)
|
||||
)
|
||||
: nextType === "device" && entries
|
||||
? entries.referenced_entities.filter(
|
||||
(entity_id) =>
|
||||
@@ -419,6 +412,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
const device = this.hass.devices[device_id];
|
||||
if (
|
||||
!hiddenAreaIds.includes(device.area_id || "") &&
|
||||
(this.type !== "label" || device.labels.includes(this.itemId)) &&
|
||||
deviceMeetsFilter(
|
||||
device,
|
||||
this.hass.entities,
|
||||
@@ -675,14 +669,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
button.link:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.domain {
|
||||
width: fit-content;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1);
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export const floorCompare =
|
||||
const floorA = entries?.[a];
|
||||
const floorB = entries?.[b];
|
||||
if (floorA && floorB && floorA.level !== floorB.level) {
|
||||
return (floorB.level ?? -9999) - (floorA.level ?? -9999);
|
||||
return (floorA.level ?? 9999) - (floorB.level ?? 9999);
|
||||
}
|
||||
const nameA = floorA?.name ?? a;
|
||||
const nameB = floorB?.name ?? b;
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-aliases-editor";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-floor-picker";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-labels-picker";
|
||||
import "../../../components/ha-picture-upload";
|
||||
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-floor-picker";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-labels-picker";
|
||||
import type {
|
||||
AreaRegistryEntry,
|
||||
AreaRegistryEntryMutableParams,
|
||||
} from "../../../data/area_registry";
|
||||
import { deleteAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
|
||||
import {
|
||||
SENSOR_DEVICE_CLASS_HUMIDITY,
|
||||
SENSOR_DEVICE_CLASS_TEMPERATURE,
|
||||
} from "../../../data/sensor";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import type { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
|
||||
|
||||
const cropOptions: CropOptions = {
|
||||
round: false,
|
||||
@@ -139,6 +139,7 @@ class DialogAreaDetail extends LitElement {
|
||||
></ha-floor-picker>
|
||||
|
||||
<ha-labels-picker
|
||||
.label=${this.hass.localize("ui.components.label-picker.labels")}
|
||||
.hass=${this.hass}
|
||||
.value=${this._labels}
|
||||
@value-changed=${this._labelsChanged}
|
||||
|
||||
@@ -407,9 +407,7 @@ class DialogAddAutomationElement
|
||||
description:
|
||||
this.hass.localize(
|
||||
`component.${dmn}.services.${service}.description`
|
||||
) ||
|
||||
services[dmn][service]?.description ||
|
||||
"",
|
||||
) || services[dmn][service]?.description,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,8 +36,7 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Number, attribute: "sidebar-key" })
|
||||
public sidebarKey?: number;
|
||||
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
@state() private _sidebarConfig?: SidebarConfig;
|
||||
|
||||
@state() private _sidebarKey = 0;
|
||||
@state() private _sidebarKey?: string;
|
||||
|
||||
@storage({
|
||||
key: "automation-sidebar-width",
|
||||
@@ -350,9 +350,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
// deselect previous selected row
|
||||
this._sidebarConfig?.close?.();
|
||||
this._sidebarConfig = ev.detail;
|
||||
|
||||
// be sure the sidebar editor is recreated
|
||||
this._sidebarKey++;
|
||||
this._sidebarKey = JSON.stringify(this._sidebarConfig);
|
||||
|
||||
await this._sidebarElement?.updateComplete;
|
||||
this._sidebarElement?.focus();
|
||||
@@ -377,7 +375,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._sidebarConfig?.close();
|
||||
this._sidebarKey = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,7 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Number, attribute: "sidebar-key" })
|
||||
public sidebarKey?: number;
|
||||
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
|
||||
@@ -44,8 +44,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Number, attribute: "sidebar-key" })
|
||||
public sidebarKey?: number;
|
||||
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Number, attribute: "sidebar-key" })
|
||||
public sidebarKey?: number;
|
||||
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ export default class HaAutomationSidebarScriptField extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Number, attribute: "sidebar-key" })
|
||||
public sidebarKey?: number;
|
||||
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
|
||||
@@ -37,8 +37,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Number, attribute: "sidebar-key" })
|
||||
public sidebarKey?: number;
|
||||
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
@state() private _sidebarConfig?: SidebarConfig;
|
||||
|
||||
@state() private _sidebarKey = 0;
|
||||
@state() private _sidebarKey?: string;
|
||||
|
||||
@storage({
|
||||
key: "automation-sidebar-width",
|
||||
@@ -512,9 +512,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
// deselect previous selected row
|
||||
this._sidebarConfig?.close?.();
|
||||
this._sidebarConfig = ev.detail;
|
||||
|
||||
// be sure the sidebar editor is recreated
|
||||
this._sidebarKey++;
|
||||
this._sidebarKey = JSON.stringify(this._sidebarConfig);
|
||||
|
||||
await this._sidebarElement?.updateComplete;
|
||||
this._sidebarElement?.focus();
|
||||
@@ -539,7 +537,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._sidebarConfig?.close();
|
||||
this._sidebarKey = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -174,10 +174,10 @@ export class HuiEntityEditor extends LitElement {
|
||||
</div>
|
||||
</ha-sortable>`}
|
||||
<ha-entity-picker
|
||||
class="add-entity"
|
||||
.hass=${this.hass}
|
||||
.entityFilter=${this.entityFilter}
|
||||
@value-changed=${this._addEntity}
|
||||
add-button
|
||||
></ha-entity-picker>
|
||||
`;
|
||||
}
|
||||
@@ -226,13 +226,6 @@ export class HuiEntityEditor extends LitElement {
|
||||
ha-entity-picker {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.add-entity {
|
||||
display: block;
|
||||
margin-left: 31px;
|
||||
margin-inline-start: 31px;
|
||||
margin-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.entity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDragHorizontalVariant,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import { mdiDelete, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import type { HaEntityPicker } from "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
@@ -36,14 +28,6 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public badges?: LovelaceHeadingBadgeConfig[];
|
||||
|
||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||
|
||||
@query("ha-entity-picker") private _entityPicker?: HaEntityPicker;
|
||||
|
||||
@state() private _addMode = false;
|
||||
|
||||
private _opened = false;
|
||||
|
||||
private _badgesKeys = new WeakMap<LovelaceHeadingBadgeConfig, string>();
|
||||
|
||||
private _getKey(badge: LovelaceHeadingBadgeConfig) {
|
||||
@@ -125,32 +109,6 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
<div class="add-container">
|
||||
<ha-button
|
||||
data-add-entity
|
||||
appearance="filled"
|
||||
@click=${this._addEntity}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass!.localize(`ui.panel.lovelace.editor.entities.add`)}
|
||||
</ha-button>
|
||||
${this._renderPicker()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
if (!this._addMode) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<mwc-menu-surface
|
||||
open
|
||||
.anchor=${this._addContainer}
|
||||
@closed=${this._onClosed}
|
||||
@opened=${this._onOpened}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@input=${stopPropagation}
|
||||
>
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
@@ -161,39 +119,15 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
"ui.components.entity.entity-picker.choose_entity"
|
||||
)}
|
||||
@value-changed=${this._entityPicked}
|
||||
.value=${undefined}
|
||||
@click=${preventDefault}
|
||||
allow-custom-entity
|
||||
add-button
|
||||
></ha-entity-picker>
|
||||
</mwc-menu-surface>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onClosed(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.target.open = true;
|
||||
}
|
||||
|
||||
private async _onOpened() {
|
||||
if (!this._addMode) {
|
||||
return;
|
||||
}
|
||||
await this._entityPicker?.focus();
|
||||
await this._entityPicker?.open();
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||
if (this._opened && !ev.detail.value) {
|
||||
this._opened = false;
|
||||
this._addMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _addEntity(ev): Promise<void> {
|
||||
ev.stopPropagation();
|
||||
this._addMode = true;
|
||||
}
|
||||
|
||||
private _entityPicked(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.value) {
|
||||
|
||||
@@ -115,6 +115,7 @@ export class HuiEntitiesCardRowEditor extends LitElement {
|
||||
class="add-entity"
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._addEntity}
|
||||
add-button
|
||||
></ha-entity-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -648,6 +648,7 @@
|
||||
"entity-picker": {
|
||||
"choose_entity": "Choose entity",
|
||||
"entity": "Entity",
|
||||
"add": "Add entity",
|
||||
"edit": "Edit",
|
||||
"clear": "Clear",
|
||||
"no_entities": "You don't have any entities",
|
||||
@@ -809,6 +810,7 @@
|
||||
"labels": "Labels",
|
||||
"add_new_sugestion": "Add new label ''{name}''",
|
||||
"add_new": "Add new label…",
|
||||
"add": "Add label",
|
||||
"no_labels": "You don't have any labels",
|
||||
"no_match": "No matching labels found",
|
||||
"failed_create_label": "Failed to create label."
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("floorCompare", () => {
|
||||
});
|
||||
|
||||
describe("floorCompare(entries)", () => {
|
||||
it("sorts by level descending (highest to lowest), then by name", () => {
|
||||
it("sorts by level, then by name", () => {
|
||||
const entries = {
|
||||
floor1: { name: "Ground Floor", level: 0 } as FloorRegistryEntry,
|
||||
floor2: { name: "First Floor", level: 1 } as FloorRegistryEntry,
|
||||
@@ -35,13 +35,13 @@ describe("floorCompare", () => {
|
||||
const floors = ["floor1", "floor2", "floor3"];
|
||||
|
||||
expect(floors.sort(floorCompare(entries))).toEqual([
|
||||
"floor2",
|
||||
"floor1",
|
||||
"floor3",
|
||||
"floor1",
|
||||
"floor2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats null level as -9999, placing it at the end", () => {
|
||||
it("treats null level as 9999, placing it at the end", () => {
|
||||
const entries = {
|
||||
floor1: { name: "Ground Floor", level: 0 } as FloorRegistryEntry,
|
||||
floor2: { name: "First Floor", level: 1 } as FloorRegistryEntry,
|
||||
@@ -50,8 +50,8 @@ describe("floorCompare", () => {
|
||||
const floors = ["floor2", "floor3", "floor1"];
|
||||
|
||||
expect(floors.sort(floorCompare(entries))).toEqual([
|
||||
"floor2",
|
||||
"floor1",
|
||||
"floor2",
|
||||
"floor3",
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user