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>
This commit is contained in:
Mathias Helsengren
2025-10-30 11:56:15 +01:00
committed by GitHub
parent 1c6d4f360d
commit 3d24f0a51e
6 changed files with 195 additions and 40 deletions

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"></path>
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"></path>
<path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"></path>
<line x1="2" x2="22" y1="2" y2="22"></line>
</svg>

After

Width:  |  Height:  |  Size: 452 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>

After

Width:  |  Height:  |  Size: 273 B

View File

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

View File

@@ -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<void> {
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]);

View File

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

View File

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