50 lines
1.2 KiB
TypeScript
50 lines
1.2 KiB
TypeScript
"use client";
|
||
|
||
import { cloneElement, isValidElement, useId, type ReactNode } from "react";
|
||
|
||
import {
|
||
cn,
|
||
fieldErrorClass,
|
||
fieldHelpClass,
|
||
fieldLabelClass,
|
||
} from "@/lib/ui/variants";
|
||
|
||
interface FieldProps {
|
||
label: string;
|
||
children: ReactNode;
|
||
htmlFor?: string;
|
||
help?: ReactNode;
|
||
error?: ReactNode;
|
||
required?: boolean;
|
||
className?: string;
|
||
}
|
||
|
||
export function Field({
|
||
label,
|
||
children,
|
||
htmlFor,
|
||
help,
|
||
error,
|
||
required = false,
|
||
className,
|
||
}: FieldProps) {
|
||
const autoId = useId();
|
||
const id = htmlFor ?? autoId;
|
||
const labelText = required ? `${label} *` : label;
|
||
// 自动把 id 注入唯一子控件,建立 label↔控件关联(仅当子控件自身未带 id)。
|
||
const control =
|
||
isValidElement<{ id?: string }>(children) && children.props.id == null
|
||
? cloneElement(children, { id })
|
||
: children;
|
||
return (
|
||
<div className={cn("space-y-1.5", className)}>
|
||
<label htmlFor={id} className={fieldLabelClass()}>
|
||
{labelText}
|
||
</label>
|
||
{control}
|
||
{error ? <p className={fieldErrorClass()}>{error}</p> : null}
|
||
{!error && help ? <p className={fieldHelpClass()}>{help}</p> : null}
|
||
</div>
|
||
);
|
||
}
|