{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-questionnaire-6","type":"registry:block","title":"A rule that sends you back","description":"A cross-question rule checked on submit, returning the flow to the question that broke it.","dependencies":[],"registryDependencies":["button","card","questionnaire"],"files":[{"path":"c-questionnaire-6.tsx","type":"registry:block","content":"\"use client\"\n\nimport { useId, useState, type FormEvent } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardAction,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport {\n  Questionnaire,\n  QuestionnaireActions,\n  QuestionnaireChoice,\n  QuestionnaireChoices,\n  QuestionnaireDescription,\n  QuestionnaireError,\n  QuestionnaireItem,\n  QuestionnaireNext,\n  QuestionnairePrevious,\n  QuestionnaireProgress,\n  QuestionnaireSubmit,\n  QuestionnaireTitle,\n} from \"@/components/ui/questionnaire\"\n\ntype Option = { value: string; label: string }\n\nconst REGIONS: Option[] = [\n  { value: \"us\", label: \"United States\" },\n  { value: \"eu\", label: \"European Union\" },\n  { value: \"self-managed\", label: \"A region you manage\" },\n]\n\nconst PLANS: Option[] = [\n  { value: \"starter\", label: \"Starter\" },\n  { value: \"growth\", label: \"Growth\" },\n  { value: \"enterprise\", label: \"Enterprise\" },\n]\n\nconst SIGNERS: Option[] = [\n  { value: \"me\", label: \"I sign it myself\" },\n  { value: \"legal\", label: \"Our legal team\" },\n  { value: \"admin\", label: \"A named administrator\" },\n]\n\nconst ITEMS = [\n  { name: \"region\", required: true },\n  { name: \"plan\", required: true },\n  { name: \"signer\", required: true },\n]\n\n// The one cross-question rule in this flow, written out once. No schema\n// library is involved and none is missing: a single rule over two FormData\n// reads costs less than a resolver, and the per-question required checks a\n// schema would carry are already the primitive's job.\nconst PLAN_CONFLICT =\n  \"A region you manage is available on Enterprise. Pick another plan, or change the region.\"\n\ntype Summary = { region: string; plan: string; signer: string }\n\nfunction labelOf(options: Option[], value: FormDataEntryValue | null) {\n  return (\n    options.find((option) => option.value === value)?.label ?? \"Not answered\"\n  )\n}\n\nexport function Pattern() {\n  // CardHeader sits between each fieldset and its legend, which stops the\n  // legend naming the group. Pairing these ids with aria-labelledby on each\n  // item puts that name back.\n  const titleId = useId()\n\n  // Controlled navigation is what makes the walk-back possible at all: item\n  // and onItemChange hand the flow's position to this component, so writing to\n  // it moves the visitor, and the root focuses whichever question it lands on.\n  const [item, setItem] = useState(\"region\")\n  const [planError, setPlanError] = useState<string | null>(null)\n  const [summary, setSummary] = useState<Summary | null>(null)\n\n  // Each question is already gated by the built-in required check, so nothing\n  // here re-implements that. This rule spans two answers and cannot be judged\n  // until both exist, which is why it runs on submit and moves item by hand.\n  //\n  // Rejected: noValidate={false} switches on native constraint validation, but\n  // the browser judges one control at a time and answers with its own bubble,\n  // so it can never express a rule that reads across two questions.\n  //\n  // The root re-checks every enabled item before this handler is reached, so\n  // all three answers are known to exist by the time it runs and the only\n  // thing left to judge is the combination.\n  function handleSubmit(event: FormEvent<HTMLFormElement>) {\n    event.preventDefault()\n\n    const formData = new FormData(event.currentTarget)\n    const region = formData.get(\"region\")\n    const plan = formData.get(\"plan\")\n\n    if (region === \"self-managed\" && plan === \"starter\") {\n      setPlanError(PLAN_CONFLICT)\n      setItem(\"plan\")\n      return\n    }\n\n    setSummary({\n      region: labelOf(REGIONS, region),\n      plan: labelOf(PLANS, plan),\n      signer: labelOf(SIGNERS, formData.get(\"signer\")),\n    })\n  }\n\n  function startOver() {\n    setSummary(null)\n    setPlanError(null)\n    setItem(\"region\")\n  }\n\n  if (summary) {\n    return (\n      <Card className=\"mx-auto w-full max-w-md\">\n        <CardHeader>\n          <CardTitle>Agreement on its way</CardTitle>\n          <CardDescription>\n            The data agreement has been drafted against these three answers and\n            sent for signature.\n          </CardDescription>\n        </CardHeader>\n        <CardContent>\n          <dl className=\"grid gap-3\">\n            <div className=\"grid min-w-0 gap-0.5\">\n              <dt className=\"text-muted-foreground text-sm\">Data residency</dt>\n              <dd className=\"text-sm font-medium\">{summary.region}</dd>\n            </div>\n            <div className=\"grid min-w-0 gap-0.5\">\n              <dt className=\"text-muted-foreground text-sm\">Plan</dt>\n              <dd className=\"text-sm font-medium\">{summary.plan}</dd>\n            </div>\n            <div className=\"grid min-w-0 gap-0.5\">\n              <dt className=\"text-muted-foreground text-sm\">Signed by</dt>\n              <dd className=\"text-sm font-medium\">{summary.signer}</dd>\n            </div>\n          </dl>\n        </CardContent>\n        <CardFooter>\n          <Button size=\"sm\" variant=\"outline\" onClick={startOver}>\n            Start over\n          </Button>\n        </CardFooter>\n      </Card>\n    )\n  }\n\n  return (\n    <Questionnaire\n      className=\"mx-auto w-full max-w-md\"\n      item={item}\n      items={ITEMS}\n      onItemChange={setItem}\n      onSubmit={handleSubmit}\n    >\n      <Card>\n        <QuestionnaireItem\n          aria-labelledby={`${titleId}-region`}\n          name=\"region\"\n          required\n        >\n          <CardHeader>\n            <QuestionnaireTitle id={`${titleId}-region`} render={<CardTitle />}>\n              Where should your data live?\n            </QuestionnaireTitle>\n            <QuestionnaireDescription render={<CardDescription />}>\n              Residency is fixed once the workspace is created.\n            </QuestionnaireDescription>\n            <CardAction>\n              <QuestionnaireProgress />\n            </CardAction>\n          </CardHeader>\n          <CardContent>\n            <QuestionnaireChoices>\n              {REGIONS.map((region) => (\n                <QuestionnaireChoice\n                  key={region.value}\n                  value={region.value}\n                  // Both questions clear the message as they change, because\n                  // either answer can resolve the conflict and a stale\n                  // sentence under a corrected question is worse than none.\n                  onChange={() => setPlanError(null)}\n                >\n                  {region.label}\n                </QuestionnaireChoice>\n              ))}\n            </QuestionnaireChoices>\n            <QuestionnaireError />\n          </CardContent>\n        </QuestionnaireItem>\n\n        <QuestionnaireItem\n          aria-labelledby={`${titleId}-plan`}\n          name=\"plan\"\n          required\n          // invalid and the error children are separate on purpose: the flag\n          // drives the styling and the alert role, the children supply the\n          // sentence. Null rather than an empty string leaves the primitive's\n          // own required copy in place for the ordinary unanswered case.\n          //\n          // The flag also blocks Next while it is set, so the rule holds on the\n          // way forward and not only on the way out. The visitor cannot\n          // walk past the question that broke it and meet the same message\n          // again at the end.\n          invalid={planError !== null}\n        >\n          <CardHeader>\n            <QuestionnaireTitle id={`${titleId}-plan`} render={<CardTitle />}>\n              Which plan are you on?\n            </QuestionnaireTitle>\n            <QuestionnaireDescription render={<CardDescription />}>\n              Not every region is offered on every plan.\n            </QuestionnaireDescription>\n            <CardAction>\n              <QuestionnaireProgress />\n            </CardAction>\n          </CardHeader>\n          <CardContent>\n            <QuestionnaireChoices>\n              {PLANS.map((plan) => (\n                <QuestionnaireChoice\n                  key={plan.value}\n                  value={plan.value}\n                  onChange={() => setPlanError(null)}\n                >\n                  {plan.label}\n                </QuestionnaireChoice>\n              ))}\n            </QuestionnaireChoices>\n            <QuestionnaireError>{planError}</QuestionnaireError>\n          </CardContent>\n        </QuestionnaireItem>\n\n        <QuestionnaireItem\n          aria-labelledby={`${titleId}-signer`}\n          name=\"signer\"\n          required\n        >\n          <CardHeader>\n            <QuestionnaireTitle id={`${titleId}-signer`} render={<CardTitle />}>\n              Who signs the data agreement?\n            </QuestionnaireTitle>\n            <QuestionnaireDescription render={<CardDescription />}>\n              We send it for signature as soon as this flow is finished.\n            </QuestionnaireDescription>\n            <CardAction>\n              <QuestionnaireProgress />\n            </CardAction>\n          </CardHeader>\n          <CardContent>\n            <QuestionnaireChoices>\n              {SIGNERS.map((signer) => (\n                <QuestionnaireChoice key={signer.value} value={signer.value}>\n                  {signer.label}\n                </QuestionnaireChoice>\n              ))}\n            </QuestionnaireChoices>\n            <QuestionnaireError />\n          </CardContent>\n        </QuestionnaireItem>\n\n        <CardFooter>\n          <QuestionnaireActions className=\"w-full\">\n            <QuestionnairePrevious />\n            <QuestionnaireNext>Next</QuestionnaireNext>\n            <QuestionnaireSubmit>Request the agreement</QuestionnaireSubmit>\n          </QuestionnaireActions>\n        </CardFooter>\n      </Card>\n    </Questionnaire>\n  )\n}","target":"components/examples/c-questionnaire-6.tsx"}],"meta":{"order":6,"gridSize":2}}