import { useState } from "react"; import { Cascader, Checkbox, CheckboxGroup, DatePicker, Field, Form, FormErrors, FormErrorsProvider, FormField, Input, InputOTP, InputOTPGroup, InputOTPSlot, NumberInput, PasswordInput, Rating, RadioGroup, SearchInput, Select, Slider, Switch, TagInput, Textarea, TimePicker, ToggleGroup, ToggleGroupItem, TreeSelect, Upload, type UploadFileItem, } from "@godxjp/ui/data-entry"; import type { DateRange } from "react-day-picker"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display"; import { Button, Heading, Text } from "@godxjp/ui/general"; import { Flex, PageContainer } from "@godxjp/ui/layout"; /** Keep the upload lifecycle visible in the form example. */ async function noopUpload(_file: File, _item: UploadFileItem) { await new Promise((resolve) => setTimeout(resolve, 800)); return { mediaId: crypto.randomUUID() }; } /** Expense category tree for the Cascader field. */ const EXPENSE_CATEGORIES = [ { value: "operating", label: "営業費用", children: [ { value: "travel", label: "旅費交通費" }, { value: "communication", label: "通信費" }, { value: "supplies", label: "消耗品費" }, ], }, { value: "labor", label: "人件費", children: [ { value: "salary", label: "給与手当" }, { value: "bonus", label: "賞与" }, ], }, ]; /** Account tree for the TreeSelect field. */ const ACCOUNT_TREE = [ { value: "assets", label: "資産", children: [ { value: "cash", label: "現金" }, { value: "bank", label: "普通預金" }, { value: "ar", label: "売掛金" }, ], }, { value: "expenses", label: "費用", children: [ { value: "rent", label: "地代家賃" }, { value: "utilities", label: "水道光熱費" }, ], }, ]; /** 都道府県 — 住所の複合行の先頭。value は ISO 3166-2:JP のサブディビジョンコード。 */ const PREFECTURES = [ { value: "JP-13", label: "東京都" }, { value: "JP-14", label: "神奈川県" }, { value: "JP-27", label: "大阪府" }, { value: "JP-23", label: "愛知県" }, { value: "JP-01", label: "北海道" }, ]; /** 生年月日の 年/月/日 3連 Select — 日本の業務フォームで最も多い複合フィールド。 */ const BIRTH_YEARS = Array.from({ length: 60 }, (_, index) => { const year = 2006 - index; return { value: String(year), label: `${year}年` }; }); const MONTHS = Array.from({ length: 12 }, (_, index) => ({ value: String(index + 1), label: `${index + 1}月`, })); const DAYS = Array.from({ length: 31 }, (_, index) => ({ value: String(index + 1), label: `${index + 1}日`, })); /** * Form — the comprehensive catalogue. One page proving (1) EVERY data-entry field type wired in a * FormField, (2) every Form LAYOUT (vertical / horizontal / inline / multi-column / responsive * collapse), and (3) every Form STATE (pristine · filled · required-error · helper · disabled · * read-only · pending · success). Composed only from real @godxjp/ui primitives; all typography is * the Text/Heading primitive (never a hand-rolled span). For the full controlled validation flow, * see the example screens under Form → examples. */ export default function Demo() { // ── Field-catalogue state (each field independent) ───────────────────────── const [text, setText] = useState(""); const [search, setSearch] = useState(""); const [memo, setMemo] = useState(""); const [account, setAccount] = useState(""); const [tree, setTree] = useState(); const [category, setCategory] = useState([]); const [terms, setTerms] = useState(false); const [channels, setChannels] = useState(["email"]); const [rounding, setRounding] = useState("round"); const [active, setActive] = useState(true); const [taxRate, setTaxRate] = useState([10]); const [issueDate, setIssueDate] = useState(new Date(2026, 0, 15)); const [period, setPeriod] = useState(); const [closeTime, setCloseTime] = useState("17:30"); const [startTime, setStartTime] = useState("09:00"); const [endTime, setEndTime] = useState(""); const [score, setScore] = useState(4); const [tags, setTags] = useState(["優先"]); const [otp, setOtp] = useState(""); const [files, setFiles] = useState([]); const [period2, setPeriod2] = useState("monthly"); // ── 複合フィールド(1ラベル × 複数入力)の状態 ───────────────────────────── const [lastName, setLastName] = useState("山田"); const [firstName, setFirstName] = useState("太郎"); const [kanaLast, setKanaLast] = useState("ヤマダ"); const [kanaFirst, setKanaFirst] = useState("タロウ"); const [zip1, setZip1] = useState("100"); const [zip2, setZip2] = useState("0005"); const [prefecture, setPrefecture] = useState("JP-13"); const [birthYear, setBirthYear] = useState("1988"); const [birthMonth, setBirthMonth] = useState("4"); const [birthDay, setBirthDay] = useState(""); // ── Textarea 系の状態(文字数カウンタ・自動伸長) ────────────────────────── const [remarks, setRemarks] = useState("初回取引のため与信枠は50万円で開始する。"); const [thread, setThread] = useState(""); return ( Validation feedback 確認中・成功・注意・エラーを区別します。
{/* ════════════════════ 1. COMPOUND FIELDS ════════════════════ */} 1. 複合フィールド · 1つのラベルに複数の入力欄 日本の業務フォームでは氏名・郵便番号・電話番号・生年月日がいずれも「1項目 × 複数入力欄」になる。FormField は直下の 1 要素にしか属性を注入できないので、直下が Flex のときはラベルを FieldNameContext で配り、Flex 自身を role=group にする。これが無いと中の Input / Select は全て無名になり、axe の label · button-name が一気に赤くなる(gh#303)。 氏名 · フリガナ(1ラベル × 2入力) 「氏名」は 1 項目・2 欄。Flex でくるんでも姓と名の両方にラベルが届く。ただし各欄の aria-label を省くと両方が「氏名」と読み上げられ、どちらが姓か分からなくなる。 複合フィールドで最初に壊れるのがここ。data-field は各 Input 自身の id から解決され、 cmp-name-last / cmp-name-first として別々に自動テストから掴める。
郵便番号 · 住所(狭い欄と広い欄が同じ行に並ぶ) 郵便番号は 3桁 + 4桁 の固定幅、住所は残り全部。幅を Input に持たせる方法は無いので、狭い欄だけを Flex width で囲み shrink={false} で潰れないようにする。区切りの「-」は Text aria-hidden で、読み上げには混ぜない。 住所行は都道府県 Select(width=auto)+市区町村+番地で、grow が効かないと番地欄が 3文字幅に潰れるのが見える。
住所を自動入力 } > -
電話番号 3分割 · 生年月日 年/月/日 Select 桁数の決まった 3 分割と、型の違うコントロールを 1 ラベル下に混ぜる場合。生年月日は Select × 3(年は60件、月日は固定)で、どれか一つでも未選択なら値として不完全になる。 その「部分入力」をどう検証するかが複合フィールドの本題。DatePicker で足りる画面ではこのパターンを使わない。
- - - - -
{/* ════════════════════ 2. EVERY FIELD TYPE ════════════════════ */} 2. 全フィールド型 · すべての入力コントロール ライブラリが提供するデータ入力コンポーネントを 1 つずつ FormField でラップ。ラベル・ヘルパー・必須マークを付与し、aria-* は自動配線される。 テキスト系 · Input / SearchInput / PasswordInput / Textarea 文字列・検索・パスワード・複数行。type=email / inputMode=numeric も Input で表現する。
setText(e.target.value)} placeholder="2026年1月度 請求書" /> {}} placeholder="社名・コードで検索…" />