1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 08:33:31 +01:00

Migrate copy-textfield to input-copy (#30276)

This commit is contained in:
Wendelin
2026-03-23 11:59:52 +01:00
committed by GitHub
parent 7ee76538ae
commit 09e4355451
8 changed files with 224 additions and 280 deletions

View File

@@ -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)}</pre
)}
</div>
<div class="input" slot="primaryAction">
<ha-textfield
<ha-input
id="message-input"
@keyup=${this._handleKeyUp}
@input=${this._handleInput}
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
.iconTrailing=${true}
>
<div slot="trailingIcon">
<div slot="end">
${this._showSendButton || !supportsSTT
? html`
<ha-icon-button
@@ -299,7 +298,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
</div>
`}
</div>
</ha-textfield>
</ha-input>
</div>
`;
}
@@ -329,7 +328,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
}
private _handleKeyUp(ev: KeyboardEvent) {
const input = ev.target as HaTextField;
const input = ev.target as HaInput;
if (!this._processing && ev.key === "Enter" && input.value) {
this._processText(input.value);
input.value = "";
@@ -338,7 +337,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
}
private _handleInput(ev: InputEvent) {
const value = (ev.target as HaTextField).value;
const value = (ev.target as HaInput).value;
if (value && !this._showSendButton) {
this._showSendButton = true;
} else if (!value && this._showSendButton) {
@@ -728,9 +727,10 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
ha-alert {
margin-bottom: var(--ha-space-2);
}
ha-textfield {
display: block;
#message-input::part(wa-base) {
padding-right: var(--ha-space-1);
}
.messages {
flex: 1 1 400px;
display: block;
@@ -944,20 +944,6 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
}
}
.listening-icon {
position: relative;
color: var(--secondary-text-color);
margin-right: -24px;
margin-inline-end: -24px;
margin-inline-start: initial;
direction: var(--direction);
transform: scaleX(var(--scale-direction));
}
.listening-icon[active] {
color: var(--primary-color);
}
.unsupported {
color: var(--error-color);
position: absolute;

View File

@@ -1,110 +0,0 @@
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { copyToClipboard } from "../common/util/copy-clipboard";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import "./ha-button";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-copy-textfield")
export class HaCopyTextfield extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "value" }) public value!: string;
@property({ attribute: "masked-value" }) public maskedValue?: string;
@property({ attribute: "label" }) public label?: string;
@state() private _showMasked = true;
public render() {
return html`
<div class="container">
<div class="textfield-container">
<ha-textfield
.value=${this._showMasked && this.maskedValue
? this.maskedValue
: this.value}
readonly
.suffix=${this.maskedValue
? html`<div style="width: 24px"></div>`
: nothing}
@click=${this._focusInput}
></ha-textfield>
${this.maskedValue
? html`<ha-icon-button
class="toggle-unmasked"
.label=${this.hass.localize(
`ui.common.${this._showMasked ? "show" : "hide"}`
)}
@click=${this._toggleMasked}
.path=${this._showMasked ? mdiEye : mdiEyeOff}
></ha-icon-button>`
: nothing}
</div>
<ha-button @click=${this._copy} appearance="plain" size="small">
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this.label || this.hass.localize("ui.common.copy")}
</ha-button>
</div>
`;
}
private _focusInput(ev) {
const inputElement = ev.currentTarget as HaTextField;
inputElement.select();
}
private _toggleMasked(): void {
this._showMasked = !this._showMasked;
}
private async _copy(): Promise<void> {
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;
}
}

View File

@@ -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`<ha-textfield
.label=${this.label}
.helper=${this.helper}
return html`<ha-input
.label=${this.label ?? ""}
.hint=${this.helper ?? ""}
.disabled=${this.disabled}
iconTrailing
helperPersistent
readonly
@click=${this._openDialog}
@keydown=${this._keyDown}
@@ -82,8 +80,8 @@ export class HaDateInput extends LitElement {
: ""}
.required=${this.required}
>
<ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon>
</ha-textfield>`;
<ha-svg-icon slot="end" .path=${mdiCalendar}></ha-svg-icon>
</ha-input>`;
}
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 {

View File

@@ -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<typeof localizeContext>;
@state() private _showMasked = true;
@query("ha-input", true) private _inputElement?: HaInput;
public reportValidity(): boolean {
return this._inputElement?.reportValidity() ?? false;
}
public render() {
return html`
<div class="container">
<div class="textfield-container">
<ha-input
.type=${this.type}
.value=${this._showMasked && this.maskedValue
? this.maskedValue
: this.value}
.readonly=${this.readonly}
.disabled=${this.disabled}
@click=${this._focusInput}
.placeholder=${this.placeholder}
.autoValidate=${this.autoValidate}
.validationMessage=${this.validationMessage}
>
${this.maskedToggle && this.maskedValue
? html`<ha-icon-button
slot="end"
class="toggle-unmasked"
.label=${this.localize(
`ui.common.${this._showMasked ? "show" : "hide"}`
)}
@click=${this._toggleMasked}
.path=${this._showMasked ? mdiEye : mdiEyeOff}
></ha-icon-button>`
: nothing}
</ha-input>
</div>
<ha-button @click=${this._copy} appearance="plain" size="small">
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this.label || this.localize("ui.common.copy")}
</ha-button>
</div>
`;
}
private _focusInput(ev: Event) {
const inputElement = ev.currentTarget as HaInput;
inputElement.select();
}
private _toggleMasked(): void {
this._showMasked = !this._showMasked;
}
private async _copy(): Promise<void> {
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;
}
}

View File

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

View File

@@ -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 {
</p>
`}
<ha-copy-textfield
.hass=${this.hass}
<ha-input-copy
readonly
.value=${`https://${remote_domain}`}
.maskedValue=${obfuscateUrl(`https://${remote_domain}`)}
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
></ha-copy-textfield>
></ha-input-copy>
<ha-expansion-panel
outlined

