mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-12 14:47:38 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ccc78114c |
@@ -319,14 +319,7 @@ const SCHEMAS: {
|
||||
selector: { config_entry: {} },
|
||||
},
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
signed_duration: {
|
||||
name: "Signed duration",
|
||||
selector: { duration: { mode: "signed" } },
|
||||
},
|
||||
offset_duration: {
|
||||
name: "Offset",
|
||||
selector: { duration: { mode: "offset", enable_day: true } },
|
||||
},
|
||||
offset: { name: "Offset", selector: { offset: { enable_day: true } } },
|
||||
app: { name: "App", selector: { app: {} } },
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
|
||||
@@ -4,22 +4,28 @@ export const durationValueToData = (
|
||||
value?: HaDurationData | string | number
|
||||
): HaDurationData | undefined => {
|
||||
if (typeof value === "number") {
|
||||
return value < 0
|
||||
? { negative: true, seconds: Math.abs(value) }
|
||||
: { seconds: value };
|
||||
return { seconds: value };
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const negative = value.trim()[0] === "-";
|
||||
const parts = value.split(":").map((p) => Math.abs(Number(p)));
|
||||
let data: HaDurationData | undefined;
|
||||
const parts = value
|
||||
.split(":")
|
||||
.map((p) => (negative && p ? -Math.abs(Number(p)) : Number(p)));
|
||||
|
||||
if (parts.length === 1) {
|
||||
data = { seconds: parts[0] };
|
||||
} else if (parts.length === 2) {
|
||||
data = { hours: parts[0], minutes: parts[1] };
|
||||
} else if (parts.length === 3) {
|
||||
data = { hours: parts[0], minutes: parts[1], seconds: parts[2] };
|
||||
return { seconds: parts[0] };
|
||||
}
|
||||
return data && negative ? { negative: true, ...data } : data;
|
||||
if (parts.length === 2) {
|
||||
return { hours: parts[0], minutes: parts[1] };
|
||||
}
|
||||
if (parts.length === 3) {
|
||||
return {
|
||||
hours: parts[0],
|
||||
minutes: parts[1],
|
||||
seconds: parts[2],
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { HaDurationData } from "../../components/ha-duration-input";
|
||||
import { durationDataToSeconds } from "./duration_to_seconds";
|
||||
|
||||
const COMPONENTS = [
|
||||
"days",
|
||||
"hours",
|
||||
"minutes",
|
||||
"seconds",
|
||||
"milliseconds",
|
||||
] as const;
|
||||
|
||||
export interface NormalizedDuration extends HaDurationData {
|
||||
negative: boolean;
|
||||
}
|
||||
|
||||
export const normalizeDuration = (
|
||||
duration: HaDurationData
|
||||
): NormalizedDuration => {
|
||||
const { negative, ...components } = duration;
|
||||
if (negative !== undefined) {
|
||||
return { negative, ...components };
|
||||
}
|
||||
for (const field of COMPONENTS) {
|
||||
const amount = components[field];
|
||||
if (amount !== undefined) {
|
||||
components[field] = Math.abs(amount);
|
||||
}
|
||||
}
|
||||
return { negative: durationDataToSeconds(duration) < 0, ...components };
|
||||
};
|
||||
@@ -1,28 +1,18 @@
|
||||
import { mdiClose, mdiMenuDown, mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-select";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./input/ha-input";
|
||||
import type { HaInput } from "./input/ha-input";
|
||||
|
||||
const SIGNS = [
|
||||
{ negative: false, label: "+" },
|
||||
{ negative: true, label: "−" },
|
||||
];
|
||||
|
||||
export interface TimeChangedEvent {
|
||||
negative?: boolean;
|
||||
days?: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
@@ -142,10 +132,6 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property({ attribute: false }) amPm: "AM" | "PM" = "AM";
|
||||
|
||||
@property({ attribute: "enable-sign", type: Boolean }) enableSign = false;
|
||||
|
||||
@property({ type: Boolean }) negative = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
@property({ attribute: "placeholder-labels", type: Boolean })
|
||||
@@ -179,43 +165,6 @@ export class HaBaseTimeInput extends LitElement {
|
||||
role="group"
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
${
|
||||
this.enableSign
|
||||
? html`<ha-dropdown
|
||||
placement="bottom-start"
|
||||
@wa-select=${this._signSelected}
|
||||
@wa-after-hide=${stopPropagation}
|
||||
@wa-hide=${stopPropagation}
|
||||
>
|
||||
<button
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class="sign"
|
||||
aria-label=${this.negative ? "-" : "+"}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this.negative ? mdiMinus : mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon
|
||||
class="chevron"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</button>
|
||||
${SIGNS.map(
|
||||
({ negative, label }) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${negative ? "-" : "+"}
|
||||
.selected=${this.negative === negative}
|
||||
>
|
||||
${label}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-dropdown>
|
||||
<div class="sign-divider"></div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.enableDay
|
||||
? html`
|
||||
@@ -375,35 +324,18 @@ export class HaBaseTimeInput extends LitElement {
|
||||
fireEvent(this, "value-changed");
|
||||
}
|
||||
|
||||
private _signSelected(ev: HaDropdownSelectEvent): void {
|
||||
ev.stopPropagation();
|
||||
const negative = ev.detail.item.value === "-";
|
||||
if (negative === this.negative) {
|
||||
return;
|
||||
}
|
||||
this.negative = negative;
|
||||
this._fireValue();
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void {
|
||||
const textField = ev.currentTarget as HaInput;
|
||||
this[textField.name || ""] =
|
||||
textField.name === "amPm"
|
||||
? (ev as HaSelectSelectEvent).detail.value
|
||||
: Number(textField.value);
|
||||
this._fireValue();
|
||||
}
|
||||
|
||||
private _fireValue(): void {
|
||||
const value: TimeChangedEvent = {
|
||||
hours: this.hours,
|
||||
minutes: this.minutes,
|
||||
seconds: this.seconds,
|
||||
milliseconds: this.milliseconds,
|
||||
};
|
||||
if (this.enableSign) {
|
||||
value.negative = this.negative;
|
||||
}
|
||||
if (this.enableDay) {
|
||||
value.days = this.days;
|
||||
}
|
||||
@@ -479,10 +411,6 @@ export class HaBaseTimeInput extends LitElement {
|
||||
padding-inline-start: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.sign-divider + ha-input::part(wa-base) {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-input:last-child::part(wa-base) {
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
}
|
||||
@@ -495,53 +423,6 @@ export class HaBaseTimeInput extends LitElement {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sign {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: 56px;
|
||||
padding: 0;
|
||||
padding-inline: var(--ha-space-3) var(--ha-space-1);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-loud);
|
||||
background-color: var(--ha-color-form-background);
|
||||
color: var(--ha-color-text-secondary);
|
||||
cursor: pointer;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
.sign .chevron {
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
.sign:hover {
|
||||
background-color: var(--ha-color-form-background-hover);
|
||||
}
|
||||
.sign:disabled {
|
||||
cursor: default;
|
||||
color: var(--ha-color-text-disabled);
|
||||
}
|
||||
.sign:focus-visible {
|
||||
outline: 2px solid var(--ha-color-border-primary-normal);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
ha-dropdown-item {
|
||||
font-size: var(--ha-font-size-l);
|
||||
text-align: center;
|
||||
}
|
||||
.sign-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
height: 56px;
|
||||
padding-inline: 0 var(--ha-space-1);
|
||||
background-color: var(--ha-color-form-background);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-loud);
|
||||
}
|
||||
.sign-divider::after {
|
||||
content: "";
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
.time-separator,
|
||||
ha-icon-button {
|
||||
background-color: var(--ha-color-form-background);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { normalizeDuration } from "../common/datetime/normalize_duration";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
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 {
|
||||
negative?: boolean;
|
||||
days?: number;
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
@@ -44,6 +44,8 @@ export class HaDurationInput extends LitElement {
|
||||
|
||||
@query("ha-base-time-input", true) private _input?: HaBaseTimeInput;
|
||||
|
||||
private _toggleNegative = false;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
@@ -54,12 +56,24 @@ export class HaDurationInput extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const data =
|
||||
this.data && this.allowNegative
|
||||
? normalizeDuration(this.data)
|
||||
: this.data;
|
||||
return html`
|
||||
<div class="row">
|
||||
${
|
||||
this.allowNegative
|
||||
? html`
|
||||
<ha-button-toggle-group
|
||||
size="s"
|
||||
.buttons=${[
|
||||
{ label: "+", iconPath: mdiPlusThick, value: "+" },
|
||||
{ label: "-", iconPath: mdiMinusThick, value: "-" },
|
||||
]}
|
||||
.active=${this._negative ? "-" : "+"}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._negativeChanged}
|
||||
></ha-button-toggle-group>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-base-time-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@@ -71,14 +85,12 @@ export class HaDurationInput extends LitElement {
|
||||
.enableSecond=${this.enableSecond}
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
.enableSign=${this.allowNegative}
|
||||
.negative=${!!data?.negative}
|
||||
format="24"
|
||||
.days=${this._component(data, "days")}
|
||||
.hours=${this._component(data, "hours")}
|
||||
.minutes=${this._component(data, "minutes")}
|
||||
.seconds=${this._component(data, "seconds")}
|
||||
.milliseconds=${this._component(data, "milliseconds")}
|
||||
.days=${this._days}
|
||||
.hours=${this._hours}
|
||||
.minutes=${this._minutes}
|
||||
.seconds=${this._seconds}
|
||||
.milliseconds=${this._milliseconds}
|
||||
@value-changed=${this._durationChanged}
|
||||
no-hours-limit
|
||||
day-label="dd"
|
||||
@@ -91,22 +103,77 @@ export class HaDurationInput extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _component(
|
||||
data: HaDurationData | undefined,
|
||||
field: keyof HaDurationData
|
||||
): number {
|
||||
const amount = data?.[field];
|
||||
if (amount) {
|
||||
return Number(amount);
|
||||
}
|
||||
return this.required || data ? 0 : NaN;
|
||||
private get _negative() {
|
||||
return (
|
||||
this._toggleNegative ||
|
||||
(this.data?.days
|
||||
? this.data.days < 0
|
||||
: this.data?.hours
|
||||
? this.data.hours < 0
|
||||
: this.data?.minutes
|
||||
? this.data.minutes < 0
|
||||
: this.data?.seconds
|
||||
? this.data.seconds < 0
|
||||
: this.data?.milliseconds
|
||||
? this.data.milliseconds < 0
|
||||
: false)
|
||||
);
|
||||
}
|
||||
|
||||
private get _days() {
|
||||
return this.data?.days
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.days))
|
||||
: Number(this.data.days)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _hours() {
|
||||
return this.data?.hours
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.hours))
|
||||
: Number(this.data.hours)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _minutes() {
|
||||
return this.data?.minutes
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.minutes))
|
||||
: Number(this.data.minutes)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _seconds() {
|
||||
return this.data?.seconds
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.seconds))
|
||||
: Number(this.data.seconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _milliseconds() {
|
||||
return this.data?.milliseconds
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.milliseconds))
|
||||
: Number(this.data.milliseconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private _durationChanged(
|
||||
ev: ValueChangedEvent<TimeChangedEvent | undefined>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const negative = ev.detail.value?.negative ?? false;
|
||||
const value = ev.detail.value ? { ...ev.detail.value } : undefined;
|
||||
|
||||
if (value) {
|
||||
@@ -150,17 +217,36 @@ export class HaDurationInput extends LitElement {
|
||||
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
|
||||
value.hours %= 24;
|
||||
}
|
||||
|
||||
if (this._negative) {
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = -Math.abs(value[t]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value:
|
||||
value && this.allowNegative ? this._withSign(value, negative) : value,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private _withSign(value: HaDurationData, negative: boolean): HaDurationData {
|
||||
const { negative: _negative, ...components } = normalizeDuration(value);
|
||||
return negative ? { negative: true, ...components } : components;
|
||||
private _negativeChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const negative = (ev.detail?.value || ev.target.value) === "-";
|
||||
this._toggleNegative = negative;
|
||||
if (this.data) {
|
||||
const value = { ...this.data };
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = negative ? -Math.abs(value[t]) : Math.abs(value[t]);
|
||||
}
|
||||
});
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -168,6 +254,9 @@ export class HaDurationInput extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-button-toggle-group {
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ const SELECTOR_FALLBACK_VALUES = {
|
||||
number: (selector) => selector.number?.min ?? 0,
|
||||
numeric_threshold: undefined,
|
||||
object: undefined,
|
||||
offset: undefined,
|
||||
period: undefined,
|
||||
qr_code: undefined,
|
||||
select: undefined,
|
||||
|
||||
@@ -74,6 +74,7 @@ const SELECTOR_INITIAL_VALUES = {
|
||||
};
|
||||
},
|
||||
object: (selector) => (selector.object?.multiple ? [] : ""),
|
||||
offset: () => ({ type: "none" }),
|
||||
period: undefined,
|
||||
qr_code: undefined,
|
||||
select: (selector) => {
|
||||
|
||||
@@ -145,7 +145,7 @@ export class HaSelect extends LitElement {
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
aria-label=${ifDefined(this.ariaLabel || this.label)}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@clear=${this._clearValue}
|
||||
.label=${this.label}
|
||||
.value=${valueLabel}
|
||||
|
||||
@@ -1,34 +1,10 @@
|
||||
import {
|
||||
mdiClockMinusOutline,
|
||||
mdiClockOutline,
|
||||
mdiClockPlusOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { durationDataToSeconds } from "../../common/datetime/duration_to_seconds";
|
||||
import { normalizeDuration } from "../../common/datetime/normalize_duration";
|
||||
import { durationValueToData } from "../../common/datetime/duration_value_to_data";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
import { getDurationSelectorMode } from "../../data/selector";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select";
|
||||
import type { HaSelectSelectEvent } from "../ha-select";
|
||||
|
||||
type OffsetType = "none" | "before" | "after";
|
||||
|
||||
const OFFSET_TYPES: { value: OffsetType; iconPath: string }[] = [
|
||||
{ value: "none", iconPath: mdiClockOutline },
|
||||
{ value: "before", iconPath: mdiClockMinusOutline },
|
||||
{ value: "after", iconPath: mdiClockPlusOutline },
|
||||
];
|
||||
|
||||
@customElement("ha-selector-duration")
|
||||
export class HaTimeDuration extends LitElement {
|
||||
@@ -45,13 +21,7 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@query("ha-duration-input") private _input?: HaDurationInput;
|
||||
|
||||
@state() private _offsetType?: OffsetType;
|
||||
@query("ha-duration-input", true) private _input?: HaDurationInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
@@ -59,193 +29,21 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
private _data = memoizeOne(durationValueToData);
|
||||
|
||||
private _offsetTypeOptions = memoizeOne((localize: LocalizeFunc) =>
|
||||
OFFSET_TYPES.map(({ value, iconPath }) => ({
|
||||
value,
|
||||
iconPath,
|
||||
label: localize(`ui.components.selectors.duration.offset.${value}`),
|
||||
}))
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const mode = getDurationSelectorMode(this.selector.duration);
|
||||
const data = this._data(this.value);
|
||||
|
||||
if (mode !== "offset") {
|
||||
return this._renderInput(
|
||||
data,
|
||||
this.label,
|
||||
this.helper,
|
||||
mode === "signed"
|
||||
);
|
||||
}
|
||||
|
||||
const offsetType = this._getOffsetType(data);
|
||||
return html`
|
||||
<div class="container">
|
||||
${
|
||||
this.label
|
||||
? html`<label id="label"
|
||||
>${this.label}${this.required ? "*" : ""}</label
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
<div
|
||||
class="inputs"
|
||||
role="group"
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
>
|
||||
<ha-select
|
||||
aria-label=${ifDefined(this.label)}
|
||||
.value=${offsetType}
|
||||
.options=${this._offsetTypeOptions(this._localize)}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._offsetTypeChanged}
|
||||
></ha-select>
|
||||
${
|
||||
offsetType === "none"
|
||||
? nothing
|
||||
: html`<div
|
||||
class="value-row"
|
||||
role="group"
|
||||
aria-labelledby="duration-label"
|
||||
>
|
||||
<span id="duration-label" class="value-label"
|
||||
>${this._localize(
|
||||
"ui.components.selectors.duration.duration"
|
||||
)}${this.required ? "*" : ""}</span
|
||||
>
|
||||
${this._renderInput(
|
||||
data && this._components(data),
|
||||
undefined
|
||||
)}
|
||||
</div>`
|
||||
}
|
||||
</div>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderInput(
|
||||
data: HaDurationData | undefined,
|
||||
label: string | undefined,
|
||||
helper?: string,
|
||||
allowNegative = false
|
||||
) {
|
||||
return html`
|
||||
<ha-duration-input
|
||||
.label=${label}
|
||||
.helper=${helper}
|
||||
.data=${data}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.data=${this._data(this.value)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.enableDay=${this.selector.duration?.enable_day}
|
||||
.enableMillisecond=${this.selector.duration?.enable_millisecond}
|
||||
.allowNegative=${allowNegative}
|
||||
.allowNegative=${this.selector.duration?.allow_negative}
|
||||
.enableSecond=${this.selector.duration?.enable_second ?? true}
|
||||
@value-changed=${this._durationChanged}
|
||||
></ha-duration-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getOffsetType(data?: HaDurationData): OffsetType {
|
||||
if (!data) {
|
||||
return this._offsetType ?? "none";
|
||||
}
|
||||
const { negative, ...components } = normalizeDuration(data);
|
||||
if (durationDataToSeconds(components) === 0) {
|
||||
return this._offsetType ?? "none";
|
||||
}
|
||||
return negative ? "before" : "after";
|
||||
}
|
||||
|
||||
private _components(data: HaDurationData): HaDurationData {
|
||||
const { negative: _negative, ...components } = normalizeDuration(data);
|
||||
return components;
|
||||
}
|
||||
|
||||
private _zeroDuration(): HaDurationData {
|
||||
const config = this.selector.duration;
|
||||
const value: HaDurationData = { hours: 0, minutes: 0 };
|
||||
if (config?.enable_day) value.days = 0;
|
||||
if (config?.enable_second ?? true) value.seconds = 0;
|
||||
if (config?.enable_millisecond) value.milliseconds = 0;
|
||||
return value;
|
||||
}
|
||||
|
||||
private _withOffsetType(
|
||||
type: OffsetType,
|
||||
data?: HaDurationData
|
||||
): HaDurationData {
|
||||
if (type === "none") {
|
||||
return this._zeroDuration();
|
||||
}
|
||||
const components = this._components(data ?? this._zeroDuration());
|
||||
return type === "before" ? { negative: true, ...components } : components;
|
||||
}
|
||||
|
||||
private _durationChanged(ev: ValueChangedEvent<HaDurationData | undefined>) {
|
||||
if (getDurationSelectorMode(this.selector.duration) !== "offset") {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
const type = this._getOffsetType(this._data(this.value));
|
||||
this._offsetType = type;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this._withOffsetType(type, ev.detail.value),
|
||||
});
|
||||
}
|
||||
|
||||
private _offsetTypeChanged(ev: HaSelectSelectEvent<OffsetType>) {
|
||||
ev.stopPropagation();
|
||||
const type = ev.detail.value;
|
||||
const data = this._data(this.value);
|
||||
if (!type || type === this._getOffsetType(data)) {
|
||||
return;
|
||||
}
|
||||
this._offsetType = type;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this._withOffsetType(type, data),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
color: var(--ha-color-text-primary);
|
||||
padding-inline-start: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.inputs,
|
||||
.value-row {
|
||||
--ha-input-padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.value-label {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
mdiClockMinusOutline,
|
||||
mdiClockOutline,
|
||||
mdiClockPlusOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { durationValueToData } from "../../common/datetime/duration_value_to_data";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type {
|
||||
OffsetSelector,
|
||||
OffsetSelectorValue,
|
||||
OffsetType,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select";
|
||||
|
||||
const OFFSET_TYPES: { value: OffsetType; iconPath: string }[] = [
|
||||
{ value: "none", iconPath: mdiClockOutline },
|
||||
{ value: "before", iconPath: mdiClockMinusOutline },
|
||||
{ value: "after", iconPath: mdiClockPlusOutline },
|
||||
];
|
||||
|
||||
const DEFAULT_DURATION: HaDurationData = { hours: 0, minutes: 0, seconds: 0 };
|
||||
|
||||
const isOffsetValue = (value: unknown): value is OffsetSelectorValue =>
|
||||
typeof value === "object" && value !== null && "type" in value;
|
||||
|
||||
@customElement("ha-selector-offset")
|
||||
export class HaOffsetSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: OffsetSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: OffsetSelectorValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-duration-input", true) private _durationInput?: HaDurationInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._durationInput?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
private get _value(): OffsetSelectorValue | undefined {
|
||||
return isOffsetValue(this.value) ? this.value : undefined;
|
||||
}
|
||||
|
||||
private _duration = memoizeOne(durationValueToData);
|
||||
|
||||
private _typeOptions = memoizeOne((localize: LocalizeFunc) =>
|
||||
OFFSET_TYPES.map(({ value, iconPath }) => ({
|
||||
value,
|
||||
iconPath,
|
||||
label: localize(`ui.components.selectors.offset.${value}`),
|
||||
}))
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const type = this._value?.type ?? "none";
|
||||
return html`
|
||||
<div class="container">
|
||||
${
|
||||
this.label
|
||||
? html`<label>${this.label}${this.required ? "*" : ""}</label>`
|
||||
: nothing
|
||||
}
|
||||
<div class="inputs">
|
||||
<ha-select
|
||||
.value=${type}
|
||||
.options=${this._typeOptions(this.hass.localize)}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._typeChanged}
|
||||
></ha-select>
|
||||
${
|
||||
type !== "none"
|
||||
? html`<div class="value-row">
|
||||
<span class="value-label"
|
||||
>${this.hass.localize(
|
||||
"ui.components.selectors.offset.duration"
|
||||
)}${this.required ? "*" : ""}</span
|
||||
>
|
||||
<ha-duration-input
|
||||
.data=${this._duration(this._value?.duration)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.enableDay=${this.selector.offset?.enable_day}
|
||||
.enableMillisecond=${
|
||||
this.selector.offset?.enable_millisecond
|
||||
}
|
||||
@value-changed=${this._durationChanged}
|
||||
></ha-duration-input>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _durationChanged(ev: CustomEvent<{ value?: HaDurationData }>) {
|
||||
ev.stopPropagation();
|
||||
const type = this._value?.type;
|
||||
if (!type || type === "none") {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { type, duration: ev.detail.value ?? DEFAULT_DURATION },
|
||||
});
|
||||
}
|
||||
|
||||
private _typeChanged(ev: CustomEvent<{ value?: string }>) {
|
||||
ev.stopPropagation();
|
||||
const type = ev.detail.value as OffsetType | undefined;
|
||||
if (!type || type === this._value?.type) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value:
|
||||
type === "none"
|
||||
? { type }
|
||||
: {
|
||||
type,
|
||||
duration:
|
||||
this._duration(this._value?.duration) ?? DEFAULT_DURATION,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.inputs,
|
||||
.value-row {
|
||||
--ha-input-padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.value-label {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-offset": HaOffsetSelector;
|
||||
}
|
||||
}
|
||||
@@ -72,13 +72,8 @@ const SELECTOR_SCHEMAS = {
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "mode",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["positive", "signed", "offset"],
|
||||
},
|
||||
},
|
||||
name: "allow_negative",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
entity: [
|
||||
@@ -258,13 +253,6 @@ export class HaSelectorSelector extends LitElement {
|
||||
type,
|
||||
...(typeof value0 === "object" ? value0 : []),
|
||||
};
|
||||
if (
|
||||
type === "duration" &&
|
||||
data.mode === undefined &&
|
||||
data.allow_negative
|
||||
) {
|
||||
data.mode = "signed";
|
||||
}
|
||||
}
|
||||
|
||||
const schema = this._schema(type, this._localize!);
|
||||
@@ -305,9 +293,6 @@ export class HaSelectorSelector extends LitElement {
|
||||
this._yamlMode = false;
|
||||
}
|
||||
delete value.type;
|
||||
if (type === "duration" && value.mode !== undefined) {
|
||||
delete value.allow_negative;
|
||||
}
|
||||
|
||||
let newValue;
|
||||
if (type === "manual") {
|
||||
|
||||
@@ -43,6 +43,7 @@ const LOAD_ELEMENTS = {
|
||||
number: () => import("./ha-selector-number"),
|
||||
numeric_threshold: () => import("./ha-selector-numeric-threshold"),
|
||||
object: () => import("./ha-selector-object"),
|
||||
offset: () => import("./ha-selector-offset"),
|
||||
period: () => import("./ha-selector-period"),
|
||||
qr_code: () => import("./ha-selector-qr-code"),
|
||||
select: () => import("./ha-selector-select"),
|
||||
|
||||
@@ -75,7 +75,6 @@ export interface BlueprintAutomationConfig extends ManualAutomationConfig {
|
||||
}
|
||||
|
||||
export interface ForDict {
|
||||
negative?: boolean;
|
||||
days?: number;
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
} from "../common/datetime/format_time";
|
||||
import { normalizeDuration } from "../common/datetime/normalize_duration";
|
||||
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
@@ -872,9 +871,8 @@ const formatSunOffset = (
|
||||
return offset;
|
||||
}
|
||||
try {
|
||||
const { negative, ...components } = normalizeDuration(offset);
|
||||
const formatted = formatDurationDigital(hass.locale, components);
|
||||
return `${negative ? "-" : "+"}${formatted}`;
|
||||
const formatted = formatDurationDigital(hass.locale, offset);
|
||||
return formatted.startsWith("-") ? formatted : `+${formatted}`;
|
||||
} catch (_e) {
|
||||
return JSON.stringify(offset);
|
||||
}
|
||||
|
||||
+16
-8
@@ -22,6 +22,7 @@ import type {
|
||||
EntityRegistryEntry,
|
||||
} from "./entity/entity_registry";
|
||||
import type { EntitySources } from "./entity/entity_sources";
|
||||
import type { HaDurationData } from "../components/ha-duration-input";
|
||||
|
||||
export type ThresholdMode = "crossed" | "changed" | "is";
|
||||
|
||||
@@ -63,6 +64,7 @@ export type Selector =
|
||||
| NumberSelector
|
||||
| NumericThresholdSelector
|
||||
| ObjectSelector
|
||||
| OffsetSelector
|
||||
| PeriodSelector
|
||||
| AssistPipelineSelector
|
||||
| QRCodeSelector
|
||||
@@ -266,23 +268,15 @@ export interface LegacyDeviceSelector {
|
||||
};
|
||||
}
|
||||
|
||||
export type DurationSelectorMode = "positive" | "signed" | "offset";
|
||||
|
||||
export interface DurationSelector {
|
||||
duration: {
|
||||
enable_day?: boolean;
|
||||
enable_millisecond?: boolean;
|
||||
allow_negative?: boolean;
|
||||
enable_second?: boolean;
|
||||
mode?: DurationSelectorMode;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const getDurationSelectorMode = (
|
||||
config: DurationSelector["duration"]
|
||||
): DurationSelectorMode =>
|
||||
config?.mode ?? (config?.allow_negative ? "signed" : "positive");
|
||||
|
||||
interface EntitySelectorFilter {
|
||||
integration?: string;
|
||||
domain?: string | readonly string[];
|
||||
@@ -454,6 +448,20 @@ export interface ObjectSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export type OffsetType = "none" | "before" | "after";
|
||||
|
||||
export interface OffsetSelector {
|
||||
offset: {
|
||||
enable_day?: boolean;
|
||||
enable_millisecond?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface OffsetSelectorValue {
|
||||
type: OffsetType;
|
||||
duration?: HaDurationData | string | number;
|
||||
}
|
||||
|
||||
export type PeriodKey =
|
||||
| "today"
|
||||
| "yesterday"
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { durationDataToSeconds } from "../../common/datetime/duration_to_seconds";
|
||||
import { durationValueToData } from "../../common/datetime/duration_value_to_data";
|
||||
import { normalizeDuration } from "../../common/datetime/normalize_duration";
|
||||
import { formatDurationLong } from "../../common/datetime/format_duration";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HaDurationData } from "../../components/ha-duration-input";
|
||||
import type { DurationSelector } from "../selector";
|
||||
import { getDurationSelectorMode } from "../selector";
|
||||
import type { FrontendLocaleData } from "../translation";
|
||||
|
||||
export const formatDurationSelectorValue = (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
value: HaDurationData | string | number | undefined,
|
||||
config: DurationSelector["duration"],
|
||||
formatDuration: (
|
||||
locale: FrontendLocaleData,
|
||||
duration: HaDurationData
|
||||
) => string = formatDurationLong
|
||||
): string => {
|
||||
const data = durationValueToData(value);
|
||||
if (!data) {
|
||||
return "";
|
||||
}
|
||||
const { negative, ...components } = normalizeDuration(data);
|
||||
const mode = getDurationSelectorMode(config);
|
||||
if (mode === "offset" && durationDataToSeconds(components) === 0) {
|
||||
return "";
|
||||
}
|
||||
const duration = formatDuration(locale, components);
|
||||
if (!duration || mode === "positive") {
|
||||
return duration;
|
||||
}
|
||||
const sign = negative ? "negative" : "positive";
|
||||
return localize(`ui.components.selectors.duration.summary.${mode}_${sign}`, {
|
||||
duration,
|
||||
});
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { durationValueToData } from "../../common/datetime/duration_value_to_data";
|
||||
import { formatDurationLong } from "../../common/datetime/format_duration";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { Selector } from "../selector";
|
||||
import { formatDurationSelectorValue } from "./format_duration_selector_value";
|
||||
import type { OffsetSelectorValue, Selector } from "../selector";
|
||||
|
||||
export const formatSelectorValue = (
|
||||
hass: HomeAssistant,
|
||||
@@ -124,13 +125,19 @@ export const formatSelectorValue = (
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("duration" in selector) {
|
||||
return formatDurationSelectorValue(
|
||||
hass.localize,
|
||||
hass.locale,
|
||||
value,
|
||||
selector.duration
|
||||
);
|
||||
if ("offset" in selector) {
|
||||
const { type, duration } = value as OffsetSelectorValue;
|
||||
const durationData = durationValueToData(duration);
|
||||
if (type === "none" || !durationData) {
|
||||
return "";
|
||||
}
|
||||
const formattedDuration = formatDurationLong(hass.locale, durationData);
|
||||
if (!formattedDuration) {
|
||||
return "";
|
||||
}
|
||||
return hass.localize(`ui.components.selectors.offset.summary.${type}`, {
|
||||
duration: formattedDuration,
|
||||
});
|
||||
}
|
||||
|
||||
return ensureArray(value)
|
||||
|
||||
+10
-14
@@ -566,20 +566,6 @@
|
||||
"background": {
|
||||
"yaml_info": "Background image is set via YAML editor."
|
||||
},
|
||||
"duration": {
|
||||
"duration": "Duration",
|
||||
"offset": {
|
||||
"none": "No offset",
|
||||
"before": "Before",
|
||||
"after": "After"
|
||||
},
|
||||
"summary": {
|
||||
"offset_negative": "{duration} before",
|
||||
"offset_positive": "{duration} after",
|
||||
"signed_negative": "-{duration}",
|
||||
"signed_positive": "+{duration}"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"latitude": "[%key:ui::panel::config::zone::detail::latitude%]",
|
||||
"longitude": "[%key:ui::panel::config::zone::detail::longitude%]",
|
||||
@@ -674,6 +660,16 @@
|
||||
"outside": "Outside range"
|
||||
}
|
||||
},
|
||||
"offset": {
|
||||
"none": "No offset",
|
||||
"before": "Before",
|
||||
"after": "After",
|
||||
"duration": "Duration",
|
||||
"summary": {
|
||||
"before": "{duration} before",
|
||||
"after": "{duration} after"
|
||||
}
|
||||
},
|
||||
"automation_behavior": {
|
||||
"trigger": {
|
||||
"options": {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { durationValueToData } from "../../../src/common/datetime/duration_value_to_data";
|
||||
|
||||
describe("durationValueToData", () => {
|
||||
it("converts numbers to seconds with a flag when negative", () => {
|
||||
expect(durationValueToData(90)).toEqual({ seconds: 90 });
|
||||
expect(durationValueToData(-90)).toEqual({ negative: true, seconds: 90 });
|
||||
});
|
||||
|
||||
it("parses colon separated strings", () => {
|
||||
expect(durationValueToData("90")).toEqual({ seconds: 90 });
|
||||
expect(durationValueToData("1:30")).toEqual({ hours: 1, minutes: 30 });
|
||||
expect(durationValueToData("01:30:15")).toEqual({
|
||||
hours: 1,
|
||||
minutes: 30,
|
||||
seconds: 15,
|
||||
});
|
||||
expect(durationValueToData("1:2:3:4")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("moves a leading minus of a string into the flag", () => {
|
||||
expect(durationValueToData("-03:00:00")).toEqual({
|
||||
negative: true,
|
||||
hours: 3,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
});
|
||||
expect(durationValueToData(" -60")).toEqual({
|
||||
negative: true,
|
||||
seconds: 60,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes dicts through untouched", () => {
|
||||
const value = { negative: true, hours: 1 };
|
||||
expect(durationValueToData(value)).toBe(value);
|
||||
expect(durationValueToData(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeDuration } from "../../../src/common/datetime/normalize_duration";
|
||||
|
||||
describe("normalizeDuration", () => {
|
||||
it("keeps the flag and the components when the flag is present", () => {
|
||||
expect(
|
||||
normalizeDuration({ negative: true, hours: 1, minutes: 30 })
|
||||
).toEqual({ negative: true, hours: 1, minutes: 30 });
|
||||
expect(normalizeDuration({ negative: false, hours: 1 })).toEqual({
|
||||
negative: false,
|
||||
hours: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("derives the flag from legacy components sharing a sign", () => {
|
||||
expect(normalizeDuration({ hours: -1, minutes: -30 })).toEqual({
|
||||
negative: true,
|
||||
hours: 1,
|
||||
minutes: 30,
|
||||
});
|
||||
expect(normalizeDuration({ hours: 0, minutes: 0 })).toEqual({
|
||||
negative: false,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -61,78 +61,3 @@ describe("formatSelectorValue", () => {
|
||||
expect(result).toContain("••••••••");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSelectorValue duration selector", () => {
|
||||
const localizedHass = {
|
||||
locale: { language: "en" },
|
||||
localize: (key: string, values?: Record<string, unknown>) => {
|
||||
const suffix = key.split("ui.components.selectors.duration.summary.")[1];
|
||||
return {
|
||||
offset_negative: `${values?.duration} before`,
|
||||
offset_positive: `${values?.duration} after`,
|
||||
signed_negative: `-${values?.duration}`,
|
||||
signed_positive: `+${values?.duration}`,
|
||||
}[suffix]!;
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
it("formats a positive duration without a sign", () => {
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
{ hours: 1, minutes: 30 },
|
||||
{ duration: {} }
|
||||
)
|
||||
).toBe("1 hour, 30 minutes");
|
||||
});
|
||||
|
||||
it("formats signed durations with an explicit sign", () => {
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
{ negative: true, minutes: 30 },
|
||||
{ duration: { mode: "signed" } }
|
||||
)
|
||||
).toBe("-30 minutes");
|
||||
expect(
|
||||
formatSelectorValue(localizedHass, "00:30:00", {
|
||||
duration: { allow_negative: true },
|
||||
})
|
||||
).toBe("+30 minutes");
|
||||
});
|
||||
|
||||
it("formats offsets as before or after", () => {
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
{ negative: true, hours: 1, minutes: 30 },
|
||||
{ duration: { mode: "offset" } }
|
||||
)
|
||||
).toBe("1 hour, 30 minutes before");
|
||||
expect(
|
||||
formatSelectorValue(localizedHass, "-00:10:00", {
|
||||
duration: { mode: "offset" },
|
||||
})
|
||||
).toBe("10 minutes before");
|
||||
expect(
|
||||
formatSelectorValue(localizedHass, 45, { duration: { mode: "offset" } })
|
||||
).toBe("45 seconds after");
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
{ minutes: -5 },
|
||||
{ duration: { mode: "offset" } }
|
||||
)
|
||||
).toBe("5 minutes before");
|
||||
});
|
||||
|
||||
it("returns an empty string for a zero offset", () => {
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
{ hours: 0, minutes: 0, seconds: 0 },
|
||||
{ duration: { mode: "offset" } }
|
||||
)
|
||||
).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user