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:
committed by
GitHub
parent
1c6d4f360d
commit
3d24f0a51e
6
src/Umbraco.Web.UI.Login/public/closedEye.svg
Normal file
6
src/Umbraco.Web.UI.Login/public/closedEye.svg
Normal 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 |
4
src/Umbraco.Web.UI.Login/public/openEye.svg
Normal file
4
src/Umbraco.Web.UI.Login/public/openEye.svg
Normal 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 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user