Files
clean-architecture-frontend…/src/presentation/forms/form-components.tsx
T

93 lines
2.3 KiB
TypeScript

import { useId, type FormHTMLAttributes, type ReactNode } from "react";
import { TextField } from "../components/ui/text-field.jsx";
import { useLocale } from "../i18n/index.js";
import type {
FieldErrors,
FieldName,
FormValues,
} from "./form-contracts.js";
export function Form(
props: FormHTMLAttributes<HTMLFormElement> & Readonly<{ pending?: boolean }>,
) {
const { pending = false, children, ...formProps } = props;
return (
<form {...formProps} noValidate aria-busy={pending || undefined}>
{children}
</form>
);
}
export function FormField(
props: React.ComponentProps<typeof TextField>,
) {
return <TextField {...props} />;
}
export function ErrorSummary<Values extends FormValues>(props: Readonly<{
fieldErrors: FieldErrors<Values>;
formErrors?: readonly string[];
fieldLabels: Readonly<Record<FieldName<Values>, string>>;
fieldId(name: FieldName<Values>): string;
onFocusField?(name: FieldName<Values>): void;
}>) {
const { message } = useLocale();
const {
fieldErrors,
formErrors = [],
fieldLabels,
fieldId,
onFocusField,
} = props;
const headingId = useId();
const entries = Object.entries(fieldErrors) as [
FieldName<Values>,
string,
][];
if (entries.length === 0 && formErrors.length === 0) return null;
return (
<section
className="form-error-summary"
role="alert"
aria-labelledby={headingId}
>
<h2 id={headingId}>{message("form.errorSummary")}</h2>
{entries.length > 0 ? (
<ul>
{entries.map(([name, message]) => (
<li key={name}>
<a
href={`#${fieldId(name)}`}
onClick={(event) => {
if (!onFocusField) return;
event.preventDefault();
onFocusField(name);
}}
>
{fieldLabels[name]}: {message}
</a>
</li>
))}
</ul>
) : null}
{formErrors.map((message) => (
<p key={message}>{message}</p>
))}
</section>
);
}
export function FormActions(props: Readonly<{
children: ReactNode;
sticky?: boolean;
}>) {
return (
<div
className={`form-actions${props.sticky ? " form-actions--sticky" : ""}`}
>
{props.children}
</div>
);
}