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;