Files
frontend/src/components/ha-time-picker.ts
Aidan Timson 0ce17e3dea Keep number
2025-09-04 14:04:07 +01:00

282 lines
7.2 KiB
TypeScript

import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { clampValue } from "../data/number";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event";
import type { FrontendLocaleData } from "../data/translation";
import type { HomeAssistant } from "../types";
import type { ClampedValue } from "../data/number";
import "./ha-base-time-input";
import "./ha-button";
import "./ha-numeric-arrow-input";
@customElement("ha-time-picker")
export class HaTimePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public locale!: FrontendLocaleData;
@property({ attribute: false }) public value?: string;
@property({ attribute: false }) public disabled = false;
@property({ attribute: false }) public required = false;
@property({ attribute: false }) public enableSeconds = false;
@state() private _hours = 0;
@state() private _minutes = 0;
@state() private _seconds = 0;
@state() private _useAmPm = false;
@state() private _isPm = false;
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this._useAmPm = useAmPm(this.locale);
let hours = NaN;
let minutes = NaN;
let seconds = NaN;
let isPm = false;
if (this.value) {
const parts = this.value?.split(":") || [];
minutes = parts[1] ? Number(parts[1]) : 0;
seconds = parts[2] ? Number(parts[2]) : 0;
const hour24 = parts[0] ? Number(parts[0]) : 0;
if (this._useAmPm) {
if (hour24 === 0) {
hours = 12;
isPm = false;
} else if (hour24 < 12) {
hours = hour24;
isPm = false;
} else if (hour24 === 12) {
hours = 12;
isPm = true;
} else {
hours = hour24 - 12;
isPm = true;
}
} else {
hours = hour24;
}
}
this._hours = hours;
this._minutes = minutes;
this._seconds = seconds;
this._isPm = isPm;
}
protected render() {
return html`<div class="time-picker-container">
<ha-numeric-arrow-input
.disabled=${this.disabled}
.required=${this.required}
.min=${this._useAmPm ? 1 : 0}
.max=${this._useAmPm ? 12 : 23}
.step=${1}
.padStart=${2}
.value=${this._hours}
@value-changed=${this._hoursChanged}
.labelUp=${
// TODO: Localize
"Increase hours"
}
.labelDown=${
// TODO: Localize
"Decrease hours"
}
></ha-numeric-arrow-input>
<span class="time-picker-separator">:</span>
<ha-numeric-arrow-input
.disabled=${this.disabled}
.required=${this.required}
.min=${0}
.max=${59}
.step=${1}
.padStart=${2}
.labelUp=${
// TODO: Localize
"Increase minutes"
}
.labelDown=${
// TODO: Localize
"Decrease minutes"
}
.value=${this._minutes}
@value-changed=${this._minutesChanged}
></ha-numeric-arrow-input>
${this.enableSeconds
? html`
<span class="time-picker-separator">:</span>
<ha-numeric-arrow-input
.disabled=${this.disabled}
.required=${this.required}
.min=${0}
.max=${59}
.step=${1}
.padStart=${2}
.labelUp=${
// TODO: Localize
"Increase seconds"
}
.labelDown=${
// TODO: Localize
"Decrease seconds"
}
.value=${this._seconds}
@value-changed=${this._secondsChanged}
></ha-numeric-arrow-input>
`
: nothing}
${this._useAmPm
? html`
<ha-button @click=${this._toggleAmPm}>
${this._isPm ? "PM" : "AM"}
</ha-button>
`
: nothing}
</div>`;
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("_hours")) {
this._timeUpdated();
}
if (changedProperties.has("_minutes")) {
this._timeUpdated();
}
if (changedProperties.has("_seconds")) {
this._timeUpdated();
}
if (changedProperties.has("_useAmPm")) {
this._timeUpdated();
}
if (changedProperties.has("_isPm")) {
this._timeUpdated();
}
}
private _hoursChanged(ev: CustomEvent<ClampedValue>) {
ev.stopPropagation?.();
this._hours = ev.detail.value;
}
private _minutesChanged(ev: CustomEvent<ClampedValue>) {
ev.stopPropagation?.();
this._minutes = ev.detail.value;
if (ev.detail.clamped) {
if (ev.detail.value === 0) {
this._hoursChanged({
detail: clampValue({
value: this._hours - 1,
min: this._useAmPm ? 1 : 0,
max: this._useAmPm ? 12 : 23,
}),
} as CustomEvent<ClampedValue>);
this._minutes = 59;
}
if (ev.detail.value === 59) {
this._hoursChanged({
detail: clampValue({
value: this._hours + 1,
min: this._useAmPm ? 1 : 0,
max: this._useAmPm ? 12 : 23,
}),
} as CustomEvent<ClampedValue>);
const hourMax = this._useAmPm ? 12 : 23;
if (this._hours < hourMax) {
this._minutes = 0;
}
}
}
}
private _secondsChanged(ev: CustomEvent<ClampedValue>) {
ev.stopPropagation?.();
this._seconds = ev.detail.value;
if (ev.detail.clamped) {
if (ev.detail.value === 0) {
this._minutesChanged({
detail: clampValue({ value: this._minutes - 1, min: 0, max: 59 }),
} as CustomEvent<ClampedValue>);
this._seconds = 59;
}
if (ev.detail.value === 59) {
this._minutesChanged({
detail: clampValue({ value: this._minutes + 1, min: 0, max: 59 }),
} as CustomEvent<ClampedValue>);
const hourMax = this._useAmPm ? 12 : 23;
if (!(this._hours === hourMax && this._minutes === 59)) {
this._seconds = 0;
}
}
}
}
private _toggleAmPm() {
this._isPm = !this._isPm;
}
private _timeUpdated() {
let hour24 = this._hours;
if (this._useAmPm) {
if (this._hours === 12) {
hour24 = this._isPm ? 12 : 0;
} else {
hour24 = this._isPm ? this._hours + 12 : this._hours;
}
}
const timeParts = [
hour24.toString().padStart(2, "0"),
this._minutes.toString().padStart(2, "0"),
this._seconds.toString().padStart(2, "0"),
];
const time = timeParts.join(":");
if (time === this.value) {
return;
}
this.value = time;
fireEvent(this, "change");
fireEvent(this, "value-changed", { value: time });
}
static styles = css`
.time-picker-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
}
.time-picker-separator {
color: var(--primary-text-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-time-picker": HaTimePicker;
}
}