본문으로 건너뛰기

폼 조합

폼은 Field 를 쌓아 만듭니다. 라벨은 칸 위, 필수는 *, 힌트와 오류 문장은 칸 아래에 aria-describedby 로 잇습니다. 칸을 떠날 때 검사하고, 제출하면 위에 오류 요약을 띄우고 첫 오류 칸으로 포커스를 옮깁니다.

import
import { FieldGroup, FormErrorSummary, SaveStatus, SubmitBar } from "@uyoolabs/ui/form";
쓰는 경우
칸 하나는 Field, 컨트롤 여러 개의 묶음은 FieldGroup, 저장 버튼이 있는 설정 · 편집 폼의 아래는 SubmitBar, 제출 뒤 고칠 칸 목록은 FormErrorSummary, 즉시 저장 · 자동 저장의 결과는 SaveStatus 입니다.
쓰지 않는 경우
다 채울 때까지 저장 버튼을 비활성으로 두지 않습니다(무엇이 빠졌는지 모릅니다). 스위치와 저장 버튼을 한 카드에 두지 않습니다. 빨간 상자 하나에 “입력값을 확인하세요” 만 쓰지 않고, 요약 항목은 칸 이름으로 시작하는 링크로 둡니다. 폼 위에 h1 을 두지 않습니다(이름은 브레드크럼, 섹션은 카드 + h2). 저장 중에 폼 전체를 흐리게 하지 않습니다.
검증 시점
입력하는 동안에는 검사하지 않고 글자 수만 셉니다. 칸을 떠날 때 형식 · 길이를 검사하고, 고치는 동안에는 맞는 값이 되는 즉시 오류를 지웁니다. 제출할 때 모두 검사해 요약 · 첫 오류 칸 포커스 · 저장 바의 “n곳”으로 알립니다. 서버 실패는 입력값을 지우지 않고 바에 문장 + 다시 저장을 둡니다.
폐기 예고
FormRow 는 Field layout="start", FormField 는 SubmitBar 로 바뀝니다. v2.x 에서 폐기를 예고합니다.

설정 폼

주소를 지우고 저장해 보세요. 요약이 뜨고 첫 오류 칸으로 포커스가 갑니다.

멤버에게 보이는 이름입니다.

3 / 40

Field — 글자 수 · 라벨 왼쪽

