Files
Home-Assistant/custom_components/maintenance_supporter/frontend-src/components/ms-textfield.ts
T
2026-07-08 10:43:39 -04:00

117 lines
4.2 KiB
TypeScript

/** ms-textfield — drop-in for `<ha-textfield>` that always renders.
*
* Background: HA's `<ha-textfield>` is lazy-loaded by HA's frontend on first
* use. In contexts where HA hasn't yet imported it (custom panels mounted
* via panel_custom; Lovelace dialogs mounted via dialog-mount onto
* document.body), `customElements.get("ha-textfield")` returns undefined,
* the element renders as HTMLUnknownElement with `offsetHeight: 0`, and
* the user sees an apparently empty form with only the label visible.
*
* Reported manifestations:
* - Issue #50: complete-dialog notes/cost/duration invisible
* - Issue #50 follow-up: task-dialog target-entity invisible
* - Issue #46 follow-up: object-dialog name/manufacturer/model/serial/url/
* notes invisible (only ha-area-picker rendered, hence user could
* "only change the area")
*
* This wrapper uses a native `<input>` element styled to match HA's
* appearance. Same API surface as ha-textfield: `label`, `value`, `type`,
* `placeholder`, `required`, `step`, `min`, `max`, `pattern`. Fires a
* standard `input` event with `event.target.value` so existing handlers
* work unchanged.
*
* The CSS uses HA's CSS custom properties so the input visually matches
* the surrounding HA UI in both light and dark themes.
*/
import { LitElement, html, css, nothing } from "lit";
import { property } from "lit/decorators.js";
export class MsTextfield extends LitElement {
@property() public label = "";
@property() public value = "";
@property() public placeholder = "";
@property() public type: "text" | "number" | "url" | "email" | "password" | "date" | "time" | "datetime-local" = "text";
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public disabled = false;
@property() public step?: string;
@property() public min?: string;
@property() public max?: string;
@property() public pattern?: string;
@property() public helper?: string;
/** Forwards the native input's value into our `value` property and
* re-fires as a bubbling `input` event so consumers reading
* `(e.target as HTMLInputElement).value` still work — that's the
* pattern used everywhere ha-textfield was. */
private _onInput(e: Event): void {
const v = (e.target as HTMLInputElement).value;
this.value = v;
this.dispatchEvent(
new CustomEvent("input", { bubbles: true, composed: true, detail: { value: v } }),
);
}
render() {
return html`
<label class="field">
${this.label ? html`<span class="label">${this.label}${this.required ? html`<span class="req">*</span>` : nothing}</span>` : nothing}
<input
.value=${this.value ?? ""}
.type=${this.type}
?required=${this.required}
?disabled=${this.disabled}
placeholder=${this.placeholder}
step=${this.step ?? nothing}
min=${this.min ?? nothing}
max=${this.max ?? nothing}
pattern=${this.pattern ?? nothing}
@input=${this._onInput}
@change=${this._onInput}
/>
${this.helper ? html`<span class="helper">${this.helper}</span>` : nothing}
</label>
`;
}
static styles = css`
:host { display: block; }
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.label {
font-size: 12px;
color: var(--secondary-text-color, #888);
font-weight: 500;
}
.req { color: var(--error-color, #f44336); margin-left: 2px; }
input {
padding: 8px 10px;
font-size: 14px;
background: var(--secondary-background-color, rgba(0,0,0,0.06));
color: var(--primary-text-color);
border: 1px solid var(--divider-color, rgba(255,255,255,0.12));
border-radius: 6px;
font-family: inherit;
width: 100%;
box-sizing: border-box;
outline: none;
}
input:focus {
border-color: var(--primary-color);
}
input:disabled { opacity: 0.5; cursor: not-allowed; }
.helper {
font-size: 11px;
color: var(--secondary-text-color);
font-style: italic;
}
`;
}
if (!customElements.get("ms-textfield")) {
customElements.define("ms-textfield", MsTextfield);
}