From 09e4355451f60acf45c44b42403054bc61aff139 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:59:52 +0100 Subject: [PATCH] Migrate copy-textfield to input-copy (#30276) --- src/components/ha-assist-chat.ts | 38 ++--- src/components/ha-copy-textfield.ts | 110 ------------ src/components/ha-date-input.ts | 21 +-- src/components/input/ha-input-copy.ts | 134 +++++++++++++++ src/components/input/ha-input.ts | 24 +-- .../config/cloud/account/cloud-remote-pref.ts | 8 +- .../dialog-manage-cloudhook.ts | 8 +- .../config/network/ha-config-url-form.ts | 161 ++++++------------ 8 files changed, 224 insertions(+), 280 deletions(-) delete mode 100644 src/components/ha-copy-textfield.ts create mode 100644 src/components/input/ha-input-copy.ts diff --git a/src/components/ha-assist-chat.ts b/src/components/ha-assist-chat.ts index f2dcc36b91..157db76719 100644 --- a/src/components/ha-assist-chat.ts +++ b/src/components/ha-assist-chat.ts @@ -10,7 +10,6 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { haStyleScrollbar } from "../resources/styles"; import { supportsFeature } from "../common/entity/supports-feature"; import { runAssistPipeline, @@ -21,13 +20,14 @@ import { } from "../data/assist_pipeline"; import { ConversationEntityFeature } from "../data/conversation"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; +import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant } from "../types"; import { AudioRecorder } from "../util/audio-recorder"; import { documentationUrl } from "../util/documentation-url"; import "./ha-alert"; import "./ha-markdown"; -import "./ha-textfield"; -import type { HaTextField } from "./ha-textfield"; +import "./input/ha-input"; +import type { HaInput } from "./input/ha-input"; interface AssistMessage { who: string; @@ -57,7 +57,7 @@ export class HaAssistChat extends LitElement { @property({ attribute: false }) public startListening?: boolean; - @query("#message-input") private _messageInput!: HaTextField; + @query("#message-input") private _messageInput!: HaInput; @query(".message:last-child") private _lastChatMessage!: LitElement; @@ -247,14 +247,13 @@ ${JSON.stringify(toolCall.result, null, 2)}
- -
+
${this._showSendButton || !supportsSTT ? html` `}
- +
`; } @@ -329,7 +328,7 @@ ${JSON.stringify(toolCall.result, null, 2)} -
-
` - : nothing} - @click=${this._focusInput} - >
- ${this.maskedValue - ? html`` - : nothing} -
- - - ${this.label || this.hass.localize("ui.common.copy")} - - - `; - } - - private _focusInput(ev) { - const inputElement = ev.currentTarget as HaTextField; - inputElement.select(); - } - - private _toggleMasked(): void { - this._showMasked = !this._showMasked; - } - - private async _copy(): Promise { - await copyToClipboard(this.value); - showToast(this, { - message: this.hass.localize("ui.common.copied_clipboard"), - }); - } - - static styles = css` - .container { - display: flex; - align-items: center; - gap: var(--ha-space-2); - margin-top: 8px; - } - - .textfield-container { - position: relative; - flex: 1; - } - - .textfield-container ha-textfield { - display: block; - } - - .toggle-unmasked { - position: absolute; - top: 8px; - right: 8px; - inset-inline-start: initial; - inset-inline-end: 8px; - --ha-icon-button-size: 40px; - --mdc-icon-size: 20px; - color: var(--secondary-text-color); - direction: var(--direction); - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "ha-copy-textfield": HaCopyTextfield; - } -} diff --git a/src/components/ha-date-input.ts b/src/components/ha-date-input.ts index 42eb43cc3c..7de80dc0f7 100644 --- a/src/components/ha-date-input.ts +++ b/src/components/ha-date-input.ts @@ -8,8 +8,8 @@ import { fireEvent } from "../common/dom/fire_event"; import { TimeZone } from "../data/translation"; import type { HomeAssistant } from "../types"; import "./ha-svg-icon"; -import "./ha-textfield"; -import type { HaTextField } from "./ha-textfield"; +import "./input/ha-input"; +import type { HaInput } from "./input/ha-input"; const loadDatePickerDialog = () => import("./date-picker/ha-dialog-date-picker"); @@ -54,19 +54,17 @@ export class HaDateInput extends LitElement { @property({ attribute: "can-clear", type: Boolean }) public canClear = false; - @query("ha-textfield", true) private _input?: HaTextField; + @query("ha-input", true) private _input?: HaInput; public reportValidity(): boolean { return this._input?.reportValidity() ?? true; } render() { - return html` - - `; + + `; } private _openDialog() { @@ -128,9 +126,6 @@ export class HaDateInput extends LitElement { ha-svg-icon { color: var(--secondary-text-color); } - ha-textfield { - display: block; - } `; } declare global { diff --git a/src/components/input/ha-input-copy.ts b/src/components/input/ha-input-copy.ts new file mode 100644 index 0000000000..fdb697557c --- /dev/null +++ b/src/components/input/ha-input-copy.ts @@ -0,0 +1,134 @@ +import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { consume, type ContextType } from "@lit/context"; +import { copyToClipboard } from "../../common/util/copy-clipboard"; +import { localizeContext } from "../../data/context"; +import { showToast } from "../../util/toast"; +import "../ha-button"; +import "../ha-icon-button"; +import "../ha-svg-icon"; +import "./ha-input"; +import type { HaInput, InputType } from "./ha-input"; + +@customElement("ha-input-copy") +export class HaInputCopy extends LitElement { + @property({ attribute: "value" }) public value!: string; + + @property({ attribute: "masked-value" }) public maskedValue?: string; + + @property({ attribute: "label" }) public label?: string; + + @property({ type: Boolean }) public readonly = false; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean, attribute: "masked-toggle" }) public maskedToggle = + false; + + @property() public type: InputType = "text"; + + @property() + public placeholder = ""; + + @property({ attribute: "validation-message" }) + public validationMessage?: string; + + @property({ type: Boolean, attribute: "auto-validate" }) public autoValidate = + false; + + @state() + @consume({ context: localizeContext, subscribe: true }) + private localize!: ContextType; + + @state() private _showMasked = true; + + @query("ha-input", true) private _inputElement?: HaInput; + + public reportValidity(): boolean { + return this._inputElement?.reportValidity() ?? false; + } + + public render() { + return html` +
+
+ + ${this.maskedToggle && this.maskedValue + ? html`` + : nothing} + +
+ + + ${this.label || this.localize("ui.common.copy")} + +
+ `; + } + + private _focusInput(ev: Event) { + const inputElement = ev.currentTarget as HaInput; + inputElement.select(); + } + + private _toggleMasked(): void { + this._showMasked = !this._showMasked; + } + + private async _copy(): Promise { + await copyToClipboard(this.value); + showToast(this, { + message: this.localize("ui.common.copied_clipboard"), + }); + } + + static styles = css` + .container { + display: flex; + align-items: center; + gap: var(--ha-space-2); + margin-top: 8px; + } + + .textfield-container { + position: relative; + flex: 1; + } + + .toggle-unmasked { + --ha-icon-button-size: 40px; + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + } + + ha-button { + margin-bottom: var(--ha-space-2); + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-input-copy": HaInputCopy; + } +} diff --git a/src/components/input/ha-input.ts b/src/components/input/ha-input.ts index 2f8c958cef..a9228bd2bc 100644 --- a/src/components/input/ha-input.ts +++ b/src/components/input/ha-input.ts @@ -12,20 +12,22 @@ import { stopPropagation } from "../../common/dom/stop_propagation"; import "../ha-icon-button"; import "../ha-tooltip"; +export type InputType = + | "date" + | "datetime-local" + | "email" + | "number" + | "password" + | "search" + | "tel" + | "text" + | "time" + | "url"; + @customElement("ha-input") export class HaInput extends LitElement { @property({ reflect: true }) - public type: - | "date" - | "datetime-local" - | "email" - | "number" - | "password" - | "search" - | "tel" - | "text" - | "time" - | "url" = "text"; + public type: InputType = "text"; @property() public value?: string; diff --git a/src/panels/config/cloud/account/cloud-remote-pref.ts b/src/panels/config/cloud/account/cloud-remote-pref.ts index 303fec6d73..0398e45433 100644 --- a/src/panels/config/cloud/account/cloud-remote-pref.ts +++ b/src/panels/config/cloud/account/cloud-remote-pref.ts @@ -10,8 +10,8 @@ import "../../../../components/ha-md-list-item"; import "../../../../components/ha-switch"; import { formatDate } from "../../../../common/datetime/format_date"; -import "../../../../components/ha-copy-textfield"; import type { HaSwitch } from "../../../../components/ha-switch"; +import "../../../../components/input/ha-input-copy"; import type { CloudStatusLoggedIn } from "../../../../data/cloud"; import { connectCloudRemote, @@ -130,12 +130,12 @@ export class CloudRemotePref extends LitElement {

`} - + > - + > diff --git a/src/panels/config/network/ha-config-url-form.ts b/src/panels/config/network/ha-config-url-form.ts index 5872db5453..d770f0df29 100644 --- a/src/panels/config/network/ha-config-url-form.ts +++ b/src/panels/config/network/ha-config-url-form.ts @@ -1,25 +1,24 @@ -import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js"; import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isIPAddress } from "../../../common/string/is_ip_address"; -import { copyToClipboard } from "../../../common/util/copy-clipboard"; import "../../../components/ha-alert"; import "../../../components/ha-button"; import "../../../components/ha-card"; import "../../../components/ha-md-list-item"; import "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch"; -import "../../../components/input/ha-input"; +import "../../../components/ha-textfield"; import type { HaInput } from "../../../components/input/ha-input"; +import "../../../components/input/ha-input-copy"; +import type { HaInputCopy } from "../../../components/input/ha-input-copy"; import type { CloudStatus } from "../../../data/cloud"; import { fetchCloudStatus } from "../../../data/cloud"; import { saveCoreConfig } from "../../../data/core"; import { getNetworkUrls, type NetworkUrls } from "../../../data/network"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import type { HomeAssistant, ValueChangedEvent } from "../../../types"; -import { showToast } from "../../../util/toast"; +import type { HomeAssistant } from "../../../types"; import { obfuscateUrl } from "../../../util/url"; @customElement("ha-config-url-form") @@ -42,16 +41,12 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) { @state() private _showCustomInternalUrl = false; - @state() private _unmaskedExternalUrl = false; - - @state() private _unmaskedInternalUrl = false; - @state() private _cloudChecked = false; - @query('[name="external_url"]') + @query('[data-name="external_url"]') private _externalUrlField?: HaInput; - @query('[name="internal_url"]') + @query('[data-name="internal_url"]') private _internalUrlField?: HaInput; protected hassSubscribe() { @@ -76,6 +71,7 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) { const internalUrl = this._showCustomInternalUrl ? this._internal_url : this._urls?.internal || ""; + const externalUrl = this._showCustomExternalUrl ? this._external_url : (this._cloudChecked ? this._urls?.cloud : this._urls?.external) || ""; @@ -148,45 +144,24 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) { ` : nothing}
-
- - ${!this._showCustomExternalUrl || !canEdit - ? html` - - ` - : nothing} - -
- - - ${this.hass.localize("ui.panel.config.common.copy_link")} - +
${hasCloud || !isComponentLoaded(this.hass, "cloud") ? nothing @@ -264,47 +239,26 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
-
- - ${!this._showCustomInternalUrl || !canEdit - ? html` - - ` - : nothing} - -
- - - ${this.hass.localize("ui.panel.config.common.copy_link")} - +
${ // If the user has configured a cert, show an error if @@ -367,25 +321,10 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) { this._showCustomInternalUrl = !(ev.currentTarget as HaSwitch).checked; } - private _toggleUnmaskedInternalUrl() { - this._unmaskedInternalUrl = !this._unmaskedInternalUrl; - } - - private _toggleUnmaskedExternalUrl() { - this._unmaskedExternalUrl = !this._unmaskedExternalUrl; - } - - private async _copyURL(ev) { - const url = ev.currentTarget.url; - await copyToClipboard(url); - showToast(this, { - message: this.hass.localize("ui.common.copied_clipboard"), - }); - } - - private _handleChange(ev: ValueChangedEvent) { - const target = ev.currentTarget as HaInput; - this[`_${target.name}`] = target.value || ""; + private _handleChange(ev: InputEvent) { + const target = ev.currentTarget as HaInputCopy; + const input = ev.composedPath()[0] as HaInput; + this[`_${target.dataset.name}`] = input.value || ""; } private async _save() { @@ -471,12 +410,10 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) { gap: var(--ha-space-2); margin-top: 8px; } - .textfield-container { + + ha-input-copy { flex: 1; } - .textfield-container ha-input { - display: block; - } ha-md-list-item { --md-list-item-top-space: 0;