DocSpace-client/packages/shared/components/password-input/PasswordInput.tsx

602 lines
16 KiB
TypeScript

import React, {
useCallback,
useRef,
useState,
KeyboardEvent,
ChangeEvent,
FocusEvent,
MouseEvent,
} from "react";
import { TooltipRefProps } from "react-tooltip";
// import equal from "fast-deep-equal/react";
import EyeReactSvgUrl from "PUBLIC_DIR/images/eye.react.svg?url";
import EyeOffReactSvgUrl from "PUBLIC_DIR/images/eye.off.react.svg?url";
import { InputBlock } from "../input-block";
import { Link, LinkType } from "../link";
import { Text } from "../text";
import { Tooltip } from "../tooltip";
import { InputType } from "../text-input";
import {
PasswordProgress,
StyledInput,
TooltipStyle,
StyledTooltipContainer,
StyledTooltipItem,
} from "./PasswordInput.styled";
import {
PasswordInputProps,
TPasswordSettings,
TPasswordValidation,
} from "./PasswordInput.types";
const PasswordInput = React.forwardRef(
(
{
inputType = InputType.password,
inputValue,
clipActionResource,
emailInputName,
onBlur,
onKeyDown,
onValidateInput,
onChange,
isDisabled,
simpleView,
passwordSettings,
generatorSpecial,
clipCopiedResource,
tooltipPasswordTitle,
tooltipPasswordLength,
tooltipPasswordDigits,
tooltipPasswordCapital,
tooltipPasswordSpecial,
generatePasswordTitle,
inputName,
scale,
size,
hasError,
hasWarning,
placeholder,
tabIndex,
maxLength,
id,
autoComplete,
forwardedRef,
isDisableTooltip,
inputWidth,
className,
style,
isFullWidth,
tooltipOffsetLeft,
tooltipOffsetTop,
isAutoFocussed,
}: PasswordInputProps,
ref,
) => {
const [state, setState] = useState({
type: inputType,
value: inputValue,
copyLabel: clipActionResource,
disableCopyAction: !emailInputName,
displayTooltip: false,
validLength: false,
validDigits: false,
validCapital: false,
validSpecial: false,
});
const refProgress = useRef(null);
const refTooltip = useRef(null);
const refTooltipContent = useRef(null);
const onBlurAction = useCallback(
(e: FocusEvent<HTMLInputElement>) => {
e.persist();
if (onBlur) onBlur(e);
},
[onBlur],
);
const onFocusAction = () => {
const length = state.value?.length ?? 0;
const minLength = passwordSettings?.minLength;
if ((minLength && length < minLength) || hasError || hasWarning) {
if (refTooltip.current) {
const tooltip = refTooltip.current as TooltipRefProps;
tooltip?.open?.();
}
}
};
const handleClickOutside = React.useCallback((event: Event) => {
if (refTooltip.current && refTooltipContent.current) {
const target = event.target as HTMLElement;
const tooltip = refTooltip.current as TooltipRefProps;
const tooltipContent = refTooltipContent.current as HTMLElement;
if (
!tooltip ||
!tooltip.isOpen ||
tooltip.activeAnchor?.contains(target) ||
tooltipContent?.parentElement?.contains(target)
)
return;
tooltip.close();
}
}, []);
React.useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [handleClickOutside]);
const onKeyDownAction = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
e.persist();
if (onKeyDown) onKeyDown(e);
},
[onKeyDown],
);
const changeInputType = () => {
const newType =
state.type === InputType.text ? InputType.password : InputType.text;
setState((s) => ({
...s,
type: newType,
}));
};
const testStrength = useCallback(
(value: string) => {
if (passwordSettings) {
const capitalRegExp = new RegExp(
passwordSettings.upperCaseRegexStr || "",
);
const digitalRegExp = new RegExp(
passwordSettings.digitsRegexStr || "",
);
const specSymbolsRegExp = new RegExp(
passwordSettings.specSymbolsRegexStr || "",
);
let capital = true;
let digits = true;
let special = true;
let allowed = true;
let length = true;
if (passwordSettings.upperCase) {
capital = capitalRegExp.test(value);
}
if (passwordSettings.digits) {
digits = digitalRegExp.test(value);
}
if (passwordSettings.specSymbols) {
special = specSymbolsRegExp.test(value);
}
if (passwordSettings.allowedCharactersRegexStr) {
const allowedRegExp = new RegExp(
`^${passwordSettings.allowedCharactersRegexStr}{1,}$`,
);
allowed = allowedRegExp.test(value);
}
if (passwordSettings?.minLength !== undefined) {
length = value.trim().length >= passwordSettings.minLength;
}
return {
allowed,
digits,
capital,
special,
length,
};
}
return {} as TPasswordValidation;
},
[passwordSettings],
);
const checkPassword = useCallback(
(value: string) => {
const passwordValidation = testStrength(value);
const progressScore =
(passwordValidation?.digits &&
passwordValidation?.capital &&
passwordValidation?.special &&
passwordValidation?.length &&
passwordValidation?.allowed) ||
false;
onValidateInput?.(progressScore, passwordValidation);
setState((s) => ({
...s,
value,
validLength: passwordValidation.length || false,
validDigits: passwordValidation.digits || false,
validCapital: passwordValidation.capital || false,
validSpecial: passwordValidation.special || false,
}));
},
[onValidateInput, testStrength],
);
const onChangeAction = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange?.(e);
if (refTooltip.current) {
const tooltip = refTooltip.current as TooltipRefProps;
if (tooltip?.isOpen) {
tooltip?.close?.();
}
}
if (simpleView) {
setState((s) => ({
...s,
value: e.target.value,
}));
return;
}
checkPassword(e.target.value);
},
[onChange, simpleView, checkPassword],
);
const getNewPassword = React.useCallback(() => {
const length = passwordSettings?.minLength || 0;
const string = "abcdefghijklmnopqrstuvwxyz";
const numeric = "0123456789";
const special = generatorSpecial || "";
let password = "";
let character = "";
while (password.length < length) {
const a = Math.ceil(string.length * Math.random() * Math.random());
const b = Math.ceil(numeric.length * Math.random() * Math.random());
const c = Math.ceil(special.length * Math.random() * Math.random());
let hold = string.charAt(a);
if (passwordSettings?.upperCase) {
hold = password.length % 2 === 0 ? hold.toUpperCase() : hold;
}
character += hold;
if (passwordSettings?.digits) {
character += numeric.charAt(b);
}
if (passwordSettings?.specSymbols) {
character += special.charAt(c);
}
password = character;
}
password = password
.split("")
.sort(() => 0.5 - Math.random())
.join("");
return password.substring(0, length);
}, [
generatorSpecial,
passwordSettings?.digits,
passwordSettings?.minLength,
passwordSettings?.specSymbols,
passwordSettings?.upperCase,
]);
const onGeneratePassword = React.useCallback(
(e: MouseEvent) => {
if (isDisabled) return e.preventDefault();
const newPassword = getNewPassword();
if (state.type !== InputType.text) {
setState((s) => ({
...s,
type: InputType.text,
}));
}
checkPassword(newPassword);
onChangeAction?.({
target: { value: newPassword },
} as ChangeEvent<HTMLInputElement>);
},
[checkPassword, getNewPassword, isDisabled, onChangeAction, state.type],
);
// const copyToClipboard = (emailName: string) => {
// const { disableCopyAction, value } = state;
// if (isDisabled || disableCopyAction) return;
// setState((s) => ({
// ...s,
// disableCopyAction: true,
// copyLabel: clipCopiedResource,
// }));
// const textField = document.createElement("textarea");
// const emailValue = (
// document.getElementsByName(emailName)[0] as HTMLInputElement
// ).value;
// const formattedText = `${clipEmailResource}${emailValue} | ${clipPasswordResource}${value}`;
// textField.innerText = formattedText;
// document.body.appendChild(textField);
// textField.select();
// document.execCommand("copy");
// textField.remove();
// onCopyToClipboard?.(formattedText);
// setTimeout(() => {
// setState({
// ...state,
// disableCopyAction: false,
// copyLabel: clipActionResource,
// });
// }, 2000);
// };
React.useEffect(() => {
setState((s) => ({ ...s, copyLabel: clipActionResource }));
}, [clipActionResource, clipCopiedResource]);
React.useImperativeHandle(
ref,
() => {
return { onGeneratePassword, setState, value: state.value };
},
[onGeneratePassword, setState, state.value],
);
const renderTextTooltip = (
settings?: TPasswordSettings,
length?: number,
digits?: string,
capital?: string,
special?: string,
) => {
return (
<>
<div className="break" />
<Text
className="text-tooltip"
fontSize="10px"
color="#A3A9AE"
as="span"
>
{settings?.minLength ? length : null}{" "}
{settings?.digits ? `, ${digits}` : null}{" "}
{settings?.upperCase ? `, ${capital}` : null}{" "}
{settings?.specSymbols ? `, ${special}` : null}
</Text>
<div className="break" />
</>
);
};
const renderTooltipContent = () => (
<TooltipStyle ref={refTooltipContent}>
<StyledTooltipContainer
forwardedAs="div"
fontSize="12px"
title={tooltipPasswordTitle}
>
{tooltipPasswordTitle}
<StyledTooltipItem
forwardedAs="div"
title={tooltipPasswordLength}
valid={state.validLength}
>
{tooltipPasswordLength}
</StyledTooltipItem>
{passwordSettings?.digits && (
<StyledTooltipItem
forwardedAs="div"
title={tooltipPasswordDigits}
valid={state.validDigits}
>
{tooltipPasswordDigits}
</StyledTooltipItem>
)}
{passwordSettings?.upperCase && (
<StyledTooltipItem
forwardedAs="div"
title={tooltipPasswordCapital}
valid={state.validCapital}
>
{tooltipPasswordCapital}
</StyledTooltipItem>
)}
{passwordSettings?.specSymbols && (
<StyledTooltipItem
forwardedAs="div"
title={tooltipPasswordSpecial}
valid={state.validSpecial}
>
{tooltipPasswordSpecial}
</StyledTooltipItem>
)}
{generatePasswordTitle && (
<div className="generate-btn-container">
<Link
className="generate-btn"
type={LinkType.action}
fontWeight="600"
isHovered
onClick={onGeneratePassword}
>
{generatePasswordTitle}
</Link>
</div>
)}
</StyledTooltipContainer>
</TooltipStyle>
);
const renderInputGroup = () => {
const { type, value } = state;
const iconName = type === "password" ? EyeOffReactSvgUrl : EyeReactSvgUrl;
const iconButtonClassName = `password_eye--${
type === "password" ? "close" : "open"
}`;
return (
<>
<InputBlock
id={id}
className="input-relative"
name={inputName}
hasError={hasError}
isDisabled={isDisabled}
iconName={iconName}
iconButtonClassName={iconButtonClassName}
value={value ?? ""}
onIconClick={changeInputType}
onChange={onChangeAction}
scale={scale}
size={size}
type={type}
iconSize={16}
isIconFill
onBlur={onBlurAction}
onFocus={onFocusAction}
onKeyDown={onKeyDownAction}
hasWarning={hasWarning}
placeholder={placeholder}
tabIndex={tabIndex}
maxLength={maxLength}
autoComplete={autoComplete}
forwardedRef={forwardedRef}
isAutoFocussed={isAutoFocussed}
/>
{!isDisableTooltip && !isDisabled && (
<Tooltip
place="top"
clickable
openOnClick
anchorSelect="div[id='tooltipContent'] input"
offsetLeft={tooltipOffsetLeft}
offsetTop={tooltipOffsetTop}
ref={refTooltip}
imperativeModeOnly
>
{renderTooltipContent()}
</Tooltip>
)}
</>
);
};
return (
<StyledInput
onValidateInput={onValidateInput}
className={className}
style={style}
$isFullWidth={isFullWidth}
data-testid="password-input"
>
{simpleView ? (
<>
{renderInputGroup()}
{renderTextTooltip()}
</>
) : (
<>
<div className="password-field-wrapper">
<PasswordProgress
id="tooltipContent"
ref={refProgress}
inputWidth={inputWidth}
>
{renderInputGroup()}
</PasswordProgress>
</div>
{renderTextTooltip()}
</>
)}
</StyledInput>
);
},
);
PasswordInput.displayName = "PasswordInput";
PasswordInput.defaultProps = {
inputName: "passwordInput",
autoComplete: "new-password",
isDisabled: false,
scale: true,
isDisableTooltip: false,
isTextTooltipVisible: false,
clipEmailResource: "E-mail ",
clipPasswordResource: "Password ",
clipCopiedResource: "Copied",
generatorSpecial: "!@#$%^&*",
className: "",
simpleView: false,
passwordSettings: {
minLength: 8,
upperCase: false,
digits: false,
specSymbols: false,
digitsRegexStr: "(?=.*\\d)",
upperCaseRegexStr: "(?=.*[A-Z])",
specSymbolsRegexStr: "(?=.*[\\x21-\\x2F\\x3A-\\x40\\x5B-\\x60\\x7B-\\x7E])",
},
isFullWidth: false,
};
export { PasswordInput };
// const compare = (
// prevProps: Readonly<PasswordInputProps>,
// nextProps: Readonly<PasswordInputProps>,
// ) => {
// return equal(prevProps, nextProps);
// };
// const PasswordInput = React.memo(
// (props) => <PasswordInputPure {...props} />,
// compare,
// );
// PasswordInput.displayName = "PasswordInput";
// export { PasswordInput };