import { useState } from "react"; import { Cascader, CheckboxGroup, ColorPicker, DatePicker, Form, FormField, Input, RadioGroup, SearchInput, Select, TimePicker, TreeSelect, } from "@godxjp/ui/data-entry"; import { Button } from "@godxjp/ui/general"; import { Alert, AlertDescription, AlertTitle } from "@godxjp/ui/feedback"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display"; import { Flex, PageContainer } from "@godxjp/ui/layout"; /** * FormField × custom controls — the accessible-name/description/error contract. * * Every control below is wrapped in a FormField that provides the visible label, helper and error. * FormField injects `aria-labelledby` / `aria-describedby` / `aria-errormessage` / `aria-invalid` / * `aria-required` onto the control, which FORWARDS them to its real semantic focus target — the * combobox trigger, the typeable input, or (for groups/ranges) the `role=group`/`role=radiogroup` * container. Open a screen reader or run axe on this frame: the visible label names the actual * control, not a wrapper div. Composed only from real @godxjp/ui components. */ const REGIONS = [ { value: "jp", label: "日本", children: [ { value: "kanto", label: "関東", children: [{ value: "tokyo", label: "東京都" }] }, { value: "kansai", label: "関西", children: [{ value: "osaka", label: "大阪府" }] }, ], }, ]; const CATEGORIES = [ { value: "acct", label: "会計", children: [ { value: "sales", label: "売上高" }, { value: "rent", label: "地代家賃" }, ], }, ]; const CURRENCIES = [ { value: "jpy", label: "日本円 (JPY)" }, { value: "usd", label: "米ドル (USD)" }, { value: "vnd", label: "ベトナムドン (VND)" }, ]; export default function Demo() { // Async / server validation + recovery: submit runs a fake server check, sets an error on the // "code" field and focuses it; entering a valid code and resubmitting clears the error. const [code, setCode] = useState(""); const [codeError, setCodeError] = useState(); const [status, setStatus] = useState<"idle" | "checking" | "ok">("idle"); const submit = async (event: React.FormEvent) => { event.preventDefault(); setStatus("checking"); // Simulate a server round-trip (a real app would call its API here). await new Promise((resolve) => setTimeout(resolve, 400)); if (code.trim().length < 4) { setCodeError("コードは4文字以上で、サーバー上で一意である必要があります"); setStatus("idle"); // Submit focus: move focus to the first invalid control by its FormField id. document.getElementById("ac-code")?.focus(); return; } setCodeError(undefined); setStatus("ok"); }; return ( ポップアップ系コントロール(combobox 契約) Select(showSearch 含む)/ Cascader / TreeSelect / 各ピッカーはトリガー(combobox)に aria-labelledby・aria-describedby・aria-expanded・aria-controls を転送します。 {}} /> {}} /> {}} /> {}} /> グループ系コントロール(group / radiogroup 契約) RadioGroup は role=radiogroup で完全な検証 ARIA を、CheckboxGroup は role=group で ラベルと説明(エラーは説明に統合)を公開します。 {}} /> {}} /> エラーのタイミング・非同期検証・回復 エラーは送信後にのみ表示(role=alert で即時アナウンス)。修正して再送信するとエラーが解除され、 helper に戻ります。送信時は最初の不正なコントロールへフォーカスします。
{ setCode(event.target.value); if (codeError) setCodeError(undefined); }} /> {status === "ok" ? ( 検証に成功しました コードは利用可能です。 ) : null}
); }