From 3d24f0a51eac2170766baf08c0b0edf981445443 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Thu, 30 Oct 2025 11:56:15 +0100 Subject: [PATCH] Implementing an inline toggle button to show/hide password. (#20611) * Implimented an inline toggle button to show/hide your password, also changed the css to accommodate these changes * Cleaned the css Added the svg's to their own const for easy reuse Added localization for the arialabel on the button Seperated the createFormLayoutItem so there is a seperate for the password input Moved all the conditional logic in the onclick event to fit inside one if/else statement * Removed old logic that added a 100ms timeout that would sometimes be enough for localization to load, and replaced it with a function. The function will try and resolve the promise by checking if the localize.terms methods returns a changed value, if not then it retries every 50ms or untill it hits a max retry of 40/2 seconds. * Re adding the hide for -ms-reveal to support Microsoft Edge browsers * Removed a console.log * Alligned the button behavior so it fits better with what we have in the uui libary. Now the button is always visible instead of appearing on hover or when in focus * Update src/Umbraco.Web.UI.Login/src/auth.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Umbraco.Web.UI.Login/src/auth.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @iOvergaard * Apply suggestion from @iOvergaard * Adding the requested changes via my own fork (#20664) Changed the logic for waitForLocallization Added the svg's as files that are imported instead of having the raw svg in the code --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Umbraco.Web.UI.Login/public/closedEye.svg | 6 + src/Umbraco.Web.UI.Login/public/openEye.svg | 4 + src/Umbraco.Web.UI.Login/src/auth-styles.css | 106 +++++++++++----- src/Umbraco.Web.UI.Login/src/auth.element.ts | 115 ++++++++++++++++-- .../src/localization/lang/da.ts | 2 + .../src/localization/lang/en.ts | 2 + 6 files changed, 195 insertions(+), 40 deletions(-) create mode 100644 src/Umbraco.Web.UI.Login/public/closedEye.svg create mode 100644 src/Umbraco.Web.UI.Login/public/openEye.svg diff --git a/src/Umbraco.Web.UI.Login/public/closedEye.svg b/src/Umbraco.Web.UI.Login/public/closedEye.svg new file mode 100644 index 0000000000..3a29b42974 --- /dev/null +++ b/src/Umbraco.Web.UI.Login/public/closedEye.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Login/public/openEye.svg b/src/Umbraco.Web.UI.Login/public/openEye.svg new file mode 100644 index 0000000000..5f28e8e03f --- /dev/null +++ b/src/Umbraco.Web.UI.Login/public/openEye.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Login/src/auth-styles.css b/src/Umbraco.Web.UI.Login/src/auth-styles.css index f63dfa92e6..9ea9111fc8 100644 --- a/src/Umbraco.Web.UI.Login/src/auth-styles.css +++ b/src/Umbraco.Web.UI.Login/src/auth-styles.css @@ -1,44 +1,84 @@ -#umb-login-form input { - width: 100%; - height: var(--input-height); - box-sizing: border-box; - display: block; - border: 1px solid var(--uui-color-border); - border-radius: var(--uui-border-radius); - background-color: var(--uui-color-surface); - padding: var(--uui-size-1, 3px) var(--uui-size-space-4, 9px); +#umb-login-form #username-input { + width: 100%; + height: var(--input-height); + box-sizing: border-box; + display: block; + border: 1px solid var(--uui-color-border); + border-radius: var(--uui-border-radius); + background-color: var(--uui-color-surface); + padding: var(--uui-size-1, 3px) var(--uui-size-space-4, 9px); } #umb-login-form uui-form-layout-item { - margin-top: var(--uui-size-space-4); - margin-bottom: var(--uui-size-space-4); + margin-top: var(--uui-size-space-4); + margin-bottom: var(--uui-size-space-4); } -#umb-login-form input:focus-within { - border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1)); - outline: calc(2px * var(--uui-show-focus-outline, 1)) solid var(--uui-color-focus); +#umb-login-form #username-input:focus-within { + border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1)); + outline: calc(2px * var(--uui-show-focus-outline, 1)) solid var(--uui-color-focus); } -#umb-login-form input:hover:not(:focus-within) { - border-color: var(--uui-input-border-color-hover, var(--uui-color-border-standalone, #c2c2c2)); +#umb-login-form #username-input:hover:not(:focus-within) { + border-color: var(--uui-input-border-color-hover, var(--uui-color-border-standalone, #c2c2c2)); +} + +#umb-login-form #password-input-span button { + color: var(--uui-color-default-standalone); + display: inline-flex; + justify-content: center; + align-items: center; + vertical-align: middle; + min-width: 24px; + min-height: 24px; + border-color: transparent; + background-color: transparent; + padding: 0; + transition-property: color; + transition-duration: 0.1s; + transition-timing-function: linear; +} + +#umb-login-form #password-input-span button:hover { + color: var(--uui-color-default-emphasis); + cursor: pointer; +} + +#umb-login-form #password-input-span { + display: inline-flex; + width: 100%; + align-items: center; + flex-wrap: nowrap; + position: relative; + vertical-align: middle; + column-gap: 0; + height: var(--input-height); + box-sizing: border-box; + border: 1px solid var(--uui-color-border); + border-radius: var(--uui-border-radius); + background-color: var(--uui-color-surface); + padding: var(--uui-size-1, 3px) var(--uui-size-space-4, 9px); +} + +#umb-login-form #password-input-span input { + flex-grow: 1; + align-self: stretch; + min-width: 0; + display: block; + border-style: none; + padding: 0; + outline-style: none; +} + +#umb-login-form #password-input-span:focus-within { + border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1)); + outline: calc(2px * var(--uui-show-focus-outline, 1)) solid var(--uui-color-focus); +} + +#umb-login-form #password-input-span:hover:not(:focus-within) { + border-color: var(--uui-input-border-color-hover, var(--uui-color-border-standalone, #c2c2c2)); } #umb-login-form input::-ms-reveal { - display: none; -} - -#umb-login-form input span { - position: absolute; - right: 1px; - top: 50%; - transform: translateY(-50%); - z-index: 100; -} - -#umb-login-form input span svg { - background-color: white; - display: block; - padding: .2em; - width: 1.3em; - height: 1.3em; + display: none; } diff --git a/src/Umbraco.Web.UI.Login/src/auth.element.ts b/src/Umbraco.Web.UI.Login/src/auth.element.ts index d2fb5e1bc0..d574ef4601 100644 --- a/src/Umbraco.Web.UI.Login/src/auth.element.ts +++ b/src/Umbraco.Web.UI.Login/src/auth.element.ts @@ -9,6 +9,10 @@ import { UmbSlimBackofficeController } from './controllers'; // We import the authStyles here so that we can inline it in the shadow DOM that is created outside of the UmbAuthElement. import authStyles from './auth-styles.css?inline'; +// Import the SVG files +import openEyeSVG from '../public/openEye.svg?raw'; +import closedEyeSVG from '../public/closedEye.svg?raw'; + // Import the main bundle import { extensions } from './umbraco-package.js'; @@ -42,14 +46,73 @@ const createLabel = (opts: { forId: string; localizeAlias: string; localizeFallb return label; }; +const createShowPasswordToggleButton = (opts: { + id: string; + name: string; + ariaLabelShowPassword: string; + ariaLabelHidePassword: string; +}) => { + const button = document.createElement('button'); + button.id = opts.id; + button.ariaLabel = opts.ariaLabelShowPassword; + button.name = opts.name; + button.type = 'button'; + + button.innerHTML = openEyeSVG; + + button.onclick = () => { + const passwordInput = document.getElementById('password-input') as HTMLInputElement; + + if (passwordInput.type === 'password') { + passwordInput.type = 'text'; + button.ariaLabel = opts.ariaLabelHidePassword; + button.innerHTML = closedEyeSVG; + } else { + passwordInput.type = 'password'; + button.ariaLabel = opts.ariaLabelShowPassword; + button.innerHTML = openEyeSVG; + } + + passwordInput.focus(); + }; + + return button; +}; + +const createShowPasswordToggleItem = (button: HTMLButtonElement) => { + const span = document.createElement('span'); + span.id = 'password-show-toggle-span'; + span.appendChild(button); + + return span; +}; + const createFormLayoutItem = (label: HTMLLabelElement, input: HTMLInputElement) => { const formLayoutItem = document.createElement('uui-form-layout-item') as UUIFormLayoutItemElement; + formLayoutItem.appendChild(label); formLayoutItem.appendChild(input); return formLayoutItem; }; +const createFormLayoutPasswordItem = ( + label: HTMLLabelElement, + input: HTMLInputElement, + showPasswordToggle: HTMLSpanElement +) => { + const formLayoutItem = document.createElement('uui-form-layout-item') as UUIFormLayoutItemElement; + + formLayoutItem.appendChild(label); + const span = document.createElement('span'); + span.id = 'password-input-span'; + span.appendChild(input); + span.appendChild(showPasswordToggle); + formLayoutItem.appendChild(span); + + return formLayoutItem; +}; + const createForm = (elements: HTMLElement[]) => { const styles = document.createElement('style'); styles.innerHTML = authStyles; @@ -112,6 +175,8 @@ export default class UmbAuthElement extends UmbLitElement { _passwordInput?: HTMLInputElement; _usernameLabel?: HTMLLabelElement; _passwordLabel?: HTMLLabelElement; + _passwordShowPasswordToggleItem?: HTMLSpanElement; + _passwordShowPasswordToggleButton?: HTMLButtonElement; #authContext = new UmbAuthContext(this, UMB_AUTH_CONTEXT); @@ -133,11 +198,35 @@ export default class UmbAuthElement extends UmbLitElement { // Register the main package for Umbraco.Auth umbExtensionsRegistry.registerMany(extensions); - setTimeout(() => { - requestAnimationFrame(() => { - this.#initializeForm(); - }); - }, 100); + // Wait for localization to be ready before loading the form + await this.#waitForLocalization(); + + this.#initializeForm(); + } + + async #waitForLocalization(): Promise { + return new Promise((resolve, reject) => { + let retryCount = 0; + // Retries 40 times with a 50ms interval = 2 seconds + const maxRetries = 40; + + // We check periodically until it is available or we reach the max retries + const checkInterval = setInterval(() => { + // If we reach max retries, we give up and reject the promise + if (retryCount > maxRetries) { + clearInterval(checkInterval); + reject('Localization not available'); + return; + } + // Check if localization is available + if (this.localize.term('auth_showPassword') !== 'auth_showPassword') { + clearInterval(checkInterval); + resolve(); + return; + } + retryCount++; + }, 50); + }); } disconnectedCallback() { @@ -148,6 +237,8 @@ export default class UmbAuthElement extends UmbLitElement { this._usernameInput?.remove(); this._passwordLabel?.remove(); this._passwordInput?.remove(); + this._passwordShowPasswordToggleItem?.remove(); + this._passwordShowPasswordToggleButton?.remove(); } /** @@ -173,6 +264,13 @@ export default class UmbAuthElement extends UmbLitElement { autocomplete: 'current-password', inputmode: '', }); + this._passwordShowPasswordToggleButton = createShowPasswordToggleButton({ + id: 'password-show-toggle', + name: 'password-show-toggle', + ariaLabelShowPassword: this.localize.term('auth_showPassword'), + ariaLabelHidePassword: this.localize.term('auth_hidePassword'), + }); + this._passwordShowPasswordToggleItem = createShowPasswordToggleItem(this._passwordShowPasswordToggleButton); this._usernameLabel = createLabel({ forId: 'username-input', localizeAlias: this.usernameIsEmail ? 'auth_email' : 'auth_username', @@ -183,9 +281,12 @@ export default class UmbAuthElement extends UmbLitElement { localizeAlias: 'auth_password', localizeFallback: 'Password', }); - this._usernameLayoutItem = createFormLayoutItem(this._usernameLabel, this._usernameInput); - this._passwordLayoutItem = createFormLayoutItem(this._passwordLabel, this._passwordInput); + this._passwordLayoutItem = createFormLayoutPasswordItem( + this._passwordLabel, + this._passwordInput, + this._passwordShowPasswordToggleItem + ); this._form = createForm([this._usernameLayoutItem, this._passwordLayoutItem]); diff --git a/src/Umbraco.Web.UI.Login/src/localization/lang/da.ts b/src/Umbraco.Web.UI.Login/src/localization/lang/da.ts index fb15444c0d..dda9767dd6 100644 --- a/src/Umbraco.Web.UI.Login/src/localization/lang/da.ts +++ b/src/Umbraco.Web.UI.Login/src/localization/lang/da.ts @@ -47,5 +47,7 @@ export default { returnToLogin: 'Tilbage til log ind', localLoginDisabled: 'Desværre er det ikke muligt at logge ind direkte. Det er blevet deaktiveret af en login-udbyder.', friendlyGreeting: 'Hej!', + showPassword: 'Vis adgangskode', + hidePassword: 'Skjul adgangskode', }, } satisfies UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Login/src/localization/lang/en.ts b/src/Umbraco.Web.UI.Login/src/localization/lang/en.ts index edbecc1972..0ee9e4c57a 100644 --- a/src/Umbraco.Web.UI.Login/src/localization/lang/en.ts +++ b/src/Umbraco.Web.UI.Login/src/localization/lang/en.ts @@ -47,5 +47,7 @@ export default { returnToLogin: 'Return to login', localLoginDisabled: 'Unfortunately, direct login is not possible. It has been disabled by a provider.', friendlyGreeting: 'Hello', + showPassword: 'Show password', + hidePassword: 'Hide password', }, } satisfies UmbLocalizationDictionary;