From cb358bd74551225ff2e3898756f29bb80cfff7f1 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Wed, 8 Jan 2025 00:06:04 +0100 Subject: [PATCH] #754: PasswordWrapper fix for React 19 --- src/login/UserProfileFormFields.tsx | 13 ++----- src/login/pages/Login.tsx | 14 ++------ src/login/pages/LoginPassword.tsx | 14 ++------ src/login/pages/LoginUpdatePassword.tsx | 13 ++----- src/tools/useIsPasswordRevealed.ts | 45 +++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 43 deletions(-) create mode 100644 src/tools/useIsPasswordRevealed.ts diff --git a/src/login/UserProfileFormFields.tsx b/src/login/UserProfileFormFields.tsx index 5e3bcd8b..53841597 100644 --- a/src/login/UserProfileFormFields.tsx +++ b/src/login/UserProfileFormFields.tsx @@ -1,6 +1,7 @@ import type { JSX } from "keycloakify/tools/JSX"; -import { useEffect, useReducer, Fragment } from "react"; +import { useEffect, Fragment } from "react"; import { assert } from "keycloakify/tools/assert"; +import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed"; import type { KcClsx } from "keycloakify/login/lib/kcClsx"; import { useUserProfileForm, @@ -249,15 +250,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/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 }; +}