Skip to content

Commit

Permalink
Clearable time selector (#18590)
Browse files Browse the repository at this point in the history
* initial attempt at clearable time selector

* only show clear button if there is a value

* use null instead of undefined.

* leave event value undefined

* move clear button into input

Co-authored-by: Bram Kragten <[email protected]>

---------

Co-authored-by: J. Nick Koston <[email protected]>
Co-authored-by: Bram Kragten <[email protected]>
  • Loading branch information
3 people authored Jul 25, 2024
1 parent 89d842c commit 0358fe5
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 104 deletions.
238 changes: 139 additions & 99 deletions src/components/ha-base-time-input.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import "@material/mwc-list/mwc-list-item";
import { css, html, LitElement, TemplateResult } from "lit";
import { css, html, LitElement, TemplateResult, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { mdiClose } from "@mdi/js";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import "./ha-icon-button";
import { HaTextField } from "./ha-textfield";
import "./ha-input-helper-text";

Expand Down Expand Up @@ -124,116 +126,128 @@ export class HaBaseTimeInput extends LitElement {
*/
@property() amPm: "AM" | "PM" = "AM";

@property({ type: Boolean, reflect: true }) public clearable?: boolean;

protected render(): TemplateResult {
return html`
${this.label
? html`<label>${this.label}${this.required ? " *" : ""}</label>`
: ""}
<div class="time-input-wrap">
${this.enableDay
? html`
<ha-textfield
id="day"
<div class="time-input-wrap-wrap">
<div class="time-input-wrap">
${this.enableDay
? html`
<ha-textfield
id="day"
type="number"
inputmode="numeric"
.value=${this.days.toFixed()}
.label=${this.dayLabel}
name="days"
@change=${this._valueChanged}
@focusin=${this._onFocus}
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
min="0"
.disabled=${this.disabled}
suffix=":"
class="hasSuffix"
>
</ha-textfield>
`
: ""}
<ha-textfield
id="hour"
type="number"
inputmode="numeric"
.value=${this.hours.toFixed()}
.label=${this.hourLabel}
name="hours"
@change=${this._valueChanged}
@focusin=${this._onFocus}
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max=${ifDefined(this._hourMax)}
min="0"
.disabled=${this.disabled}
suffix=":"
class="hasSuffix"
>
</ha-textfield>
<ha-textfield
id="min"
type="number"
inputmode="numeric"
.value=${this._formatValue(this.minutes)}
.label=${this.minLabel}
@change=${this._valueChanged}
@focusin=${this._onFocus}
name="minutes"
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max="59"
min="0"
.disabled=${this.disabled}
.suffix=${this.enableSecond ? ":" : ""}
class=${this.enableSecond ? "has-suffix" : ""}
>
</ha-textfield>
${this.enableSecond
? html`<ha-textfield
id="sec"
type="number"
inputmode="numeric"
.value=${this.days.toFixed()}
.label=${this.dayLabel}
name="days"
.value=${this._formatValue(this.seconds)}
.label=${this.secLabel}
@change=${this._valueChanged}
@focusin=${this._onFocus}
name="seconds"
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max="59"
min="0"
.disabled=${this.disabled}
suffix=":"
class="hasSuffix"
.suffix=${this.enableMillisecond ? ":" : ""}
class=${this.enableMillisecond ? "has-suffix" : ""}
>
</ha-textfield>
`
: ""}
</ha-textfield>`
: ""}
${this.enableMillisecond
? html`<ha-textfield
id="millisec"
type="number"
.value=${this._formatValue(this.milliseconds, 3)}
.label=${this.millisecLabel}
@change=${this._valueChanged}
@focusin=${this._onFocus}
name="milliseconds"
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="3"
max="999"
min="0"
.disabled=${this.disabled}
>
</ha-textfield>`
: ""}
${this.clearable && !this.required && !this.disabled
? html`<ha-icon-button
label="clear"
@click=${this._clearValue}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
</div>
<ha-textfield
id="hour"
type="number"
inputmode="numeric"
.value=${this.hours.toFixed()}
.label=${this.hourLabel}
name="hours"
@change=${this._valueChanged}
@focusin=${this._onFocus}
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max=${ifDefined(this._hourMax)}
min="0"
.disabled=${this.disabled}
suffix=":"
class="hasSuffix"
>
</ha-textfield>
<ha-textfield
id="min"
type="number"
inputmode="numeric"
.value=${this._formatValue(this.minutes)}
.label=${this.minLabel}
@change=${this._valueChanged}
@focusin=${this._onFocus}
name="minutes"
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max="59"
min="0"
.disabled=${this.disabled}
.suffix=${this.enableSecond ? ":" : ""}
class=${this.enableSecond ? "has-suffix" : ""}
>
</ha-textfield>
${this.enableSecond
? html`<ha-textfield
id="sec"
type="number"
inputmode="numeric"
.value=${this._formatValue(this.seconds)}
.label=${this.secLabel}
@change=${this._valueChanged}
@focusin=${this._onFocus}
name="seconds"
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="2"
max="59"
min="0"
.disabled=${this.disabled}
.suffix=${this.enableMillisecond ? ":" : ""}
class=${this.enableMillisecond ? "has-suffix" : ""}
>
</ha-textfield>`
: ""}
${this.enableMillisecond
? html`<ha-textfield
id="millisec"
type="number"
.value=${this._formatValue(this.milliseconds, 3)}
.label=${this.millisecLabel}
@change=${this._valueChanged}
@focusin=${this._onFocus}
name="milliseconds"
no-spinner
.required=${this.required}
.autoValidate=${this.autoValidate}
maxlength="3"
max="999"
min="0"
.disabled=${this.disabled}
>
</ha-textfield>`
: ""}
${this.format === 24
? ""
: html`<ha-select
Expand All @@ -249,13 +263,17 @@ export class HaBaseTimeInput extends LitElement {
<mwc-list-item value="AM">AM</mwc-list-item>
<mwc-list-item value="PM">PM</mwc-list-item>
</ha-select>`}
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}

private _clearValue(): void {
fireEvent(this, "value-changed");
}

private _valueChanged(ev: InputEvent) {
const textField = ev.currentTarget as HaTextField;
this[textField.name] =
Expand Down Expand Up @@ -302,18 +320,25 @@ export class HaBaseTimeInput extends LitElement {
}

static styles = css`
:host([clearable]) {
position: relative;
}
:host {
display: block;
}
.time-input-wrap-wrap {
display: flex;
}
.time-input-wrap {
display: flex;
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
overflow: hidden;
position: relative;
direction: ltr;
padding-right: 3px;
}
ha-textfield {
width: 40px;
width: 55px;
text-align: center;
--mdc-shape-small: 0;
--text-field-appearance: none;
Expand All @@ -335,6 +360,21 @@ export class HaBaseTimeInput extends LitElement {
--mdc-shape-small: 0;
width: 85px;
}
:host([clearable]) .mdc-select__anchor {
padding-inline-end: var(--select-selected-text-padding-end, 12px);
}
ha-icon-button {
position: relative
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
display: flex;
align-items: center;
background-color:var(--mdc-text-field-fill-color, whitesmoke);
border-bottom-style: solid;
border-bottom-width: 1px;
}
label {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
Expand Down
1 change: 1 addition & 0 deletions src/components/ha-selector/ha-selector-time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class HaTimeSelector extends LitElement {
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
clearable
.helper=${this.helper}
.label=${this.label}
enable-second
Expand Down
16 changes: 11 additions & 5 deletions src/components/ha-time-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export class HaTimeInput extends LitElement {
@property({ type: Boolean, attribute: "enable-second" })
public enableSecond = false;

@property({ type: Boolean, reflect: true }) public clearable?: boolean;

protected render() {
const useAMPM = useAmPm(this.locale);

Expand All @@ -48,22 +50,26 @@ export class HaTimeInput extends LitElement {
@value-changed=${this._timeChanged}
.enableSecond=${this.enableSecond}
.required=${this.required}
.clearable=${this.clearable && this.value !== undefined}
.helper=${this.helper}
></ha-base-time-input>
`;
}

private _timeChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) {
private _timeChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
ev.stopPropagation();
const eventValue = ev.detail.value;

const useAMPM = useAmPm(this.locale);
let value;
let value: string | undefined;

// An undefined eventValue means the time selector is being cleared,
// the `value` variable will (intentionally) be left undefined.
if (
!isNaN(eventValue.hours) ||
!isNaN(eventValue.minutes) ||
!isNaN(eventValue.seconds)
eventValue !== undefined &&
(!isNaN(eventValue.hours) ||
!isNaN(eventValue.minutes) ||
!isNaN(eventValue.seconds))
) {
let hours = eventValue.hours || 0;
if (eventValue && useAMPM) {
Expand Down

0 comments on commit 0358fe5

Please sign in to comment.