@@ -0,0 +1,50 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../lib/cn";
|
||||
import { Label } from "./label";
|
||||
|
||||
export interface FormFieldProps {
|
||||
label: React.ReactNode;
|
||||
children: React.ReactElement<{
|
||||
id?: string;
|
||||
"aria-describedby"?: string;
|
||||
"aria-invalid"?: boolean;
|
||||
}>;
|
||||
id?: string;
|
||||
description?: React.ReactNode;
|
||||
error?: React.ReactNode;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Associates a control with its label, supporting text, and validation error. */
|
||||
export function FormField({
|
||||
label,
|
||||
children,
|
||||
id,
|
||||
description,
|
||||
error,
|
||||
required,
|
||||
className,
|
||||
}: FormFieldProps) {
|
||||
const generatedId = React.useId();
|
||||
const controlId = id ?? children.props.id ?? `field-${generatedId}`;
|
||||
const descriptionId = description ? `${controlId}-description` : undefined;
|
||||
const errorId = error ? `${controlId}-error` : undefined;
|
||||
const describedBy = [children.props["aria-describedby"], descriptionId, errorId].filter(Boolean).join(" ") || undefined;
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-1.5", className)}>
|
||||
<Label htmlFor={controlId} className="mb-0 text-sm text-text">
|
||||
{label}
|
||||
{required && <span className="ml-1 text-danger" aria-hidden>*</span>}
|
||||
</Label>
|
||||
{React.cloneElement(children, {
|
||||
id: controlId,
|
||||
"aria-describedby": describedBy,
|
||||
"aria-invalid": Boolean(error) || children.props["aria-invalid"],
|
||||
})}
|
||||
{description && !error && <p id={descriptionId} className="text-xs text-muted">{description}</p>}
|
||||
{error && <p id={errorId} className="text-xs font-medium text-danger">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user