From 4e8b58cd6c14f8791a71a68b8a3cac2e761337ad Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 27 Sep 2024 12:34:28 +0200 Subject: [PATCH] Add password field element (#22121) * Add password field element * Update ha-password-field.ts --- .../components/supervisor-backup-content.ts | 11 +- .../dialogs/network/dialog-hassio-network.ts | 10 +- src/components/ha-password-field.ts | 160 ++++++++++++++++++ src/components/ha-textfield.ts | 20 ++- .../dialog-add-application-credential.ts | 8 +- src/panels/config/cloud/login/cloud-login.ts | 6 +- .../config/cloud/register/cloud-register.ts | 6 +- .../config/network/supervisor-network.ts | 14 +- src/panels/config/users/dialog-add-user.ts | 19 ++- src/panels/profile/ha-change-password-card.ts | 16 +- 10 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 src/components/ha-password-field.ts 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` - + ` + >` : ""}