Files
gerbeur/src/components/form/TextField.tsx
khannurien 76de798faf
Some checks failed
Build and Publish Docker Image / build-and-push (push) Failing after 20s
v3: fixed authorization bug with stale tokens, fixed private dump access bug, various small fixes across the app
2026-06-27 10:40:07 +00:00

101 lines
2.7 KiB
TypeScript

import { type ReactNode, useId } from "react";
import {
type FieldValues,
type Path,
type RegisterOptions,
useFormContext,
useWatch,
} from "react-hook-form";
import { charCountClass } from "../../utils/charCount.ts";
interface TextFieldProps<T extends FieldValues> {
name: Path<T>;
label?: string;
/** Optional control rendered to the right of the label (e.g. a reset button). */
labelAction?: ReactNode;
type?: "text" | "email" | "password" | "url" | "search";
placeholder?: string;
autoComplete?: string;
autoFocus?: boolean;
disabled?: boolean;
rules?: RegisterOptions<T, Path<T>>;
/** Cap input length at `max` (native `maxLength`). */
maxLength?: number;
/** Show a live `n / maxLength` counter. Requires `maxLength`. */
showCount?: boolean;
}
/** Live `n / max` counter; isolated so only counted fields subscribe to value. */
function CharCounter<T extends FieldValues>(
{ name, max }: { name: Path<T>; max: number },
) {
const value = useWatch({ name }) as string | undefined;
const len = value?.length ?? 0;
return (
<span className={`text-editor-count${charCountClass(len, max)}`}>
{len} / {max}
</span>
);
}
/**
* Native text input wired to react-hook-form via the surrounding `FormProvider`.
* Renders a label, the input (with an optional character counter), and a
* validation hint sourced from `formState.errors[name]`.
*/
export function TextField<T extends FieldValues>({
name,
label,
labelAction,
type = "text",
placeholder,
autoComplete,
autoFocus,
disabled,
rules,
maxLength,
showCount = false,
}: TextFieldProps<T>) {
const id = useId();
const { register, formState: { errors } } = useFormContext<T>();
const error = errors[name];
const counted = showCount && maxLength != null;
return (
<div className="form-field">
{(label || labelAction) && (
<div className="form-label-row">
{label
? (
<label className="form-label" htmlFor={id}>
{label}
</label>
)
: <span />}
{labelAction}
</div>
)}
<div className={counted ? "input-with-count" : undefined}>
<input
id={id}
type={type}
placeholder={placeholder}
autoComplete={autoComplete}
autoFocus={autoFocus}
disabled={disabled}
maxLength={maxLength}
aria-invalid={error ? true : undefined}
{...register(name, rules)}
/>
{counted && <CharCounter<T> name={name} max={maxLength} />}
</div>
{error?.message && (
<span className="form-hint form-hint--error">
{String(error.message)}
</span>
)}
</div>
);
}