{
  "componentId": "radio-group",
  "componentName": "Radio Group",
  "lastReviewed": "2026-05-03",
  "keyboardWalk": [
    {
      "keys": "Tab (entering the group)",
      "expected": "Focus lands on the checked radio. If no radio is checked, focus lands on the first radio in DOM order. Subsequent Tab presses exit the group as a single tab stop — Tab does NOT move between radios."
    },
    {
      "keys": "ArrowDown / ArrowRight (focus inside group)",
      "expected": "Moves focus to the next radio AND selects it (auto-selection per APG canonical). Wraps from last to first. ArrowDown / ArrowRight equivalent for vertical / horizontal orientation."
    },
    {
      "keys": "ArrowUp / ArrowLeft (focus inside group)",
      "expected": "Moves focus to the previous radio AND selects it. Wraps from first to last."
    },
    {
      "keys": "Space (focus on unchecked radio)",
      "expected": "Selects the focused radio. Used when the user has moved into the group via Tab and needs to confirm selection without triggering arrow-key auto-select."
    },
    {
      "keys": "Tab (exiting the group)",
      "expected": "Moves focus to the next focusable outside the group. The radio set behaves as a single tab stop."
    }
  ],
  "announcements": [
    {
      "trigger": "SR enters the radiogroup",
      "expected": "Announces \"group, [legend text]\" on group entry, then the focused radio's role + position + label + state — \"radio button, 1 of 3, Email, selected\" or \"not selected\". The 1-of-N position counter aids orientation."
    },
    {
      "trigger": "SR encounters arrow-key navigation",
      "expected": "Announces the new selection plus state — \"radio button, 2 of 3, Phone, selected\". The previously- selected radio is implicitly unchecked; SR may or may not announce the deselection (varies by SR)."
    },
    {
      "trigger": "SR encounters group with validation error",
      "expected": "Announces \"group, [legend], invalid entry, [error message]\" via aria-invalid + aria-describedby on the root. Per-radio state continues to announce normally."
    }
  ],
  "axeRules": [
    "aria-allowed-attr",
    "aria-required-attr",
    "aria-valid-attr-value",
    "label",
    "color-contrast"
  ],
  "_about": "Per-component a11y-acceptance data shaped for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe.\n\nSuggested wiring:\n- axeRules → pass to AxeBuilder.options({ runOnly: { type: \"rule\", values: axeRules } }) so the run targets only the rules the canon has pinned for this component (other rules can run in your global pass).\n- keyboardWalk → iterate the entries; each `keys` is a human-readable sequence (e.g. \"Tab → Tab → Esc\"). Translate to page.keyboard.press calls and assert `expected` against the result (focused element, aria-state, visible text, etc.).\n- announcements → assert text content of any aria-live region or capture the accessibility tree at the trigger moment and match against `expected`.\n\nEmpty sub-arrays mean the canon does not yet pin behaviour for that axis on this component, not that none is required."
}