View File

@@ -11,8 +11,8 @@ import { documentationUrl } from "../../../../util/documentation-url";
import type { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
import "../../../../components/ha-button";
import "../../../../components/ha-copy-textfield";
import "../../../../components/ha-dialog";
import "../../../../components/input/ha-input-copy";
@customElement("dialog-manage-cloudhook")
export class DialogManageCloudhook extends LitElement {
@@ -79,11 +79,11 @@ export class DialogManageCloudhook extends LitElement {
`}
</p>
<ha-copy-textfield
.hass=${this.hass}
<ha-input-copy
readonly
.value=${cloudhook.cloudhook_url}
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
></ha-copy-textfield>
></ha-input-copy>
</div>
<ha-dialog-footer slot="footer">

View File

@@ -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}
<div class="url-container">
<div class="textfield-container">
<ha-input
name="external_url"
type="url"
auto-validate
.validationMessage=${this.hass.localize(
"ui.panel.config.url.invalid_url"
)}
placeholder="https://example.duckdns.org:8123"
.value=${this._unmaskedExternalUrl ||
(this._showCustomExternalUrl && canEdit)
? externalUrl
: obfuscateUrl(externalUrl)}
@change=${this._handleChange}
.disabled=${disabled || !this._showCustomExternalUrl}
>
${!this._showCustomExternalUrl || !canEdit
? html`
<ha-icon-button
slot="end"
.label=${this.hass.localize(
`ui.panel.config.common.${this._unmaskedExternalUrl ? "hide" : "show"}_url`
)}
@click=${this._toggleUnmaskedExternalUrl}
.path=${this._unmaskedExternalUrl ? mdiEyeOff : mdiEye}
></ha-icon-button>
`
: nothing}
</ha-input>
</div>
<ha-button
size="small"
appearance="plain"
.url=${externalUrl}
@click=${this._copyURL}
<ha-input-copy
auto-validate
.validationMessage=${this.hass.localize(
"ui.panel.config.url.invalid_url"
)}
data-name="external_url"
type="url"
.maskedToggle=${!(this._showCustomExternalUrl && canEdit)}
placeholder="https://example.duckdns.org:8123"
.value=${externalUrl}
.maskedValue=${this._showCustomExternalUrl && canEdit
? undefined
: obfuscateUrl(externalUrl)}
@change=${this._handleChange}
.readonly=${!this._showCustomExternalUrl}
.disabled=${disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this.hass.localize("ui.panel.config.common.copy_link")}
</ha-button>
</ha-input-copy>
</div>
${hasCloud || !isComponentLoaded(this.hass, "cloud")
? nothing
@@ -264,47 +239,26 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
</ha-md-list-item>
<div class="url-container">
<div class="textfield-container">
<ha-input
name="internal_url"
type="url"
auto-validate
.validationMessage=${this.hass.localize(
"ui.panel.config.url.invalid_url"
)}
placeholder=${this.hass.localize(
"ui.panel.config.url.internal_url_placeholder"
)}
.value=${this._unmaskedInternalUrl ||
(this._showCustomInternalUrl && canEdit)
? internalUrl
: obfuscateUrl(internalUrl)}
@change=${this._handleChange}
.disabled=${disabled || !this._showCustomInternalUrl}
>
${!this._showCustomInternalUrl || !canEdit
? html`
<ha-icon-button
slot="end"
.label=${this.hass.localize(
`ui.panel.config.common.${this._unmaskedInternalUrl ? "hide" : "show"}_url`
)}
@click=${this._toggleUnmaskedInternalUrl}
.path=${this._unmaskedInternalUrl ? mdiEyeOff : mdiEye}
></ha-icon-button>
`
: nothing}
</ha-input>
</div>
<ha-button
size="small"
appearance="plain"
.url=${internalUrl}
@click=${this._copyURL}
<ha-input-copy
auto-validate
.validationMessage=${this.hass.localize(
"ui.panel.config.url.invalid_url"
)}
data-name="internal_url"
.maskedToggle=${!(this._showCustomInternalUrl && canEdit)}
type="url"
placeholder=${this.hass.localize(
"ui.panel.config.url.internal_url_placeholder"
)}
.value=${internalUrl}
.maskedValue=${this._showCustomInternalUrl && canEdit
? undefined
: obfuscateUrl(internalUrl)}
@change=${this._handleChange}
.readonly=${!this._showCustomInternalUrl}
.disabled=${disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this.hass.localize("ui.panel.config.common.copy_link")}
</ha-button>
</ha-input-copy>
</div>
${
// 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<string>) {
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;