Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
1b77c69a01 | |||
158275f5c2 | |||
a085c8093e | |||
cb358bd745 | |||
e788c46601 | |||
d551b4bffb | |||
c168c7b156 | |||
7a46115042 | |||
249a7bde89 | |||
813740a002 | |||
7840c2a6f5 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "11.8.3",
|
"version": "11.8.8",
|
||||||
"description": "Framework to create custom Keycloak UIs",
|
"description": "Framework to create custom Keycloak UIs",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -280,6 +280,24 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
"fonts",
|
"fonts",
|
||||||
"OpenSans-Semibold-webfont.woff2"
|
"OpenSans-Semibold-webfont.woff2"
|
||||||
),
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.woff2"
|
||||||
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.woff"
|
||||||
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.ttf"
|
||||||
|
),
|
||||||
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
||||||
pathJoin("jquery", "dist", "jquery.min.js"),
|
pathJoin("jquery", "dist", "jquery.min.js"),
|
||||||
pathJoin("rfc4648", "lib", "rfc4648.js")
|
pathJoin("rfc4648", "lib", "rfc4648.js")
|
||||||
|
@ -103,14 +103,21 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
|
|
||||||
const moduleName = `@keycloakify/email-native`;
|
const moduleName = `@keycloakify/email-native`;
|
||||||
|
|
||||||
const [version] = (
|
const [version] = ((): string[] => {
|
||||||
JSON.parse(
|
const cmdOutput = child_process
|
||||||
child_process
|
.execSync(`npm show ${moduleName} versions --json`)
|
||||||
.execSync(`npm show ${moduleName} versions --json`)
|
.toString("utf8")
|
||||||
.toString("utf8")
|
.trim();
|
||||||
.trim()
|
|
||||||
) as string[]
|
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||||
)
|
|
||||||
|
// NOTE: Bug in some older npm versions
|
||||||
|
if (typeof versions === "string") {
|
||||||
|
return [versions];
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
})()
|
||||||
.reverse()
|
.reverse()
|
||||||
.filter(version => !version.includes("-"));
|
.filter(version => !version.includes("-"));
|
||||||
|
|
||||||
|
@ -6,8 +6,7 @@ import {
|
|||||||
join as pathJoin,
|
join as pathJoin,
|
||||||
relative as pathRelative,
|
relative as pathRelative,
|
||||||
dirname as pathDirname,
|
dirname as pathDirname,
|
||||||
extname as pathExtname,
|
basename as pathBasename
|
||||||
sep as pathSep
|
|
||||||
} from "path";
|
} from "path";
|
||||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||||
@ -37,6 +36,7 @@ import propertiesParser from "properties-parser";
|
|||||||
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
||||||
import { listInstalledModules } from "../../tools/listInstalledModules";
|
import { listInstalledModules } from "../../tools/listInstalledModules";
|
||||||
import { isInside } from "../../tools/isInside";
|
import { isInside } from "../../tools/isInside";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||||
BuildContextLike_generateMessageProperties & {
|
BuildContextLike_generateMessageProperties & {
|
||||||
@ -663,7 +663,7 @@ export async function generateResources(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const themeVariantName of buildContext.themeNames) {
|
for (const themeVariantName of [...buildContext.themeNames].reverse()) {
|
||||||
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||||
copy_main_theme_to_theme_variant_theme: {
|
copy_main_theme_to_theme_variant_theme: {
|
||||||
let isNative: boolean;
|
let isNative: boolean;
|
||||||
@ -678,44 +678,59 @@ export async function generateResources(params: {
|
|||||||
isNative = !v.isImplemented && v.isImplemented_native;
|
isNative = !v.isImplemented && v.isImplemented_native;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeVariantName === themeName) {
|
if (!isNative && themeVariantName === themeName) {
|
||||||
break copy_main_theme_to_theme_variant_theme;
|
break copy_main_theme_to_theme_variant_theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
srcDirPath: pathJoin(resourcesDirPath, "theme", themeName, themeType),
|
srcDirPath: getThemeTypeDirPath({ themeName, themeType }),
|
||||||
destDirPath: pathJoin(
|
destDirPath: getThemeTypeDirPath({
|
||||||
resourcesDirPath,
|
themeName: themeVariantName,
|
||||||
"theme",
|
|
||||||
themeVariantName,
|
|
||||||
themeType
|
themeType
|
||||||
),
|
}),
|
||||||
transformSourceCode: isNative
|
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||||
? undefined
|
patch_xKeycloakify_themeName: {
|
||||||
: ({ fileRelativePath, sourceCode }) => {
|
if (!fileRelativePath.endsWith(".ftl")) {
|
||||||
if (
|
break patch_xKeycloakify_themeName;
|
||||||
pathExtname(fileRelativePath) === ".ftl" &&
|
}
|
||||||
fileRelativePath.split(pathSep).length === 1
|
|
||||||
) {
|
|
||||||
const modifiedSourceCode = Buffer.from(
|
|
||||||
Buffer.from(sourceCode)
|
|
||||||
.toString("utf-8")
|
|
||||||
.replace(
|
|
||||||
`"themeName": "${themeName}"`,
|
|
||||||
`"themeName": "${themeVariantName}"`
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(
|
if (
|
||||||
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
!isNative &&
|
||||||
);
|
pathBasename(fileRelativePath) !== fileRelativePath
|
||||||
|
) {
|
||||||
|
break patch_xKeycloakify_themeName;
|
||||||
|
}
|
||||||
|
|
||||||
return { modifiedSourceCode };
|
const modifiedSourceCode = Buffer.from(
|
||||||
}
|
Buffer.from(sourceCode)
|
||||||
|
.toString("utf-8")
|
||||||
|
.replace(
|
||||||
|
...id<[string | RegExp, string]>(
|
||||||
|
isNative
|
||||||
|
? [
|
||||||
|
/xKeycloakify\.themeName/g,
|
||||||
|
`"${themeVariantName}"`
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`"themeName": "${themeName}"`,
|
||||||
|
`"themeName": "${themeVariantName}"`
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
return { modifiedSourceCode: sourceCode };
|
if (!isNative) {
|
||||||
}
|
assert(
|
||||||
|
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode: sourceCode };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
run_writeMessagePropertiesFiles: {
|
run_writeMessagePropertiesFiles: {
|
||||||
@ -734,42 +749,6 @@ export async function generateResources(params: {
|
|||||||
themeName: themeVariantName
|
themeName: themeVariantName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
replace_xKeycloakify_themeName_in_native_ftl_files: {
|
|
||||||
{
|
|
||||||
const v = buildContext.implementedThemeTypes[themeType];
|
|
||||||
|
|
||||||
if (v.isImplemented || !v.isImplemented_native) {
|
|
||||||
break replace_xKeycloakify_themeName_in_native_ftl_files;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailThemeDirPath = getThemeTypeDirPath({
|
|
||||||
themeName: themeVariantName,
|
|
||||||
themeType
|
|
||||||
});
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: emailThemeDirPath,
|
|
||||||
destDirPath: emailThemeDirPath,
|
|
||||||
transformSourceCode: ({ filePath, sourceCode }) => {
|
|
||||||
if (!filePath.endsWith(".ftl")) {
|
|
||||||
return { modifiedSourceCode: sourceCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
modifiedSourceCode: Buffer.from(
|
|
||||||
sourceCode
|
|
||||||
.toString("utf8")
|
|
||||||
.replace(
|
|
||||||
/xKeycloakify\.themeName/g,
|
|
||||||
`"${themeVariantName}"`
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,14 +105,21 @@ export async function initializeSpa(params: {
|
|||||||
|
|
||||||
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
|
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
|
||||||
|
|
||||||
const version = (
|
const version = ((): string[] => {
|
||||||
JSON.parse(
|
const cmdOutput = child_process
|
||||||
child_process
|
.execSync(`npm show ${moduleName} versions --json`)
|
||||||
.execSync(`npm show ${moduleName} versions --json`)
|
.toString("utf8")
|
||||||
.toString("utf8")
|
.trim();
|
||||||
.trim()
|
|
||||||
) as string[]
|
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||||
)
|
|
||||||
|
// NOTE: Bug in some older npm versions
|
||||||
|
if (typeof versions === "string") {
|
||||||
|
return [versions];
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
})()
|
||||||
.reverse()
|
.reverse()
|
||||||
.filter(version => !version.includes("-"))
|
.filter(version => !version.includes("-"))
|
||||||
.find(version =>
|
.find(version =>
|
||||||
|
@ -101,7 +101,6 @@ function addOrEditTestUser(params: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
newUser.username = defaultUser_default.username;
|
newUser.username = defaultUser_default.username;
|
||||||
newUser.email = defaultUser_default.email;
|
|
||||||
|
|
||||||
delete_existing_password_credential_if_any: {
|
delete_existing_password_credential_if_any: {
|
||||||
const i = newUser.credentials.findIndex(
|
const i = newUser.credentials.findIndex(
|
||||||
|
@ -100,9 +100,19 @@ function addCommentToSourceCode(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fileRelativePath.endsWith(".ftl")) {
|
if (fileRelativePath.endsWith(".ftl")) {
|
||||||
return toResult(
|
const comment = [`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join(
|
||||||
[`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join("\n")
|
"\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (sourceCode.trim().startsWith("<#ftl")) {
|
||||||
|
const [first, ...rest] = sourceCode.split(">");
|
||||||
|
|
||||||
|
const last = rest.join(">");
|
||||||
|
|
||||||
|
return [`${first}>`, comment, last].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResult(comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
|
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
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 { assert } from "keycloakify/tools/assert";
|
||||||
|
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||||
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import {
|
import {
|
||||||
useUserProfileForm,
|
useUserProfileForm,
|
||||||
@ -249,15 +250,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useState, useEffect, useReducer } from "react";
|
import { useState } from "react";
|
||||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
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 { clsx } from "keycloakify/tools/clsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
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 { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useState, useEffect, useReducer } from "react";
|
import { useState } from "react";
|
||||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
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 { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
@ -107,15 +107,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
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 { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
|
||||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
@ -146,15 +145,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
45
src/tools/useIsPasswordRevealed.ts
Normal file
45
src/tools/useIsPasswordRevealed.ts
Normal file
@ -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 };
|
||||||
|
}
|
Reference in New Issue
Block a user