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.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;