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;