Compare commits

..

2 Commits

48 changed files with 534 additions and 509 deletions

View File

@@ -92,7 +92,7 @@
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.1.0",
"barcode-detector": "3.0.8",
"color-name": "2.1.0",
"comlink": "4.4.2",
"core-js": "3.48.0",

View File

@@ -2,7 +2,7 @@
import { genClientId } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
@@ -23,6 +23,7 @@ import type {
DataEntryFlowStepForm,
} from "../data/data_entry_flow";
import "./ha-auth-form";
import type { HaAuthForm } from "./ha-auth-form";
type State = "loading" | "error" | "step";
@@ -52,6 +53,8 @@ export class HaAuthFlow extends LitElement {
@state() private _submitting = false;
@query("ha-auth-form") private _form?: HaAuthForm;
createRenderRoot() {
return this;
}
@@ -179,7 +182,7 @@ export class HaAuthFlow extends LitElement {
<div class="action">
<ha-button
@click=${this._handleSubmit}
.disabled=${this._submitting}
.loading=${this._submitting}
>
${this.step.type === "form"
? this.localize("ui.panel.page-authorize.form.next")
@@ -370,6 +373,11 @@ export class HaAuthFlow extends LitElement {
this._providerChanged(this.authProvider);
return;
}
if (!this._form?.reportValidity()) {
return;
}
this._submitting = true;
const postData = { ...this._stepData, client_id: this.clientId };

View File

@@ -12,6 +12,10 @@ export class HaAuthFormString extends HaFormString {
return this;
}
public reportValidity(): boolean {
return this.querySelector("ha-auth-textfield")?.reportValidity() ?? true;
}
protected render(): TemplateResult {
return html`
<style>

View File

@@ -133,34 +133,33 @@ const computeStateToPartsFromEntityAttributes = (
),
});
} catch (_err) {
// fallback to default numeric formatting below
// fallback to default
}
if (parts.length) {
const TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
group: "value",
decimal: "value",
fraction: "value",
literal: "literal",
currency: "unit",
};
const TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
group: "value",
decimal: "value",
fraction: "value",
literal: "literal",
currency: "unit",
};
const valueParts: ValuePart[] = [];
const valueParts: ValuePart[] = [];
for (const part of parts) {
const type = TYPE_MAP[part.type];
if (!type) continue;
const last = valueParts[valueParts.length - 1];
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
if (type === "value" && last?.type === "value") {
last.value += part.value;
} else {
valueParts.push({ type, value: part.value });
}
for (const part of parts) {
const type = TYPE_MAP[part.type];
if (!type) continue;
const last = valueParts[valueParts.length - 1];
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
if (type === "value" && last?.type === "value") {
last.value += part.value;
} else {
valueParts.push({ type, value: part.value });
}
return valueParts;
}
return valueParts;
}
// default processing of numeric values

View File

@@ -1,7 +1,7 @@
import { mdiClose } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, queryAll } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon-button";
@@ -133,6 +133,17 @@ export class HaBaseTimeInput extends LitElement {
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
@queryAll("ha-textfield") private _inputs?: HaTextField[];
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._inputs?.every((input) => input.reportValidity()) ?? true;
}
protected render(): TemplateResult {
return html`
${this.label

View File

@@ -245,7 +245,7 @@ export class HaButton extends Button {
}
.label {
overflow: var(--ha-button-label-overflow, hidden);
overflow: hidden;
text-overflow: ellipsis;
padding: var(--ha-space-1) 0;
}

View File

@@ -1,9 +1,11 @@
import { mdiMenuDown } from "@mdi/js";
import type { TemplateResult } from "lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../types";
import "./ha-attribute-icon";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-icon";
@@ -14,10 +16,17 @@ export interface SelectOption {
value: string;
iconPath?: string;
icon?: string;
attributeIcon?: {
stateObj: HassEntity;
attribute: string;
attributeValue?: string;
};
}
@customElement("ha-control-select-menu")
export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "show-arrow" })
public showArrow = false;
@@ -38,9 +47,6 @@ export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public options: SelectOption[] = [];
@property({ attribute: false })
public renderIcon?: (value: string) => TemplateResult<1> | typeof nothing;
@query("button") private _triggerButton!: HTMLButtonElement;
public override render() {
@@ -88,8 +94,14 @@ export class HaControlSelectMenu extends LitElement {
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
: option.icon
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
: this.renderIcon
? html`<span slot="icon">${this.renderIcon(option.value)}</span>`
: option.attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${option.attributeIcon.stateObj}
.attribute=${option.attributeIcon.attribute}
.attributeValue=${option.attributeIcon.attributeValue}
></ha-attribute-icon>`
: nothing}
${option.label}</ha-dropdown-item
>`;
@@ -107,20 +119,24 @@ export class HaControlSelectMenu extends LitElement {
}
private _renderIcon() {
const value = this.getValueObject(this.options, this.value);
const { iconPath, icon, attributeIcon } =
this.getValueObject(this.options, this.value) ?? {};
const defaultIcon = this.querySelector("[slot='icon']");
return html`
<div class="icon">
${value?.iconPath
? html`<ha-svg-icon
slot="icon"
.path=${value.iconPath}
></ha-svg-icon>`
: value?.icon
? html`<ha-icon slot="icon" .icon=${value.icon}></ha-icon>`
: this.renderIcon && this.value
? this.renderIcon(this.value)
${iconPath
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
: icon
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
: attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${attributeIcon.stateObj}
.attribute=${attributeIcon.attribute}
.attributeValue=${attributeIcon.attributeValue}
></ha-attribute-icon>`
: defaultIcon
? html`<slot name="icon"></slot>`
: nothing}
@@ -156,12 +172,12 @@ export class HaControlSelectMenu extends LitElement {
font-size: var(--ha-font-size-m);
line-height: 1.4;
width: auto;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
border: none;
text-align: left;
color: var(--primary-text-color);
height: var(--control-select-menu-height);
padding: var(--control-select-menu-padding);
overflow: hidden;

View File

@@ -1,7 +1,7 @@
import { mdiCalendar } from "@mdi/js";
import type { HassConfig } from "home-assistant-js-websocket";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { formatDateNumeric } from "../common/datetime/format_date";
import { fireEvent } from "../common/dom/fire_event";
@@ -9,6 +9,7 @@ import { TimeZone } from "../data/translation";
import type { HomeAssistant } from "../types";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
@@ -52,6 +53,12 @@ export class HaDateInput extends LitElement {
@property({ attribute: "can-clear", type: Boolean }) public canClear = false;
@query("ha-textfield", true) private _input?: HaTextField;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
render() {
return html`<ha-textfield
.label=${this.label}

View File

@@ -1,12 +1,12 @@
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import "./ha-button-toggle-group";
import type { ValueChangedEvent } from "../types";
import "./ha-base-time-input";
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
import "./ha-button-toggle-group";
export interface HaDurationData {
days?: number;
@@ -19,7 +19,7 @@ export interface HaDurationData {
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
@customElement("ha-duration-input")
class HaDurationInput extends LitElement {
export class HaDurationInput extends LitElement {
@property({ attribute: false }) public data?: HaDurationData;
@property() public label?: string;
@@ -42,8 +42,19 @@ class HaDurationInput extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-base-time-input", true) private _input?: HaBaseTimeInput;
private _toggleNegative = false;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {
return html`
<div class="row">

View File

@@ -1,8 +1,9 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import "./ha-form";
import "../ha-expansion-panel";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormDataContainer,
HaFormElement,
@@ -35,6 +36,12 @@ export class HaFormExpandable extends LitElement implements HaFormElement {
key: string
) => string;
@query("ha-form", true) private _form?: HaForm;
public async reportValidity(): Promise<boolean> {
return this._form?.reportValidity() ?? true;
}
private _renderDescription() {
const description = this.computeHelper?.(this.schema);
return description ? html`<p>${description}</p>` : nothing;

View File

@@ -1,15 +1,15 @@
import type { TemplateResult, PropertyValues } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HaTextField } from "../ha-textfield";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
import type {
HaFormElement,
HaFormFloatData,
HaFormFloatSchema,
} from "./types";
import type { LocalizeFunc } from "../../common/translations/localize";
@customElement("ha-form-float")
export class HaFormFloat extends LitElement implements HaFormElement {
@@ -25,12 +25,15 @@ export class HaFormFloat extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-textfield") private _input?: HaTextField;
@query("ha-textfield", true) private _input?: HaTextField;
public focus() {
if (this._input) {
this._input.focus();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {

View File

@@ -1,14 +1,15 @@
import "./ha-form";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, queryAll } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormGridSchema,
HaFormDataContainer,
HaFormElement,
HaFormGridSchema,
HaFormSchema,
} from "./types";
import type { HomeAssistant } from "../../types";
@customElement("ha-form-grid")
export class HaFormGrid extends LitElement implements HaFormElement {
@@ -33,9 +34,22 @@ export class HaFormGrid extends LitElement implements HaFormElement {
key: string
) => string;
public async focus() {
await this.updateComplete;
this.renderRoot.querySelector("ha-form")?.focus();
@queryAll("ha-form", true) private _forms?: HaForm[];
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public async reportValidity(): Promise<boolean> {
const forms = this._forms ?? [];
let valid = true;
for (const form of forms) {
if (!form.reportValidity()) {
valid = false;
}
}
return valid;
}
protected updated(changedProps: PropertyValues): void {

View File

@@ -2,10 +2,11 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-slider";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-input-helper-text";
import "../ha-slider";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
import type {
@@ -13,7 +14,6 @@ import type {
HaFormIntegerData,
HaFormIntegerSchema,
} from "./types";
import type { LocalizeFunc } from "../../common/translations/localize";
@customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement {
@@ -29,24 +29,39 @@ export class HaFormInteger extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-textfield ha-slider") private _input?:
@query("ha-textfield, ha-slider", true) private _input?:
| HaTextField
| HTMLInputElement;
private _lastValue?: HaFormIntegerData;
public focus() {
if (this._input) {
this._input.focus();
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
const showSlider = this._showSlider();
if (showSlider && this.schema.required && isNaN(Number(this.data))) {
return false;
}
if (!showSlider) {
return this._input?.reportValidity() ?? true;
}
return true;
}
protected render(): TemplateResult {
if (
private _showSlider(): boolean {
return (
this.schema.valueMin !== undefined &&
this.schema.valueMax !== undefined &&
this.schema.valueMax - this.schema.valueMin < 256
) {
);
}
protected render(): TemplateResult {
if (this._showSlider()) {
return html`
<div>
${this.label}

View File

@@ -44,6 +44,13 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
this._dropdown?.focus();
}
public reportValidity(): boolean {
if (!this.schema.required || (this.data && this.data.length > 0)) {
return true;
}
return false;
}
protected render(): TemplateResult {
const options = Array.isArray(this.schema.options)
? this.schema.options

View File

@@ -11,6 +11,7 @@ import "../ha-dropdown";
import "../ha-dropdown-item";
import "../ha-svg-icon";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormDataContainer,
HaFormElement,
@@ -53,6 +54,11 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
this.renderRoot.querySelector("ha-form")?.focus();
}
public async reportValidity(): Promise<boolean> {
const form = this.renderRoot.querySelector<HaForm>("ha-form");
return form ? form.reportValidity() : true;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("data")) {

View File

@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../ha-duration-input";
import type { HaDurationInput } from "../ha-duration-input";
import type { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./types";
@customElement("ha-form-positive_time_period_dict")
@@ -14,12 +15,15 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-time-input", true) private _input?: HTMLElement;
@query("ha-duration-input", true) private _input?: HaDurationInput;
public focus() {
if (this._input) {
this._input.focus();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {

View File

@@ -1,16 +1,16 @@
import memoizeOne from "memoize-one";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-selector/ha-selector-select";
import type {
HaFormElement,
HaFormSelectData,
HaFormSelectSchema,
} from "./types";
import type { SelectSelector } from "../../data/selector";
import "../ha-selector/ha-selector-select";
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@@ -41,6 +41,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
})
);
public reportValidity(): boolean {
if (!this.schema.required || this.data) {
return true;
}
return false;
}
protected render(): TemplateResult {
return html`
<ha-selector-select

View File

@@ -37,12 +37,15 @@ export class HaFormString extends LitElement implements HaFormElement {
@state() protected unmaskedPassword = false;
@query("ha-textfield") private _input?: HaTextField;
@query("ha-textfield", true) private _input?: HaTextField;
public focus(): void {
if (this._input) {
this._input.focus();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {

View File

@@ -1,5 +1,5 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, ReactiveElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
@@ -76,22 +76,64 @@ export class HaForm extends LitElement implements HaFormElement {
return {};
}
public async focus() {
await this.updateComplete;
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
public reportValidity(): boolean {
const root = this.renderRoot.querySelector(".root");
if (!root) {
return;
return true;
}
for (const child of root.children) {
if (child.tagName !== "HA-ALERT") {
if (child instanceof ReactiveElement) {
// eslint-disable-next-line no-await-in-loop
await child.updateComplete;
}
(child as HTMLElement).focus();
break;
const elements = [...root.children].filter(
(child) => child.localName !== "ha-alert"
) as (HTMLElement & { reportValidity?: () => boolean })[];
let isValid = true;
let firstInvalidElement: HTMLElement | undefined;
this.schema.forEach((item, index) => {
const element = elements[index];
if (!element) {
return;
}
let elementValid = true;
if (
"reportValidity" in element &&
typeof element.reportValidity === "function"
) {
elementValid = element.reportValidity();
} else if (
item.required &&
!(
"type" in item && ["boolean", "constant"].includes(item.type ?? "")
) &&
!(
"selector" in item &&
("boolean" in item.selector || "constant" in item.selector)
)
) {
const value = getValue(this.data, item);
elementValid = value !== undefined && value !== null && value !== "";
}
if (!elementValid) {
isValid = false;
if (!firstInvalidElement) {
firstInvalidElement = element;
}
}
});
if (firstInvalidElement) {
firstInvalidElement.focus?.();
}
return isValid;
}
protected willUpdate(changedProps: PropertyValues) {
@@ -105,11 +147,6 @@ export class HaForm extends LitElement implements HaFormElement {
}
}
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
protected render(): TemplateResult {
return html`
<div class="root" part="root">

View File

@@ -74,7 +74,6 @@ export class HaIconButton extends LitElement {
);
--wa-color-on-normal: currentColor;
--wa-color-fill-quiet: transparent;
--ha-button-label-overflow: visible;
}
ha-button::after {
content: "";

View File

@@ -1,5 +1,5 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import type { DateSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
@@ -20,6 +20,12 @@ export class HaDateSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@query("ha-date-input", true) private _input?: HTMLInputElement;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render() {
return html`
<ha-date-input

View File

@@ -29,6 +29,10 @@ export class HaDateTimeSelector extends LitElement {
@query("ha-time-input") private _timeInput!: HaTimeInput;
public reportValidity(): boolean {
return this._dateInput.reportValidity() && this._timeInput.reportValidity();
}
protected render() {
const values =
typeof this.value === "string" ? this.value.split(" ") : undefined;

View File

@@ -1,10 +1,10 @@
import memoizeOne from "memoize-one";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { DurationSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import type { HaDurationData } from "../ha-duration-input";
import "../ha-duration-input";
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
@customElement("ha-selector-duration")
export class HaTimeDuration extends LitElement {
@@ -25,6 +25,12 @@ export class HaTimeDuration extends LitElement {
@property({ type: Boolean }) public required = true;
@query("ha-duration-input", true) private _input?: HaDurationInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
private _data = memoizeOne(
(value?: HaDurationData | string | number): HaDurationData | undefined => {
if (typeof value === "number") {

View File

@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import type { NumberSelector } from "../../data/selector";
@@ -8,6 +8,7 @@ import type { HomeAssistant } from "../../types";
import "../ha-input-helper-text";
import "../ha-slider";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
@customElement("ha-selector-number")
export class HaNumberSelector extends LitElement {
@@ -30,8 +31,14 @@ export class HaNumberSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-textfield", true) private _input?: HaTextField | HTMLInputElement;
private _valueStr = "";
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected willUpdate(changedProps: PropertyValues) {
if (changedProps.has("value")) {
if (this._valueStr === "" || this.value !== Number(this._valueStr)) {

View File

@@ -1,6 +1,6 @@
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { StringSelector } from "../../data/selector";
@@ -32,11 +32,18 @@ export class HaTextSelector extends LitElement {
@state() private _unmaskedPassword = false;
@query("ha-textfield, ha-textarea") private _input?: HTMLInputElement;
public async focus() {
await this.updateComplete;
(
this.renderRoot.querySelector("ha-textarea, ha-textfield") as HTMLElement
)?.focus();
this._input?.focus();
}
public reportValidity(): boolean {
if (this.selector.text?.multiple) {
return true;
}
return this._input?.reportValidity() ?? true;
}
protected render() {

View File

@@ -1,8 +1,9 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import type { TimeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-time-input";
import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-time")
export class HaTimeSelector extends LitElement {
@@ -20,6 +21,12 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public required = false;
@query("ha-time-input") private _input?: HaTimeInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render() {
return html`
<ha-time-input

View File

@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import type { Selector } from "../../data/selector";
@@ -94,9 +94,27 @@ export class HaSelector extends LitElement {
@property({ attribute: false }) public context?: Record<string, any>;
@query("#selector", true) private _selectorElement?: HTMLElement;
public reportValidity(): boolean {
if (
this._selectorElement &&
"reportValidity" in this._selectorElement &&
typeof this._selectorElement.reportValidity === "function"
) {
return this._selectorElement?.reportValidity() ?? true;
}
if (this.required) {
return (
this.value !== undefined && this.value !== null && this.value !== ""
);
}
return true;
}
public async focus() {
await this.updateComplete;
(this.renderRoot.querySelector("#selector") as HTMLElement)?.focus();
this._selectorElement?.focus();
}
private get _type() {

View File

@@ -1,11 +1,11 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event";
import type { FrontendLocaleData } from "../data/translation";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import type { ValueChangedEvent } from "../types";
import "./ha-base-time-input";
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
@customElement("ha-time-input")
export class HaTimeInput extends LitElement {
@@ -26,6 +26,12 @@ export class HaTimeInput extends LitElement {
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
@query("ha-base-time-input") private _input?: HaBaseTimeInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render() {
const useAMPM = useAmPm(this.locale);

View File

@@ -9,7 +9,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle";
@@ -40,38 +39,6 @@ class MoreInfoClimate extends LitElement {
@state() private _mainControl: MainControl = "temperature";
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderFanModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="fan_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderSwingModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderSwingHorizontalModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
protected willUpdate(changedProps: PropertyValues): void {
if (
changedProps.has("stateObj") &&
@@ -238,8 +205,12 @@ class MoreInfoClimate extends LitElement {
"preset_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "preset_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderPresetModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -263,8 +234,12 @@ class MoreInfoClimate extends LitElement {
"fan_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "fan_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderFanModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
</ha-control-select-menu>
@@ -288,8 +263,12 @@ class MoreInfoClimate extends LitElement {
"swing_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderSwingModeIcon}
>
<ha-svg-icon
slot="icon"
@@ -318,9 +297,13 @@ class MoreInfoClimate extends LitElement {
"swing_horizontal_mode",
mode
),
attributeIcon: {
stateObj,
attribute: "swing_horizontal_mode",
attributeValue: mode,
},
})
)}
.renderIcon=${this._renderSwingHorizontalModeIcon}
>
<ha-svg-icon
slot="icon"

View File

@@ -40,22 +40,6 @@ class MoreInfoFan extends LitElement {
@state() public _presetMode?: string;
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private _renderDirectionIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${value}
></ha-attribute-icon>`;
private _toggle = () => {
const service = this.stateObj?.state === "on" ? "turn_off" : "turn_on";
forwardHaptic(this, "light");
@@ -208,9 +192,15 @@ class MoreInfoFan extends LitElement {
"preset_mode",
mode
),
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "preset_mode",
attributeValue: mode,
}
: undefined,
})
)}
.renderIcon=${this._renderPresetModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>
@@ -236,8 +226,14 @@ class MoreInfoFan extends LitElement {
direction
)
: direction,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "direction",
attributeValue: direction,
}
: undefined,
}))}
.renderIcon=${this._renderDirectionIcon}
>
<ha-attribute-icon
slot="icon"

View File

@@ -23,14 +23,6 @@ class MoreInfoHumidifier extends LitElement {
@state() public _mode?: string;
private _renderModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="mode"
.attributeValue=${value}
></ha-attribute-icon>`;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("stateObj")) {
@@ -114,8 +106,14 @@ class MoreInfoHumidifier extends LitElement {
mode
)
: mode,
attributeIcon: stateObj
? {
stateObj,
attribute: "mode",
attributeValue: mode,
}
: undefined,
})) || []}
.renderIcon=${this._renderModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -55,14 +55,6 @@ class MoreInfoLight extends LitElement {
@state() private _mainControl: MainControl = "brightness";
private _renderEffectIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="effect"
.attributeValue=${value}
></ha-attribute-icon>`;
protected updated(changedProps: PropertyValues<typeof this>): void {
if (changedProps.has("stateObj")) {
this._effect = this.stateObj?.attributes.effect;
@@ -279,9 +271,15 @@ class MoreInfoLight extends LitElement {
effect
)
: effect,
attributeIcon: this.stateObj
? {
stateObj: this.stateObj,
attribute: "effect",
attributeValue: effect,
}
: undefined,
})
)}
.renderIcon=${this._renderEffectIcon}
>
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -24,14 +24,6 @@ class MoreInfoWaterHeater extends LitElement {
@property({ attribute: false }) public stateObj?: WaterHeaterEntity;
private _renderOperationModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="operation_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -93,8 +85,12 @@ class MoreInfoWaterHeater extends LitElement {
.map((mode) => ({
value: mode,
label: this.hass.formatEntityState(stateObj, mode),
attributeIcon: {
stateObj,
attribute: "operation_mode",
attributeValue: mode,
},
}))}
.renderIcon=${this._renderOperationModeIcon}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -2,27 +2,17 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
import checkValidDate from "../../common/datetime/check_valid_date";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import "../../components/ha-attribute-value";
import "../../components/ha-card";
import type { LocalizeKeys } from "../../common/translations/localize";
import { computeShownAttributes } from "../../data/entity/entity_attributes";
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import "../../components/ha-yaml-editor";
interface DetailsViewParams {
entityId: string;
}
interface DetailEntry {
translationKey: LocalizeKeys;
value: string;
}
@customElement("ha-more-info-details")
class HaMoreInfoDetails extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -31,8 +21,6 @@ class HaMoreInfoDetails extends LitElement {
@property({ attribute: false }) public params?: DetailsViewParams;
@property({ attribute: false }) public yamlMode = false;
@state() private _stateObj?: HassEntity;
protected willUpdate(changedProps: PropertyValues): void {
@@ -49,127 +37,60 @@ class HaMoreInfoDetails extends LitElement {
return nothing;
}
const { stateEntries, attributes, yamlData } = this._getDetailData(
this._stateObj
const translatedState = this.hass.formatEntityState(this._stateObj);
const detailsAttributes = computeShownAttributes(this._stateObj);
const detailsAttributeSet = new Set(detailsAttributes);
const builtInAttributes = Object.keys(this._stateObj.attributes).filter(
(attribute) => !detailsAttributeSet.has(attribute)
);
const allAttributes = [...detailsAttributes, ...builtInAttributes];
return html`
<div class="content">
${this.yamlMode
? html`<ha-yaml-editor
.hass=${this.hass}
.value=${yamlData}
read-only
auto-update
></ha-yaml-editor>`
: html`
<section class="section">
<h2 class="section-title">
${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)}
</h2>
<ha-card>
<div class="card-content">
<div class="data-group">
${stateEntries.map(
(entry) =>
html`<div class="data-entry">
<div class="key">
${this.hass.localize(entry.translationKey)}
</div>
<div class="value">${entry.value}</div>
</div>`
)}
</div>
<section class="section">
<h2 class="section-title">
${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)}
</h2>
<ha-card>
<div class="card-content">
<div class="attribute-group">
<div class="data-entry">
<div class="key">
${this.hass.localize(
"ui.dialogs.more_info_control.translated"
)}
</div>
</ha-card>
</section>
<div class="value">${translatedState}</div>
</div>
<div class="data-entry">
<div class="key">
${this.hass.localize("ui.dialogs.more_info_control.raw")}
</div>
<div class="value">${this._stateObj.state}</div>
</div>
</div>
</div>
</ha-card>
</section>
<section class="section">
<h2 class="section-title">
${this.hass.localize(
"ui.dialogs.more_info_control.attributes"
)}
</h2>
<ha-card>
<div class="card-content">
<div class="data-group">
${this._renderAttributes(attributes)}
</div>
</div>
</ha-card>
</section>
`}
<section class="section">
<h2 class="section-title">
${this.hass.localize("ui.dialogs.more_info_control.attributes")}
</h2>
<ha-card>
<div class="card-content">
<div class="attribute-group">
${this._renderAttributes(allAttributes)}
</div>
</div>
</ha-card>
</section>
</div>
`;
}
private _getDetailData = memoizeOne(
(
stateObj: HassEntity
): {
stateEntries: DetailEntry[];
attributes: string[];
yamlData: {
state: {
translated: string;
raw: string;
last_changed: string;
last_updated: string;
};
attributes: Record<string, string>;
};
} => {
const translatedState = this.hass.formatEntityState(stateObj);
const detailsAttributes = computeShownAttributes(stateObj);
const detailsAttributeSet = new Set(detailsAttributes);
const builtInAttributes = Object.keys(stateObj.attributes).filter(
(attribute) => !detailsAttributeSet.has(attribute)
);
return {
stateEntries: [
{
translationKey: "ui.dialogs.more_info_control.translated",
value: translatedState,
},
{
translationKey: "ui.dialogs.more_info_control.raw",
value: stateObj.state,
},
{
translationKey: "ui.dialogs.more_info_control.last_changed",
value: this._formatTimestamp(stateObj.last_changed),
},
{
translationKey: "ui.dialogs.more_info_control.last_updated",
value: this._formatTimestamp(stateObj.last_updated),
},
],
attributes: [...detailsAttributes, ...builtInAttributes],
yamlData: {
state: {
translated: translatedState,
raw: stateObj.state,
last_changed: stateObj.last_changed,
last_updated: stateObj.last_updated,
},
attributes: stateObj.attributes,
},
};
}
);
private _formatTimestamp(value: string): string {
const date = new Date(value);
return checkValidDate(date)
? formatDateTimeWithSeconds(date, this.hass.locale, this.hass.config)
: value;
}
private _renderAttributes(attributes: string[]) {
if (attributes.length === 0) {
return html`<div class="empty">
@@ -238,7 +159,7 @@ class HaMoreInfoDetails extends LitElement {
border-bottom: 1px solid var(--divider-color);
}
.data-group .data-entry:last-of-type {
.attribute-group .data-entry:last-of-type {
border-bottom: none;
}

View File

@@ -1,7 +1,6 @@
import {
mdiChartBoxOutline,
mdiClose,
mdiCodeBraces,
mdiCogOutline,
mdiDevices,
mdiDotsVertical,
@@ -133,8 +132,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
@state() private _infoEditMode = false;
@state() private _detailsYamlMode = false;
@state() private _isEscapeEnabled = true;
@state() private _sensorNumericDeviceClasses?: string[] = [];
@@ -185,7 +182,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._parentEntityIds = [];
this._entry = undefined;
this._infoEditMode = false;
this._detailsYamlMode = false;
this._initialView = DEFAULT_VIEW;
this._currView = DEFAULT_VIEW;
this._childView = undefined;
@@ -255,7 +251,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
private _goBack() {
if (this._childView) {
this._childView = undefined;
this._detailsYamlMode = false;
return;
}
if (this._initialView !== this._currView) {
@@ -319,10 +314,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
this._infoEditMode = !this._infoEditMode;
}
private _toggleDetailsYamlMode() {
this._detailsYamlMode = !this._detailsYamlMode;
}
private _handleToggleInfoEditModeEvent(ev) {
this._infoEditMode = ev.detail;
}
@@ -646,18 +637,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
</ha-dropdown-item>
</ha-dropdown>
`
: this._childView?.viewTag === "ha-more-info-details"
? html`
<ha-icon-button
slot="headerActionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.toggle_yaml_mode"
)}
.path=${mdiCodeBraces}
@click=${this._toggleDetailsYamlMode}
></ha-icon-button>
`
: nothing}
: nothing}
<div
class=${classMap({
"content-wrapper": true,
@@ -683,7 +663,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
hass: this.hass,
entry: this._entry,
params: this._childView.viewParams,
yamlMode: this._detailsYamlMode,
})}
</div>
`
@@ -752,7 +731,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
if (changedProps.has("_currView")) {
this._childView = undefined;
this._infoEditMode = false;
this._detailsYamlMode = false;
}
}

View File

@@ -49,14 +49,6 @@ class HuiClimateFanModesCardFeature
@state() _currentFanMode?: string;
private _renderFanModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="fan_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -183,8 +175,14 @@ class HuiClimateFanModesCardFeature
.value=${this._currentFanMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderFanModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "fan_mode",
attributeValue: option.value,
},
}))}
><ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
</ha-control-select-menu>
`;

View File

@@ -48,14 +48,6 @@ class HuiClimatePresetModesCardFeature
@state() _currentPresetMode?: string;
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -187,8 +179,14 @@ class HuiClimatePresetModesCardFeature
.value=${this._currentPresetMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderPresetModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "preset_mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -48,14 +48,6 @@ class HuiClimateSwingHorizontalModesCardFeature
@state() _currentSwingHorizontalMode?: string;
private _renderSwingHorizontalModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="swing_horizontal_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -195,8 +187,14 @@ class HuiClimateSwingHorizontalModesCardFeature
.value=${this._currentSwingHorizontalMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderSwingHorizontalModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "swing_horizontal_mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiArrowOscillating}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -48,14 +48,6 @@ class HuiClimateSwingModesCardFeature
@state() _currentSwingMode?: string;
private _renderSwingModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="swing_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -187,8 +179,14 @@ class HuiClimateSwingModesCardFeature
.value=${this._currentSwingMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderSwingModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj,
attribute: "swing_mode",
attributeValue: option.value,
},
}))}
><ha-svg-icon slot="icon" .path=${mdiArrowOscillating}></ha-svg-icon>
</ha-control-select-menu>
`;

View File

@@ -47,14 +47,6 @@ class HuiFanPresetModesCardFeature
@state() _currentPresetMode?: string;
private _renderPresetModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="preset_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -181,8 +173,14 @@ class HuiFanPresetModesCardFeature
.value=${this._currentPresetMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderPresetModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: stateObj,
attribute: "preset_mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -48,14 +48,6 @@ class HuiHumidifierModesCardFeature
@state() _currentMode?: string;
private _renderModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -182,8 +174,14 @@ class HuiHumidifierModesCardFeature
.value=${this._currentMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj,
attribute: "mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -49,14 +49,6 @@ class HuiWaterHeaterOperationModeCardFeature
@state() _currentOperationMode?: OperationMode;
private _renderOperationModeIcon = (value: string) =>
html`<ha-attribute-icon
.hass=${this.hass}
.stateObj=${this._stateObj}
attribute="operation_mode"
.attributeValue=${value}
></ha-attribute-icon>`;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
@@ -161,8 +153,14 @@ class HuiWaterHeaterOperationModeCardFeature
.value=${this._currentOperationMode}
.disabled=${this._stateObj.state === UNAVAILABLE}
@wa-select=${this._valueChanged}
.options=${options}
.renderIcon=${this._renderOperationModeIcon}
.options=${options.map((option) => ({
...option,
attributeIcon: {
stateObj: this._stateObj,
attribute: "operation_mode",
attributeValue: option.value,
},
}))}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
</ha-control-select-menu>

View File

@@ -218,9 +218,7 @@ function formatTooltip(
}
// when comparing the first value is offset to match the main period
// and the real date is in the third value
// find the first param with the real date to handle gap-filled entries
const origDate = params.find((p) => p.value?.[2] != null)?.value?.[2];
const date = new Date(origDate ?? params[0].value?.[0]);
const date = new Date(params[0].value?.[2] ?? params[0].value?.[0]);
let period: string;
if (suggestedPeriod === "month") {

View File

@@ -6,7 +6,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-spinner";
import type { HistoryStates } from "../../../data/history";
import { subscribeHistoryStatesTimeWindow } from "../../../data/history";
import type { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities";
@@ -67,8 +66,6 @@ export class HuiGraphHeaderFooter
private _error?: string;
private _history?: HistoryStates;
private _interval?: number;
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
@@ -164,8 +161,24 @@ export class HuiGraphHeaderFooter
// Message came in before we had a chance to unload
return;
}
this._history = combinedHistory;
this._computeCoordinates();
const width = this.clientWidth || this.offsetWidth;
// sample to 1 point per hour or 1 point per 5 pixels
const maxDetails = Math.max(
10,
this._config.detail! > 1
? Math.max(width / 5, this._config.hours_to_show!)
: this._config.hours_to_show!
);
const useMean = this._config.detail !== 2;
const { points } = coordinatesMinimalResponseCompressedState(
combinedHistory[this._config.entity],
width,
width / 5,
maxDetails,
{ minY: this._config.limits?.min, maxY: this._config.limits?.max },
useMean
);
this._coordinates = points;
},
this._config.hours_to_show!,
[this._config.entity]
@@ -177,63 +190,10 @@ export class HuiGraphHeaderFooter
this._setRedrawTimer();
}
private _computeCoordinates() {
if (!this._history || !this._config) {
return;
}
const entityHistory = this._history[this._config.entity];
if (!entityHistory?.length) {
return;
}
const width = this.clientWidth || this.offsetWidth;
// sample to 1 point per hour or 1 point per 5 pixels
const maxDetails = Math.max(
10,
this._config.detail! > 1
? Math.max(width / 5, this._config.hours_to_show!)
: this._config.hours_to_show!
);
const useMean = this._config.detail !== 2;
const { points } = coordinatesMinimalResponseCompressedState(
entityHistory,
width,
width / 5,
maxDetails,
{ minY: this._config.limits?.min, maxY: this._config.limits?.max },
useMean
);
this._coordinates = points;
}
private _redrawGraph() {
if (!this._history || !this._config?.hours_to_show) {
return;
if (this._coordinates) {
this._coordinates = [...this._coordinates];
}
const entityId = this._config.entity;
const entityHistory = this._history[entityId];
if (entityHistory?.length) {
const purgeBeforeTimestamp =
(Date.now() - this._config.hours_to_show * 60 * 60 * 1000) / 1000;
let purgedHistory = entityHistory.filter(
(entry) => entry.lu >= purgeBeforeTimestamp
);
if (purgedHistory.length !== entityHistory.length) {
if (
!purgedHistory.length ||
purgedHistory[0].lu !== purgeBeforeTimestamp
) {
// Preserve the last expired state as the start boundary
const lastExpiredState = {
...entityHistory[entityHistory.length - purgedHistory.length - 1],
};
lastExpiredState.lu = purgeBeforeTimestamp;
delete lastExpiredState.lc;
purgedHistory = [lastExpiredState, ...purgedHistory];
}
this._history = { ...this._history, [entityId]: purgedHistory };
}
}
this._computeCoordinates();
}
private _setRedrawTimer() {
@@ -251,7 +211,6 @@ export class HuiGraphHeaderFooter
this._subscribed.then((unsub) => unsub?.());
this._subscribed = undefined;
}
this._history = undefined;
}
protected updated(changedProps: PropertyValues) {

View File

@@ -1485,8 +1485,6 @@ class HUIRoot extends LitElement {
padding-inline-start: var(--safe-area-inset-left);
}
hui-view-container > * {
display: flex;
flex-direction: column;
flex: 1 1 100%;
max-width: 100%;
}

View File

@@ -461,7 +461,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
--column-min-width: var(--ha-view-sections-column-min-width, 320px);
--top-margin: var(--ha-view-sections-extra-top-margin, 80px);
display: block;
flex: 1;
}
@media (max-width: 600px) {
@@ -471,9 +470,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
.wrapper {
display: flex;
flex-direction: column;
min-height: calc(100% - 2 * var(--row-gap));
display: block;
padding: var(--row-gap) var(--column-gap);
box-sizing: content-box;
margin: 0 auto;
@@ -506,7 +503,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
gap: var(--row-gap) var(--column-gap);
padding: var(--row-gap) 0;
align-items: flex-start;
flex: 1 0 auto;
}
.wrapper.has-sidebar .container {

View File

@@ -1524,7 +1524,6 @@
"settings": "Settings",
"edit": "Edit entity",
"details": "Details",
"toggle_yaml_mode": "Toggle YAML mode",
"translated": "Translated",
"raw": "Raw",
"back_to_info": "Back to info",

View File

@@ -6086,12 +6086,12 @@ __metadata:
languageName: node
linkType: hard
"barcode-detector@npm:3.1.0":
version: 3.1.0
resolution: "barcode-detector@npm:3.1.0"
"barcode-detector@npm:3.0.8":
version: 3.0.8
resolution: "barcode-detector@npm:3.0.8"
dependencies:
zxing-wasm: "npm:3.0.0"
checksum: 10/60767161081b827e290b60bb3416999dee616bab39291ee55565df9b72d59f0bbbf511fd3bb85db18eee7c0ad9acf1ff90359cdb21e10f80793acd0105c86a1d
zxing-wasm: "npm:2.2.4"
checksum: 10/7de6225f659c69a0f4101d080a9e0812f2404c485fa2406424b8b13eaff274f6e7405c94de24827a09def52c32a63b19b9c9fba61c5274b074d558f696ec1684
languageName: node
linkType: hard
@@ -9268,7 +9268,7 @@ __metadata:
app-datepicker: "npm:5.1.1"
babel-loader: "npm:10.0.0"
babel-plugin-template-html-minifier: "npm:4.1.0"
barcode-detector: "npm:3.1.0"
barcode-detector: "npm:3.0.8"
browserslist-useragent-regexp: "npm:4.1.3"
color-name: "npm:2.1.0"
comlink: "npm:4.4.2"
@@ -14139,12 +14139,12 @@ __metadata:
languageName: node
linkType: hard
"type-fest@npm:^5.4.4":
version: 5.4.4
resolution: "type-fest@npm:5.4.4"
"type-fest@npm:^5.2.0":
version: 5.4.1
resolution: "type-fest@npm:5.4.1"
dependencies:
tagged-tag: "npm:^1.0.0"
checksum: 10/0bbdca645f95740587f389a2d712fe8d5e9ab7d13e74aac97cf396112510abcaab6b75fd90d65172bc13b02fdfc827e6a871322cc9c1c1a5a2754d9ab264c6f5
checksum: 10/be7d4749e1e5cf2e2c9904fa1aaf9da5eef6c47c130881bf93bfd5a670b2ab59c5502466768e42c521281056a2375b1617176a75cf6c52b575f4bbabbd450b21
languageName: node
linkType: hard
@@ -15703,14 +15703,14 @@ __metadata:
languageName: node
linkType: hard
"zxing-wasm@npm:3.0.0":
version: 3.0.0
resolution: "zxing-wasm@npm:3.0.0"
"zxing-wasm@npm:2.2.4":
version: 2.2.4
resolution: "zxing-wasm@npm:2.2.4"
dependencies:
"@types/emscripten": "npm:^1.41.5"
type-fest: "npm:^5.4.4"
type-fest: "npm:^5.2.0"
peerDependencies:
"@types/emscripten": ">=1.39.6"
checksum: 10/0acf04829acf8f3987173af011784642792fc877c7765f79222fe33efff8af09fbf95bf5d590d2490ae39ec411e6c4de06ea24e96d4eb48189b9d06f7502eaa2
checksum: 10/e5928cbb066c854c970cbf724e978e502c3469d69de2469bd37d1f6ab8f5d2a2acdbaa9dea32d35cfc058e2b482e29a9c4f12161d9df3e1e952c30dda3a96d8c
languageName: node
linkType: hard