diff --git a/src/login/pages/Login.tsx b/src/login/pages/Login.tsx
index c9a8cd3d..14c40e00 100644
--- a/src/login/pages/Login.tsx
+++ b/src/login/pages/Login.tsx
@@ -1,7 +1,7 @@
import type { JSX } from "keycloakify/tools/JSX";
-import { useState, useEffect, useReducer } from "react";
+import { useState } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
-import { assert } from "keycloakify/tools/assert";
+import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
@@ -200,15 +200,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
const { msgStr } = i18n;
- const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
-
- useEffect(() => {
- const passwordInputElement = document.getElementById(passwordInputId);
-
- assert(passwordInputElement instanceof HTMLInputElement);
-
- passwordInputElement.type = isPasswordRevealed ? "text" : "password";
- }, [isPasswordRevealed]);
+ const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
return (
diff --git a/src/login/pages/LoginPassword.tsx b/src/login/pages/LoginPassword.tsx
index 3e734318..b703e1f1 100644
--- a/src/login/pages/LoginPassword.tsx
+++ b/src/login/pages/LoginPassword.tsx
@@ -1,8 +1,8 @@
import type { JSX } from "keycloakify/tools/JSX";
-import { useState, useEffect, useReducer } from "react";
+import { useState } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { clsx } from "keycloakify/tools/clsx";
-import { assert } from "keycloakify/tools/assert";
+import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
@@ -107,15 +107,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
const { msgStr } = i18n;
- const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
-
- useEffect(() => {
- const passwordInputElement = document.getElementById(passwordInputId);
-
- assert(passwordInputElement instanceof HTMLInputElement);
-
- passwordInputElement.type = isPasswordRevealed ? "text" : "password";
- }, [isPasswordRevealed]);
+ const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
return (
diff --git a/src/login/pages/LoginUpdatePassword.tsx b/src/login/pages/LoginUpdatePassword.tsx
index 6f9ce2dd..b568f41b 100644
--- a/src/login/pages/LoginUpdatePassword.tsx
+++ b/src/login/pages/LoginUpdatePassword.tsx
@@ -1,7 +1,6 @@
import type { JSX } from "keycloakify/tools/JSX";
-import { useEffect, useReducer } from "react";
+import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
-import { assert } from "keycloakify/tools/assert";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
@@ -146,15 +145,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
const { msgStr } = i18n;
- const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
-
- useEffect(() => {
- const passwordInputElement = document.getElementById(passwordInputId);
-
- assert(passwordInputElement instanceof HTMLInputElement);
-
- passwordInputElement.type = isPasswordRevealed ? "text" : "password";
- }, [isPasswordRevealed]);
+ const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
return (
diff --git a/src/tools/useIsPasswordRevealed.ts b/src/tools/useIsPasswordRevealed.ts
new file mode 100644
index 00000000..4dba509d
--- /dev/null
+++ b/src/tools/useIsPasswordRevealed.ts
@@ -0,0 +1,45 @@
+import { useEffect, useReducer } from "react";
+import { assert } from "keycloakify/tools/assert";
+
+/**
+ * Initially false, state that enables to dynamically control if
+ * the type of a password input is "password" (false) or "text" (true).
+ */
+export function useIsPasswordRevealed(params: { passwordInputId: string }) {
+ const { passwordInputId } = params;
+
+ const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer(
+ (isPasswordRevealed: boolean) => !isPasswordRevealed,
+ false
+ );
+
+ useEffect(() => {
+ const passwordInputElement = document.getElementById(passwordInputId);
+
+ assert(passwordInputElement instanceof HTMLInputElement);
+
+ const type = isPasswordRevealed ? "text" : "password";
+
+ passwordInputElement.type = type;
+
+ const observer = new MutationObserver(mutations => {
+ mutations.forEach(mutation => {
+ if (mutation.attributeName !== "type") {
+ return;
+ }
+ if (passwordInputElement.type === type) {
+ return;
+ }
+ passwordInputElement.type = type;
+ });
+ });
+
+ observer.observe(passwordInputElement, { attributes: true });
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [isPasswordRevealed]);
+
+ return { isPasswordRevealed, toggleIsPasswordRevealed };
+}