Field 에 count · layout 이 더해졌습니다(결정 #14).

멤버에게 보이는 이름입니다.

3 / 40

채널 · 멘션에 보이는 이름입니다.

FieldGroup

fieldset · legend 묶음입니다. 묶음 오류 문장은 묶음 아래에 한 번만 둡니다.

주간 요약을 받을 채널

하나 이상 고릅니다.

채널을 고르지 않았습니다. 하나 이상 선택합니다.

SubmitBar · SaveStatus

저장 바는 폼 열 아래에 붙어 뜹니다(sticky, z 10). 문서에서는 sticky 를 껐습니다.

저장하지 않은 변경 사항이 있습니다
2곳을 고쳐야 저장할 수 있습니다
저장하지 않은 변경 사항이 있습니다
저장하지 못했습니다. 연결을 확인하고 다시 저장합니다.
저장했습니다 ·

키보드

키동작
Tab읽는 순서(왼쪽 → 오른쪽, 위 → 아래)대로 칸을 옮깁니다
Enter한 줄 입력에서 제출합니다(저장 바의 저장 버튼). 한 · 일 IME 조합 중의 Enter 는 제출하지 않습니다(isComposing). 저장 중에는 막힙니다
Enter (요약 링크)그 칸으로 포커스를 옮기고 화면 가운데로 스크롤합니다
Esc대화상자 안의 폼이면 취소입니다. 변경이 있으면 떠날 때 경고합니다(Dialog)

내장 문구

lang(ko · en · ja)으로 고르고 labels 로 일부를 바꿉니다. 개수가 들어가는 문장은 함수입니다.

컴포넌트 · 키koenja
SubmitBar.dirty저장하지 않은 변경 사항이 있습니다You have unsaved changes保存していない変更があります
SubmitBar.invalid(n)n곳을 고쳐야 저장할 수 있습니다Fix n fields to saven か所を修正すると保存できます
SubmitBar.save · retry · cancel저장 · 다시 저장 · 취소Save changes · Save again · Cancel保存 · もう一度保存 · キャンセル
FormErrorSummary.title(n)저장하지 못했습니다. n곳을 고칩니다.Couldn’t save. Fix n fields.保存できませんでした。n か所を修正してください。
SaveStatus.saving · saved · error · retry저장 중 · 저장했습니다 · 저장하지 못했습니다 · 다시 시도Saving… · Saved · Couldn’t save · Try again保存中… · 保存しました · 保存できませんでした · 再試行

props

소스의 타입과 JSDoc 에서 생성한 표입니다. 컴포넌트를 고치면 pnpm gen 으로 다시 만듭니다.

Field

prop타입기본값설명
label필수ReactNode—
hintReactNode—무엇을 넣는지 · 어디에 쓰이는지. 컨트롤 아래에 두고 aria-describedby 로 연결한다
errorReactNode—무엇이 문제인지 + 어떻게 고치는지 한 문장(스펙 보드 C-Input). 있으면 aria-invalid 를 단다
requiredboolean—라벨 옆에 * 를 보이고 aria-required 를 단다. 네이티브 required 는 브라우저 말풍선이 오류 요약과 겹쳐 쓰지 않는다
announceboolean—오류가 생기는 즉시 읽어 준다(role="alert"). 기본값은 끈다. 스펙 보드는 칸을 떠날 때 검사하므로, alert 를 켜 두면 Tab 으로 다음 칸에 들어가는 순간 그 칸의 라벨 대신 앞 칸의 오류를 끼어들어 읽는다. 오류 안내는 제출할 때 오류 요약이 한 번에 맡는다. 제출 없이 서버 확인(주소 중복 등)만으로 오류가 생기는 칸에만 켠다.
count{ value: number; max: number }—글자 수. 힌트 오른쪽에 “3 / 40” 을 보이고 aria-describedby 로 잇는다. 넘으면 숫자만 오류 색 500 이 된다. 입력하는 동안 검사하지 않는다(C-Form 검증 시점). 넘은 것은 칸을 떠날 때 error 로 알린다
layout"top" | "start""top"라벨 자리. top(기본)은 칸 위, start 는 왼쪽 144px 열(칸이 많은 설정 화면, FormRow 대신). start 는 폼 폭 480 미만(uyoo-form 컨테이너, 없으면 화면 폭)에서 위로 접힌다
idstring—컨트롤 id. 주지 않으면 useId 로 만든다
classNamestring—
children필수(control: FieldControlProps) => ReactNode—컨트롤을 그리는 함수. cloneElement 대신 render-prop 을 고른 이유: - 검색 · 단위 · 복사 버튼이 붙은 입력처럼 컨트롤이 감싸져 있어도 props 가 정확히 그 입력에 간다. cloneElement 는 바깥 div 에 id 를 붙인다. - 받는 props 가 타입으로 드러나고, 어떤 컨트롤(Input · NativeSelect · textarea)이든 같은 방식이다.

FieldGroup

prop타입기본값설명
legend필수ReactNode—묶음 이름(“새 멤버의 기본 역할”). legend 로 그린다
hintReactNode—묶음 아래 도움말. aria-describedby 로 잇는다
errorReactNode—무엇이 문제인지 + 어떻게 고치는지 한 문장. 묶음 아래에 한 번만 보인다
requiredboolean—legend 옆에 * 를 보이고 위젯에 aria-required 를 단다
disabledboolean—묶음 전체 비활성(fieldset disabled). 안의 컨트롤이 모두 꺼진다
idstring—legend id 의 바탕. 주지 않으면 useId 로 만든다
classNamestring—
children필수ReactNode | ((group: FieldGroupControlProps) => ReactNode)—묶음 안. 두 가지로 쓴다. - 함수: 자기 역할이 있는 위젯 하나(RadioGroup · SegmentedControl)에 묶음 aria 를 펼친다. 이때 fieldset 은 모양만 남기고(role="presentation") 이름 · 설명 · 오류 · 필수는 그 위젯이 갖는다. 묶음이 두 번 읽히지 않는다. - 노드: 체크박스 · 스위치 여러 개. fieldset 이 묶음(group)이 되고 legend 가 이름이다.

FormErrorSummary

prop타입기본값설명
errors필수ReadonlyArray<FormError>—화면 순서대로의 오류. 비어 있으면 아무것도 그리지 않는다
attemptnumber—제출 횟수. 처음 나타날 때와 이 값이 바뀔 때마다 첫 오류 칸으로 포커스를 옮긴다
lang"ko" | "en" | "ja"—내장 문구의 언어
labelsPartial<FormErrorSummaryLabels>—내장 문구 중 바꿀 것만
classNamestring—

SubmitBar

prop타입기본값설명
dirtybooleantrue바뀐 것이 있다. false 이고 오류도 없으면 바를 그리지 않는다(저장되면 바가 내려간다)
pendingbooleanfalse저장 중. 저장 버튼 폭 그대로 스피너, 취소 잠금, 두 번 제출을 막는다
invalidCountnumber0제출했는데 고칠 칸이 남은 수. 바의 문장이 오류 톤이 된다
errorReactNode—칸과 무관한 서버 오류(무엇이 문제인지 + 할 일). role="alert" 로 읽고 저장 버튼이 labels.retry 가 된다
onCancel() => void—취소. 없으면 취소 버튼을 그리지 않는다
formstring—바가 form 밖에 있을 때 저장 버튼이 제출할 form 의 id
stickybooleantrue폼 열 아래에 붙어 뜬다(sticky). 정적인 자리(문서 · 대화상자 안)에서는 끈다
lang"ko" | "en" | "ja"—내장 문구의 언어
labelsPartial<SubmitBarLabels>—내장 문구 중 바꿀 것만
classNamestring—

SaveStatus

prop타입기본값설명
state필수"idle" | "saving" | "saved" | "error"—idle 은 아무 글자도 보이지 않는다(자리만 둔다)
atstring | Date—저장한 시각(UTC ISO 문자열 또는 Date). saved 옆에 “· 14:32” 로 보인다
timeZonestring"Asia/Seoul"시각을 보일 IANA 시간대. 없으면 서울. 서버 · 클라이언트가 같은 글자를 그리도록 시간대를 정해 넘긴다
onRetry() => void—있으면 error 에서 “다시 시도” 링크 버튼을 보인다. 실패 이유 문장은 그 줄(Switch 의 error)이 맡는다
lang"ko" | "en" | "ja""ko"내장 문구의 언어
labelsPartial<SaveStatusLabels>—내장 문구 중 바꿀 것만
classNamestring—

import

FieldGroup · FormErrorSummary · SubmitBar · SaveStatus 는 ./form 에 있어 radix-ui 가 peer 로 필요합니다. Field · Input 은 ./ui 입니다.