@olundot/ui · Inputs
FormField
레이블, 컨트롤, 도움말 텍스트, 카운터, 유효성 검사 메시지를 묶는 컴포지션 래퍼입니다.
- 역할
- label · description · error · 자식 control 묶기
- API
- render-prop ({controlProps}) → id/aria-* 자동 전파
- 접근성 기준
- aria-describedby 머지 + aria-invalid는 자식에
기본
레이블과 설명을 controlProps로 연결공식 기관명을 입력합니다.
코드 보기tsx
import { FormField, Input } from "@olundot/ui";
export function OrganizationField() {
return (
<FormField
id="organization"
label="소속 기관"
description="공식 기관명을 입력합니다."
>
{({ controlProps }) => (
<Input surface="canvas" {...controlProps} name="organization" placeholder="전북대학교 의과대학" />
)}
</FormField>
);
}검증 form
required, submit error, description 전환코드 보기tsx
import { useState, type FormEvent } from "react";
import { Button, FormField, Input, Select } from "@olundot/ui";
// 응시 등록 form — submit 시 검증. FormField가 error 시 description 자동 hide.
// description은 기본 안내를 제공하고, error가 있으면 오류 메시지가 대신 노출된다.
// Select는 value=""로 선택 안 함 상태를 표현한다.
const SELECT_OPTIONS = [
{ value: "answer-key", label: "정답 키" },
{ value: "score", label: "점수" },
{ value: "metadata", label: "메타데이터" },
];
export function ExamRegistrationForm() {
const [studentNumber, setStudentNumber] = useState("");
const [email, setEmail] = useState("");
const [correctionType, setCorrectionType] = useState("");
const [errors, setErrors] = useState<{ studentNumber?: string; email?: string; correctionType?: string }>({});
const [submitted, setSubmitted] = useState(false);
const clearError = (key: keyof typeof errors) => {
if (errors[key]) setErrors({ ...errors, [key]: undefined });
};
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const next: typeof errors = {};
if (!/^\d{8}$/.test(studentNumber)) next.studentNumber = "8자리 숫자(예: 20260001).";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) next.email = "올바른 이메일 형식이 아닙니다.";
if (!correctionType) next.correctionType = "정정 유형을 선택합니다.";
setErrors(next);
if (Object.keys(next).length === 0) setSubmitted(true);
};
if (submitted) {
return (
<div style={{ display: "grid", gap: "8px" }}>
<p>등록이 완료되었습니다.</p>
<Button type="button" variant="ghost" onClick={() => setSubmitted(false)}>다시 작성</Button>
</div>
);
}
return (
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "16px" }} noValidate>
<FormField
id="reg-sn"
label="학번"
required
description="시험 카드의 학번 8자리"
error={errors.studentNumber}
>
{({ controlProps }) => (
<Input surface="canvas" {...controlProps} name="studentNumber" placeholder="20260001" maxLength={8}
value={studentNumber}
onChange={(e) => { setStudentNumber(e.target.value); clearError("studentNumber"); }} />
)}
</FormField>
<FormField
id="reg-email"
label="이메일"
required
description="계정 복구에 사용됩니다."
error={errors.email}
>
{({ controlProps }) => (
<Input surface="canvas" {...controlProps} name="email" type="email" placeholder="you@example.com"
value={email}
onChange={(e) => { setEmail(e.target.value); clearError("email"); }} />
)}
</FormField>
<FormField
id="reg-type"
label="정정 유형"
required
description="가장 가까운 유형 1개"
error={errors.correctionType}
>
{({ controlProps }) => (
<Select surface="canvas" {...controlProps} name="correctionType" placeholder="선택"
options={SELECT_OPTIONS}
value={correctionType}
onValueChange={(v) => { setCorrectionType(v); clearError("correctionType"); }} />
)}
</FormField>
<Button type="submit">등록</Button>
</form>
);
}