From ed001fb10ba3cab6381c261ab6cf01bf5d059010 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 9 Feb 2022 18:20:56 +0100 Subject: [PATCH] Convert time inputs to Lit + mwc (#11609) --- src/common/datetime/create_duration_data.ts | 7 +- src/components/ha-base-time-input.ts | 308 +++++++++++ src/components/ha-duration-input.ts | 112 ++-- .../ha-selector/ha-selector-time.ts | 1 - src/components/ha-textfield.ts | 23 + src/components/ha-time-input.ts | 46 +- src/components/paper-time-input.js | 497 ------------------ .../controls/more-info-input_datetime.ts | 1 - .../hui-input-datetime-entity-row.ts | 1 - 9 files changed, 396 insertions(+), 600 deletions(-) create mode 100644 src/components/ha-base-time-input.ts delete mode 100644 src/components/paper-time-input.js diff --git a/src/common/datetime/create_duration_data.ts b/src/common/datetime/create_duration_data.ts index 92b3d01021..b198b5dbcb 100644 --- a/src/common/datetime/create_duration_data.ts +++ b/src/common/datetime/create_duration_data.ts @@ -1,5 +1,5 @@ -import { HaDurationData } from "../../components/ha-duration-input"; -import { ForDict } from "../../data/automation"; +import type { HaDurationData } from "../../components/ha-duration-input"; +import type { ForDict } from "../../data/automation"; export const createDurationData = ( duration: string | number | ForDict | undefined @@ -19,6 +19,9 @@ export const createDurationData = ( } return { seconds: duration }; } + if (!("days" in duration)) { + return duration; + } const { days, minutes, seconds, milliseconds } = duration; let hours = duration.hours || 0; hours = (hours || 0) + (days || 0) * 24; diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts new file mode 100644 index 0000000000..e8ede0a78b --- /dev/null +++ b/src/components/ha-base-time-input.ts @@ -0,0 +1,308 @@ +import { LitElement, html, TemplateResult, css } from "lit"; +import { customElement, property } from "lit/decorators"; +import "@material/mwc-select/mwc-select"; +import "@material/mwc-list/mwc-list-item"; +import "./ha-textfield"; +import { fireEvent } from "../common/dom/fire_event"; +import { stopPropagation } from "../common/dom/stop_propagation"; + +export interface TimeChangedEvent { + hours: number; + minutes: number; + seconds: number; + milliseconds: number; + amPm?: "AM" | "PM"; +} + +@customElement("ha-base-time-input") +export class HaBaseTimeInput extends LitElement { + /** + * Label for the input + */ + @property() label?: string; + + /** + * auto validate time inputs + */ + @property({ type: Boolean }) autoValidate = false; + + /** + * 12 or 24 hr format + */ + @property({ type: Number }) format: 12 | 24 = 12; + + /** + * disables the inputs + */ + @property({ type: Boolean }) disabled = false; + + /** + * hour + */ + @property({ type: Number }) hours = 0; + + /** + * minute + */ + @property({ type: Number }) minutes = 0; + + /** + * second + */ + @property({ type: Number }) seconds = 0; + + /** + * milli second + */ + @property({ type: Number }) milliseconds = 0; + + /** + * Label for the hour input + */ + @property() hourLabel = ""; + + /** + * Label for the min input + */ + @property() minLabel = ""; + + /** + * Label for the sec input + */ + @property() secLabel = ""; + + /** + * Label for the milli sec input + */ + @property() millisecLabel = ""; + + /** + * show the sec field + */ + @property({ type: Boolean }) enableSecond = false; + + /** + * show the milli sec field + */ + @property({ type: Boolean }) enableMillisecond = false; + + /** + * limit hours input + */ + @property({ type: Boolean }) noHoursLimit = false; + + /** + * AM or PM + */ + @property() amPm: "AM" | "PM" = "AM"; + + /** + * Formatted time string + */ + @property() value?: string; + + protected render(): TemplateResult { + return html` + ${this.label ? html`` : ""} +
+ + + + + ${this.enableSecond + ? html` + ` + : ""} + ${this.enableMillisecond + ? html` + ` + : ""} + ${this.format === 24 + ? "" + : html` + AM + PM + `} +
+ `; + } + + private _valueChanged(ev) { + this[ev.target.name] = + ev.target.name === "amPm" ? ev.target.value : Number(ev.target.value); + const value: TimeChangedEvent = { + hours: this.hours, + minutes: this.minutes, + seconds: this.seconds, + milliseconds: this.milliseconds, + }; + if (this.format === 12) { + value.amPm = this.amPm; + } + fireEvent(this, "value-changed", { + value, + }); + } + + private _onFocus(ev) { + ev.target.select(); + } + + /** + * Format time fragments + */ + private _formatValue(value: number, padding = 2) { + return value.toString().padStart(padding, "0"); + } + + /** + * 24 hour format has a max hr of 23 + */ + private get _hourMax() { + if (this.noHoursLimit) { + return null; + } + if (this.format === 12) { + return 12; + } + return 23; + } + + static styles = css` + :host { + display: block; + } + .time-input-wrap { + display: flex; + border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0; + overflow: hidden; + position: relative; + } + ha-textfield { + width: 40px; + text-align: center; + --mdc-shape-small: 0; + --text-field-appearance: none; + --text-field-padding: 0 4px; + --text-field-suffix-padding-left: 2px; + --text-field-suffix-padding-right: 0; + --text-field-text-align: center; + } + ha-textfield.hasSuffix { + --text-field-padding: 0 0 0 4px; + } + ha-textfield:first-child { + --text-field-border-top-left-radius: var(--mdc-shape-medium); + } + ha-textfield:last-child { + --text-field-border-top-right-radius: var(--mdc-shape-medium); + } + mwc-select { + --mdc-shape-small: 0; + width: 85px; + } + label { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-family: var( + --mdc-typography-body2-font-family, + var(--mdc-typography-font-family, Roboto, sans-serif) + ); + font-size: var(--mdc-typography-body2-font-size, 0.875rem); + line-height: var(--mdc-typography-body2-line-height, 1.25rem); + font-weight: var(--mdc-typography-body2-font-weight, 400); + letter-spacing: var( + --mdc-typography-body2-letter-spacing, + 0.0178571429em + ); + text-decoration: var(--mdc-typography-body2-text-decoration, inherit); + text-transform: var(--mdc-typography-body2-text-transform, inherit); + color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87)); + padding-left: 4px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-base-time-input": HaBaseTimeInput; + } +} diff --git a/src/components/ha-duration-input.ts b/src/components/ha-duration-input.ts index 3de83b1fd3..9ac4e72e2f 100644 --- a/src/components/ha-duration-input.ts +++ b/src/components/ha-duration-input.ts @@ -1,7 +1,8 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; -import "./paper-time-input"; +import "./ha-base-time-input"; +import type { TimeChangedEvent } from "./ha-base-time-input"; export interface HaDurationData { hours?: number; @@ -32,110 +33,69 @@ class HaDurationInput extends LitElement { protected render(): TemplateResult { return html` - + .hours=${this._hours} + .minutes=${this._minutes} + .seconds=${this._seconds} + .milliseconds=${this._milliseconds} + @value-changed=${this._durationChanged} + noHoursLimit + hourLabel="hh" + minLabel="mm" + secLabel="ss" + millisecLabel="ms" + > `; } private get _hours() { - return this.data && this.data.hours ? Number(this.data.hours) : 0; + return this.data?.hours ? Number(this.data.hours) : 0; } private get _minutes() { - return this.data && this.data.minutes ? Number(this.data.minutes) : 0; + return this.data?.minutes ? Number(this.data.minutes) : 0; } private get _seconds() { - return this.data && this.data.seconds ? Number(this.data.seconds) : 0; + return this.data?.seconds ? Number(this.data.seconds) : 0; } private get _milliseconds() { - return this.data && this.data.milliseconds - ? Number(this.data.milliseconds) - : 0; + return this.data?.milliseconds ? Number(this.data.milliseconds) : 0; } - private _parseDuration(value) { - return value.toString().padStart(2, "0"); - } + private _durationChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) { + ev.stopPropagation(); + const value = { ...ev.detail.value }; - private _parseDurationMillisec(value) { - return value.toString().padStart(3, "0"); - } - - private _hourChanged(ev) { - this._durationChanged(ev, "hours"); - } - - private _minChanged(ev) { - this._durationChanged(ev, "minutes"); - } - - private _secChanged(ev) { - this._durationChanged(ev, "seconds"); - } - - private _millisecChanged(ev) { - this._durationChanged(ev, "milliseconds"); - } - - private _durationChanged(ev, unit) { - let value = Number(ev.detail.value); - - if (value === this[`_${unit}`]) { - return; + if (!this.enableMillisecond && !value.milliseconds) { + // @ts-ignore + delete value.milliseconds; + } else if (value.milliseconds > 999) { + value.seconds += Math.floor(value.milliseconds / 1000); + value.milliseconds %= 1000; } - let hours = this._hours; - let minutes = this._minutes; - - if (unit === "seconds" && value > 59) { - minutes += Math.floor(value / 60); - value %= 60; + if (value.seconds > 59) { + value.minutes += Math.floor(value.seconds / 60); + value.seconds %= 60; } - if (unit === "minutes" && value > 59) { - hours += Math.floor(value / 60); - value %= 60; + if (value.minutes > 59) { + value.hours += Math.floor(value.minutes / 60); + value.minutes %= 60; } - const newValue: HaDurationData = { - hours, - minutes, - seconds: this._seconds, - }; - - if (this.enableMillisecond || this._milliseconds) { - newValue.milliseconds = this._milliseconds; - } - - newValue[unit] = value; - fireEvent(this, "value-changed", { - value: newValue, + value, }); } } diff --git a/src/components/ha-selector/ha-selector-time.ts b/src/components/ha-selector/ha-selector-time.ts index f1a116d371..fb3b4e2b8d 100644 --- a/src/components/ha-selector/ha-selector-time.ts +++ b/src/components/ha-selector/ha-selector-time.ts @@ -22,7 +22,6 @@ export class HaTimeSelector extends LitElement { .value=${this.value} .locale=${this.hass.locale} .disabled=${this.disabled} - hide-label enable-second > `; diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts index 8ca6992f2d..7aae676aa7 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -45,6 +45,29 @@ export class HaTextField extends TextFieldBase { .mdc-text-field__input { width: var(--ha-textfield-input-width, 100%); } + .mdc-text-field:not(.mdc-text-field--with-leading-icon) { + padding: var(--text-field-padding, 0px 16px); + } + .mdc-text-field__affix--suffix { + padding-left: var(--text-field-suffix-padding-left, 12px); + padding-right: var(--text-field-suffix-padding-right, 0px); + } + + input { + text-align: var(--text-field-text-align); + } + + /* Chrome, Safari, Edge, Opera */ + :host([no-spinner]) input::-webkit-outer-spin-button, + :host([no-spinner]) input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + /* Firefox */ + :host([no-spinner]) input[type="number"] { + -moz-appearance: textfield; + } `, ]; } diff --git a/src/components/ha-time-input.ts b/src/components/ha-time-input.ts index 1b14f40579..5efd5a13f3 100644 --- a/src/components/ha-time-input.ts +++ b/src/components/ha-time-input.ts @@ -2,12 +2,13 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { useAmPm } from "../common/datetime/use_am_pm"; import { fireEvent } from "../common/dom/fire_event"; -import "./paper-time-input"; +import "./ha-base-time-input"; import { FrontendLocaleData } from "../data/translation"; +import type { TimeChangedEvent } from "./ha-base-time-input"; @customElement("ha-time-input") export class HaTimeInput extends LitElement { - @property() public locale!: FrontendLocaleData; + @property({ attribute: false }) public locale!: FrontendLocaleData; @property() public value?: string; @@ -15,9 +16,6 @@ export class HaTimeInput extends LitElement { @property({ type: Boolean }) public disabled = false; - @property({ type: Boolean, attribute: "hide-label" }) public hideLabel = - false; - @property({ type: Boolean, attribute: "enable-second" }) public enableSecond = false; @@ -35,40 +33,44 @@ export class HaTimeInput extends LitElement { } return html` - = 12 ? "PM" : "AM")} .disabled=${this.disabled} - @change=${this._timeChanged} - @am-pm-changed=${this._timeChanged} - .hideLabel=${this.hideLabel} + @value-changed=${this._timeChanged} .enableSecond=${this.enableSecond} - > + > `; } - private _timeChanged(ev) { - let value = ev.target.value; + private _timeChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) { + ev.stopPropagation(); + const eventValue = ev.detail.value; + const useAMPM = useAmPm(this.locale); - let hours = Number(ev.target.hour || 0); - if (value && useAMPM) { - if (ev.target.amPm === "PM" && hours < 12) { + let hours = eventValue.hours || 0; + if (eventValue && useAMPM) { + if (eventValue.amPm === "PM" && hours < 12) { hours += 12; } - if (ev.target.amPm === "AM" && hours === 12) { + if (eventValue.amPm === "AM" && hours === 12) { hours = 0; } - value = `${hours.toString().padStart(2, "0")}:${ev.target.min || "00"}:${ - ev.target.sec || "00" - }`; } + const value = `${hours.toString().padStart(2, "0")}:${ + eventValue.minutes ? eventValue.minutes.toString().padStart(2, "0") : "00" + }:${ + eventValue.seconds ? eventValue.seconds.toString().padStart(2, "0") : "00" + }`; + if (value === this.value) { return; } + this.value = value; fireEvent(this, "change"); fireEvent(this, "value-changed", { diff --git a/src/components/paper-time-input.js b/src/components/paper-time-input.js deleted file mode 100644 index 99e9c8de8f..0000000000 --- a/src/components/paper-time-input.js +++ /dev/null @@ -1,497 +0,0 @@ -/** -Adapted from paper-time-input from -https://github.com/ryanburns23/paper-time-input -MIT Licensed. Copyright (c) 2017 Ryan Burns - -`` Polymer element to accept a time with paper-input & paper-dropdown-menu -Inspired by the time input in google forms - -### Styling - -`` provides the following custom properties and mixins for styling: - -Custom property | Description | Default -----------------|-------------|---------- -`--paper-time-input-dropdown-ripple-color` | dropdown ripple color | `--primary-color` -`--paper-time-input-cotnainer` | Mixin applied to the inputs | `{}` -`--paper-time-dropdown-input-cotnainer` | Mixin applied to the dropdown input | `{}` -*/ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -export class PaperTimeInput extends PolymerElement { - static get template() { - return html` - - - -
- - - : - - - - - : - - - - - : - - - - - - - - - - AM - PM - - -
- `; - } - - static get properties() { - return { - /** - * Label for the input - */ - label: { - type: String, - value: "Time", - }, - /** - * auto validate time inputs - */ - autoValidate: { - type: Boolean, - value: true, - }, - /** - * hides the label - */ - hideLabel: { - type: Boolean, - value: false, - }, - /** - * float the input labels - */ - floatInputLabels: { - type: Boolean, - value: false, - }, - /** - * always float the input labels - */ - alwaysFloatInputLabels: { - type: Boolean, - value: false, - }, - /** - * 12 or 24 hr format - */ - format: { - type: Number, - value: 12, - }, - /** - * disables the inputs - */ - disabled: { - type: Boolean, - value: false, - }, - /** - * hour - */ - hour: { - type: String, - notify: true, - }, - /** - * minute - */ - min: { - type: String, - notify: true, - }, - /** - * second - */ - sec: { - type: String, - notify: true, - }, - /** - * milli second - */ - millisec: { - type: String, - notify: true, - }, - /** - * Label for the hour input - */ - hourLabel: { - type: String, - value: "", - }, - /** - * Label for the min input - */ - minLabel: { - type: String, - value: "", - }, - /** - * Label for the sec input - */ - secLabel: { - type: String, - value: "", - }, - /** - * Label for the milli sec input - */ - millisecLabel: { - type: String, - value: "", - }, - /** - * show the sec field - */ - enableSecond: { - type: Boolean, - value: false, - }, - /** - * show the milli sec field - */ - enableMillisecond: { - type: Boolean, - value: false, - }, - /** - * limit hours input - */ - noHoursLimit: { - type: Boolean, - value: false, - }, - /** - * AM or PM - */ - amPm: { - type: String, - notify: true, - value: "AM", - }, - /** - * Formatted time string - */ - value: { - type: String, - notify: true, - readOnly: true, - computed: "_computeTime(min, hour, sec, millisec, amPm)", - }, - }; - } - - /** - * Validate the inputs - * @return {boolean} - */ - validate() { - let valid = true; - // Validate hour & min fields - if (!this.$.hour.validate() || !this.$.min.validate()) { - valid = false; - } - // Validate second field - if (this.enableSecond && !this.$.sec.validate()) { - valid = false; - } - // Validate milli second field - if (this.enableMillisecond && !this.$.millisec.validate()) { - valid = false; - } - // Validate AM PM if 12 hour time - if (this.format === 12 && !this.$.dropdown.validate()) { - valid = false; - } - return valid; - } - - /** - * Create time string - */ - _computeTime(min, hour, sec, millisec, amPm) { - let str; - if ( - hour || - min || - (sec && this.enableSecond) || - (millisec && this.enableMillisecond) - ) { - hour = hour || "00"; - min = min || "00"; - sec = sec || "00"; - millisec = millisec || "000"; - str = hour + ":" + min; - // add sec field - if (this.enableSecond && sec) { - str = str + ":" + sec; - } - // add milli sec field - if (this.enableMillisecond && millisec) { - str = str + ":" + millisec; - } - // No ampm on 24 hr time - if (this.format === 12) { - str = str + " " + amPm; - } - } - - return str; - } - - _onFocus(ev) { - ev.target.inputElement.inputElement.select(); - } - - /** - * Format milli sec - */ - _formatMillisec() { - if (this.millisec.toString().length === 1) { - this.millisec = this.millisec.toString().padStart(3, "0"); - } - } - - /** - * Format sec - */ - _formatSec() { - if (this.sec.toString().length === 1) { - this.sec = this.sec.toString().padStart(2, "0"); - } - } - - /** - * Format min - */ - _formatMin() { - if (this.min.toString().length === 1) { - this.min = this.min.toString().padStart(2, "0"); - } - } - - /** - * Format hour - */ - _shouldFormatHour() { - if (this.format === 24 && this.hour.toString().length === 1) { - this.hour = this.hour.toString().padStart(2, "0"); - } - } - - /** - * 24 hour format has a max hr of 23 - */ - _computeHourMax(format) { - if (this.noHoursLimit) { - return null; - } - if (format === 12) { - return format; - } - return 23; - } - - _equal(n1, n2) { - return n1 === n2; - } - - _computeClassNames(hasSuffix) { - return hasSuffix ? " " : "no-suffix"; - } -} - -customElements.define("paper-time-input", PaperTimeInput); diff --git a/src/dialogs/more-info/controls/more-info-input_datetime.ts b/src/dialogs/more-info/controls/more-info-input_datetime.ts index 4d1be5c70b..a0f11eb726 100644 --- a/src/dialogs/more-info/controls/more-info-input_datetime.ts +++ b/src/dialogs/more-info/controls/more-info-input_datetime.ts @@ -43,7 +43,6 @@ class MoreInfoInputDatetime extends LitElement { : this.stateObj.state} .locale=${this.hass.locale} .disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)} - hide-label @value-changed=${this._timeChanged} @click=${this._stopEventPropagation} > diff --git a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts index 21a8ada291..a727d2f67d 100644 --- a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts @@ -72,7 +72,6 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { : stateObj.state} .locale=${this.hass.locale} .disabled=${UNAVAILABLE_STATES.includes(stateObj.state)} - hide-label @value-changed=${this._timeChanged} @click=${this._stopEventPropagation} >