From 7715e0112601d3d5e634ec00c88bde73fb560105 Mon Sep 17 00:00:00 2001 From: Tom Carpenter Date: Mon, 30 Mar 2026 07:13:51 +0100 Subject: [PATCH] Add date range picker time validation (#51267) * Fix base time inputs reportValidity() function The queryAll selector returns a NodeList not not an array. Need to spread it to an array before we can use every(). * Validate the date range picker time inputs Enable auto validation to get the nice red underline on invalid values, and then check validity before accepting the input. * Fix automatic 24hr value conversion in AM/PM format When using AM/PM, entering a 24 hour value will automatically convert the first time. For example 15 will become 3. However if you then enter 15 again it will stay as 15 and not update. To fix this, make sure we trigger an update of the input field once the current update cycle is complete. * Validate time inputs on save not value update In the value changed callback, the update 24->12hr input correction will not have been updated and therefore they will report invalid. --- .../date-picker/date-range-picker.ts | 18 ++++++++++++++++-- src/components/ha-base-time-input.ts | 6 ++++-- src/components/ha-time-input.ts | 17 +++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/components/date-picker/date-range-picker.ts b/src/components/date-picker/date-range-picker.ts index 677660c3f3..3152615d31 100644 --- a/src/components/date-picker/date-range-picker.ts +++ b/src/components/date-picker/date-range-picker.ts @@ -4,7 +4,7 @@ import type { ActionDetail } from "@material/mwc-list"; import { mdiCalendarToday } from "@mdi/js"; import "cally"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property, queryAll, state } from "lit/decorators"; import { firstWeekdayIndex } from "../../common/datetime/first_weekday"; import { formatCallyDateRange, @@ -29,6 +29,7 @@ import "../ha-list-item"; import "../ha-time-input"; import type { DateRangePickerRanges } from "./ha-date-range-picker"; import { datePickerStyles, dateRangePickerStyles } from "./styles"; +import type { HaTimeInput } from "../ha-time-input"; @customElement("date-range-picker") export class DateRangePicker extends LitElement { @@ -69,6 +70,8 @@ export class DateRangePicker extends LitElement { to: { hours: 23, minutes: 59 }, }; + @queryAll("ha-time-input") private _timeInputs?: NodeListOf; + public connectedCallback() { super.connectedCallback(); @@ -153,6 +156,7 @@ export class DateRangePicker extends LitElement { )} id="from" placeholder-labels + auto-validate > ` @@ -200,6 +205,14 @@ export class DateRangePicker extends LitElement { let endDate = new Date(`${dates[1]}T23:59:00`); if (this.timePicker) { + const timeInputs = this._timeInputs; + if ( + timeInputs && + ![...timeInputs].every((input) => input.reportValidity()) + ) { + // If we have time inputs, and they don't all report valid, don't save + return; + } startDate.setHours(this._timeValue.from.hours); startDate.setMinutes(this._timeValue.from.minutes); endDate.setHours(this._timeValue.to.hours); @@ -281,7 +294,8 @@ export class DateRangePicker extends LitElement { private _handleChangeTime(ev: ValueChangedEvent) { ev.stopPropagation(); const time = ev.detail.value; - const type = (ev.target as HaBaseTimeInput).id; + const target = ev.target as HaBaseTimeInput; + const type = target.id; if (time) { if (!this._timeValue) { this._timeValue = { diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index e0aeb75142..3ab107c2e4 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -137,7 +137,7 @@ export class HaBaseTimeInput extends LitElement { @property({ attribute: "placeholder-labels", type: Boolean }) public placeholderLabels = false; - @queryAll("ha-input") private _inputs?: HaInput[]; + @queryAll("ha-input") private _inputs?: NodeListOf; static shadowRootOptions = { ...LitElement.shadowRootOptions, @@ -145,7 +145,9 @@ export class HaBaseTimeInput extends LitElement { }; public reportValidity(): boolean { - return this._inputs?.every((input) => input.reportValidity()) ?? true; + const inputs = this._inputs; + if (!inputs) return true; + return [...inputs].every((input) => input.reportValidity()); } protected render(): TemplateResult { diff --git a/src/components/ha-time-input.ts b/src/components/ha-time-input.ts index 79281ccd3a..090a6c2dbb 100644 --- a/src/components/ha-time-input.ts +++ b/src/components/ha-time-input.ts @@ -21,6 +21,8 @@ export class HaTimeInput extends LitElement { @property({ type: Boolean }) public required = false; + @property({ attribute: "auto-validate", type: Boolean }) autoValidate = false; + @property({ type: Boolean, attribute: "enable-second" }) public enableSecond = false; @@ -71,6 +73,7 @@ export class HaTimeInput extends LitElement { .clearable=${this.clearable && this.value !== undefined} .helper=${this.helper} .placeholderLabels=${this.placeholderLabels} + .autoValidate=${this.autoValidate} day-label="dd" hour-label="hh" min-label="mm" @@ -86,6 +89,7 @@ export class HaTimeInput extends LitElement { const useAMPM = useAmPm(this.locale); let value: string | undefined; + let updateHours = 0; // An undefined eventValue means the time selector is being cleared, // the `value` variable will (intentionally) be left undefined. @@ -97,6 +101,8 @@ export class HaTimeInput extends LitElement { ) { let hours = eventValue.hours || 0; if (eventValue && useAMPM) { + updateHours = + hours >= 12 && hours < 24 ? hours - 12 : hours === 0 ? 12 : 0; if (eventValue.amPm === "PM" && hours < 12) { hours += 12; } @@ -115,6 +121,17 @@ export class HaTimeInput extends LitElement { }`; } + if (updateHours) { + // If the user entered a 24hr time in a 12hr input, we need to refresh the + // input to ensure it resets back to the 12hr equivalent. + this.updateComplete.then(() => { + const input = this._input; + if (input) { + input.hours = updateHours; + } + }); + } + if (value === this.value) { return; }