Some checks failed
Build and Publish Docker Image / build-and-push (push) Failing after 20s
101 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|