diff --git a/hassio/src/components/supervisor-backup-content.ts b/hassio/src/components/supervisor-backup-content.ts
index ddbde0f4f3..31d2f3b788 100644
--- a/hassio/src/components/supervisor-backup-content.ts
+++ b/hassio/src/components/supervisor-backup-content.ts
@@ -15,6 +15,7 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
+import "../../../src/components/ha-password-field";
import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio";
import {
@@ -261,23 +262,21 @@ export class SupervisorBackupContent extends LitElement {
: ""}
${this.backupHasPassword
? html`
-
-
+
${!this.backup
- ? html`
- `
+ `
: ""}
`
: ""}
diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts
index 66b381e0c0..4def6de2a1 100644
--- a/hassio/src/dialogs/network/dialog-hassio-network.ts
+++ b/hassio/src/dialogs/network/dialog-hassio-network.ts
@@ -13,10 +13,12 @@ import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield";
-import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
+import "../../../../src/components/ha-password-field";
import "../../../../src/components/ha-radio";
+import "../../../../src/components/ha-textfield";
+import type { HaTextField } from "../../../../src/components/ha-textfield";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
AccessPoints,
@@ -34,7 +36,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
-import type { HaTextField } from "../../../../src/components/ha-textfield";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -246,9 +247,8 @@ export class DialogHassioNetwork
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
-
-
+
`
: ""}
`
diff --git a/src/components/ha-password-field.ts b/src/components/ha-password-field.ts
new file mode 100644
index 0000000000..8612703053
--- /dev/null
+++ b/src/components/ha-password-field.ts
@@ -0,0 +1,160 @@
+import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
+import { mdiEye, mdiEyeOff } from "@mdi/js";
+import { LitElement, css, html } from "lit";
+import { customElement, eventOptions, property, state } from "lit/decorators";
+import { HomeAssistant } from "../types";
+import "./ha-icon-button";
+import "./ha-textfield";
+
+@customElement("ha-password-field")
+export class HaPasswordField extends LitElement {
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @property({ type: Boolean }) public invalid?: boolean;
+
+ @property({ attribute: "error-message" }) public errorMessage?: string;
+
+ @property({ type: Boolean }) public icon = false;
+
+ @property({ type: Boolean }) public iconTrailing = false;
+
+ @property() public autocomplete?: string;
+
+ @property() public autocorrect?: string;
+
+ @property({ attribute: "input-spellcheck" })
+ public inputSpellcheck?: string;
+
+ @property({ type: String }) value = "";
+
+ @property({ type: String }) placeholder = "";
+
+ @property({ type: String }) label = "";
+
+ @property({ type: Boolean, reflect: true }) disabled = false;
+
+ @property({ type: Boolean }) required = false;
+
+ @property({ type: Number }) minLength = -1;
+
+ @property({ type: Number }) maxLength = -1;
+
+ @property({ type: Boolean, reflect: true }) outlined = false;
+
+ @property({ type: String }) helper = "";
+
+ @property({ type: Boolean }) validateOnInitialRender = false;
+
+ @property({ type: String }) validationMessage = "";
+
+ @property({ type: Boolean }) autoValidate = false;
+
+ @property({ type: String }) pattern = "";
+
+ @property({ type: Number }) size: number | null = null;
+
+ @property({ type: Boolean }) helperPersistent = false;
+
+ @property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
+ false;
+
+ @property({ type: Boolean }) endAligned = false;
+
+ @property({ type: String }) prefix = "";
+
+ @property({ type: String }) suffix = "";
+
+ @property({ type: String }) name = "";
+
+ @property({ type: String, attribute: "input-mode" })
+ inputMode!: string;
+
+ @property({ type: Boolean }) readOnly = false;
+
+ @property({ type: String }) autocapitalize = "";
+
+ @state() private _unmaskedPassword = false;
+
+ protected render() {
+ return html``}
+ @input=${this._handleInputChange}
+ >
+ `;
+ }
+
+ private _toggleUnmaskedPassword(): void {
+ this._unmaskedPassword = !this._unmaskedPassword;
+ }
+
+ @eventOptions({ passive: true })
+ private _handleInputChange(ev) {
+ this.value = ev.target.value;
+ }
+
+ static styles = css`
+ :host {
+ display: block;
+ position: relative;
+ }
+ ha-textfield {
+ width: 100%;
+ }
+ ha-icon-button {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ inset-inline-start: initial;
+ inset-inline-end: 8px;
+ --mdc-icon-button-size: 40px;
+ --mdc-icon-size: 20px;
+ color: var(--secondary-text-color);
+ direction: var(--direction);
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-password-field": HaPasswordField;
+ }
+}
diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts
index bf45e85a1f..4b2df4c09f 100644
--- a/src/components/ha-textfield.ts
+++ b/src/components/ha-textfield.ts
@@ -6,7 +6,7 @@ import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-textfield")
export class HaTextField extends TextFieldBase {
- @property({ type: Boolean }) public invalid = false;
+ @property({ type: Boolean }) public invalid?: boolean;
@property({ attribute: "error-message" }) public errorMessage?: string;
@@ -28,14 +28,24 @@ export class HaTextField extends TextFieldBase {
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
- (changedProperties.has("invalid") &&
- (this.invalid || changedProperties.get("invalid") !== undefined)) ||
+ changedProperties.has("invalid") ||
changedProperties.has("errorMessage")
) {
this.setCustomValidity(
- this.invalid ? this.errorMessage || "Invalid" : ""
+ this.invalid
+ ? this.errorMessage || this.validationMessage || "Invalid"
+ : ""
);
- this.reportValidity();
+ if (
+ this.invalid ||
+ this.validateOnInitialRender ||
+ (changedProperties.has("invalid") &&
+ changedProperties.get("invalid") !== undefined)
+ ) {
+ // Only report validity if the field is invalid or the invalid state has changed from
+ // true to false to prevent setting empty required fields to invalid on first render
+ this.reportValidity();
+ }
}
if (changedProperties.has("autocomplete")) {
if (this.autocomplete) {
diff --git a/src/panels/config/application_credentials/dialog-add-application-credential.ts b/src/panels/config/application_credentials/dialog-add-application-credential.ts
index e30e494b47..d5e9ca4c84 100644
--- a/src/panels/config/application_credentials/dialog-add-application-credential.ts
+++ b/src/panels/config/application_credentials/dialog-add-application-credential.ts
@@ -5,12 +5,13 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
+import "../../../components/ha-button";
import "../../../components/ha-circular-progress";
import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-markdown";
+import "../../../components/ha-password-field";
import "../../../components/ha-textfield";
-import "../../../components/ha-button";
import {
ApplicationCredential,
ApplicationCredentialsConfig,
@@ -208,11 +209,10 @@ export class DialogAddApplicationCredential extends LitElement {
)}
helperPersistent
>
-
+ >
${this._loading
? html`
diff --git a/src/panels/config/cloud/login/cloud-login.ts b/src/panels/config/cloud/login/cloud-login.ts
index d31f7c7250..c617dfd2b2 100644
--- a/src/panels/config/cloud/login/cloud-login.ts
+++ b/src/panels/config/cloud/login/cloud-login.ts
@@ -22,6 +22,7 @@ import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
+import "../../../../components/ha-password-field";
@customElement("cloud-login")
export class CloudLogin extends LitElement {
@@ -142,14 +143,13 @@ export class CloudLogin extends LitElement {
"ui.panel.config.cloud.login.email_error_msg"
)}
>
-
+ >
-
+ >
-
+
`
: ""}
`
diff --git a/src/panels/config/users/dialog-add-user.ts b/src/panels/config/users/dialog-add-user.ts
index 640f1aa104..7e7e9e70cd 100644
--- a/src/panels/config/users/dialog-add-user.ts
+++ b/src/panels/config/users/dialog-add-user.ts
@@ -29,6 +29,7 @@ import {
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant, ValueChangedEvent } from "../../../types";
import { AddUserDialogParams } from "./show-dialog-add-user";
+import "../../../components/ha-password-field";
@customElement("dialog-add-user")
export class DialogAddUser extends LitElement {
@@ -87,6 +88,7 @@ export class DialogAddUser extends LitElement {
if (!this._params) {
return nothing;
}
+
return html`
-
+ >
-
+ >
${this.hass.localize(
@@ -311,7 +311,8 @@ export class DialogAddUser extends LitElement {
display: flex;
padding: 8px 0;
}
- ha-textfield {
+ ha-textfield,
+ ha-password-field {
display: block;
margin-bottom: 8px;
}
diff --git a/src/panels/profile/ha-change-password-card.ts b/src/panels/profile/ha-change-password-card.ts
index a09ac13a3e..063ca339d8 100644
--- a/src/panels/profile/ha-change-password-card.ts
+++ b/src/panels/profile/ha-change-password-card.ts
@@ -11,6 +11,7 @@ import { customElement, property, state } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-circular-progress";
import "../../components/ha-textfield";
+import "../../components/ha-password-field";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../../components/ha-alert";
@@ -52,47 +53,44 @@ class HaChangePasswordCard extends LitElement {
? html`${this._statusMsg}`
: ""}
-
+ >
${this._currentPassword
- ? html`
-
+ `
+ >`
: ""}