98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import type { AnyFieldMeta } from '@tanstack/react-form';
|
|
import { LucideEye, LucideEyeClosed } from 'lucide-react';
|
|
import { useCallback, useId, useState } from 'react';
|
|
import { InfoIcon, type InfoIconProps } from '../info';
|
|
|
|
export type TextFieldProps = {
|
|
label?: string;
|
|
value?: string;
|
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
labelProps?: React.LabelHTMLAttributes<HTMLLabelElement>;
|
|
labelDivProps?: React.HTMLAttributes<HTMLDivElement>;
|
|
infoIconProps?: InfoIconProps;
|
|
} & React.InputHTMLAttributes<HTMLInputElement> & {
|
|
type?: 'password';
|
|
showPasswordToggle?: boolean;
|
|
};
|
|
|
|
export function TextField({ label, value, onChange, labelProps, labelDivProps, showPasswordToggle, infoIconProps, ...rest }: TextFieldProps) {
|
|
const id = useId();
|
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
|
const handlePasswordVisibilitySet = useCallback(
|
|
(e: React.MouseEvent | React.TouchEvent, visible: boolean) => {
|
|
if (rest.type !== 'password') return;
|
|
e.preventDefault();
|
|
setIsPasswordVisible(() => visible);
|
|
},
|
|
[rest.type]
|
|
);
|
|
|
|
return (
|
|
<label htmlFor={id} style={{ display: 'block', marginBottom: 8 }} {...labelProps}>
|
|
{label && (
|
|
<div style={{ fontSize: 12, color: 'var(--gray-9)', marginBottom: 6, display: 'flex', alignItems: 'center' }} {...labelDivProps}>
|
|
{label}
|
|
{infoIconProps && <InfoIcon {...infoIconProps} style={{ marginLeft: 4, verticalAlign: 'middle' }} />}
|
|
</div>
|
|
)}
|
|
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<input
|
|
{...rest}
|
|
type={rest.type === 'password' ? (isPasswordVisible && showPasswordToggle ? 'text' : 'password') : rest.type}
|
|
id={id}
|
|
value={value}
|
|
onChange={onChange}
|
|
style={{
|
|
width: '100%',
|
|
padding: '10px 12px',
|
|
borderRadius: 6,
|
|
border: '1px solid var(--gray-5)',
|
|
...rest?.style,
|
|
}}
|
|
/>
|
|
|
|
<div
|
|
style={{ position: 'absolute', right: 12 }}
|
|
onMouseDown={(e) => {
|
|
handlePasswordVisibilitySet(e, true);
|
|
}}
|
|
onMouseUp={(e) => {
|
|
handlePasswordVisibilitySet(e, false);
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
handlePasswordVisibilitySet(e, false);
|
|
}}
|
|
onTouchStart={(e) => {
|
|
handlePasswordVisibilitySet(e, true);
|
|
}}
|
|
onTouchEnd={(e) => {
|
|
handlePasswordVisibilitySet(e, false);
|
|
}}
|
|
>
|
|
{showPasswordToggle ? isPasswordVisible ? <LucideEye size={16} /> : <LucideEyeClosed size={16} /> : null}
|
|
</div>
|
|
</div>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
export type TextFieldErrorMessageProps = AnyFieldMeta & {
|
|
errorMessage?: string;
|
|
};
|
|
|
|
export function TextFieldErrorMessage({ isValid, errors, errorMessage }: TextFieldErrorMessageProps) {
|
|
return (
|
|
!isValid && (
|
|
<div
|
|
style={{
|
|
marginTop: 4,
|
|
fontSize: 12,
|
|
color: 'var(--red-9)',
|
|
}}
|
|
>
|
|
{errorMessage ?? errors?.reduce((msg, err) => msg + err.message + ' ', '')}
|
|
</div>
|
|
)
|
|
);
|
|
}
|