{
  "componentId": "menu",
  "componentName": "Menu",
  "lastReviewed": "2026-05-04",
  "keyboardWalk": [
    {
      "keys": "Tab",
      "expected": "Focus enters the menu at the first menuitem (or the most-recently-focused item from the prior visit). Tab inside the menu typically closes the menu (or moves focus out, depending on context — for context menus Tab closes; for embedded menus Tab moves to the next document focusable while keeping the menu visible)."
    },
    {
      "keys": "ArrowDown / ArrowUp (vertical orientation)",
      "expected": "Moves focus to the next / previous menuitem, skipping separators. Wraps at boundaries per `wrapNavigation` property (default true). The roving-tabindex shifts."
    },
    {
      "keys": "ArrowLeft / ArrowRight (horizontal orientation)",
      "expected": "Same as ArrowDown / ArrowUp for horizontal menus. For horizontal menus with submenus, ArrowDown opens a vertical submenu beneath the focused item."
    },
    {
      "keys": "ArrowRight (focused menuitem with submenu)",
      "expected": "For vertical menus, opens the submenu and moves focus to the first item in the submenu. The submenu-trigger's `aria-expanded` flips to `true`. APG- canonical submenu-open keyboard."
    },
    {
      "keys": "ArrowLeft (focused item in submenu)",
      "expected": "Closes the submenu and returns focus to the submenu-trigger in the parent menu. For deeply nested submenus, ArrowLeft closes only the current submenu, not the entire chain."
    },
    {
      "keys": "Enter / Space (focused menuitem)",
      "expected": "Activates the menuitem — fires the itemActivate event. For action menuitems, the menu typically closes (per `closeOnSelect`). For menuitemcheckbox, toggles `aria-checked` without closing. For menuitemradio, sets the new selection within the radio group."
    },
    {
      "keys": "Home / End",
      "expected": "Home moves focus to the first menuitem; End to the last. The roving-tabindex shifts."
    },
    {
      "keys": "Printable character (a–z, 0–9)",
      "expected": "Typeahead — the character buffers (within ~500ms of inactivity) and focus jumps to the next menuitem whose accessible name starts with the buffered string. Multi- character buffers (\"se\") match accessible names starting with \"se\" before single- character matches."
    },
    {
      "keys": "Escape",
      "expected": "Closes the current menu (or current submenu, returning focus to the parent's submenu-trigger). For top-level menus, Escape closes the menu and returns focus to the trigger element (or the previously- focused element for context menus triggered by right-click)."
    }
  ],
  "announcements": [
    {
      "trigger": "SR enters the menu",
      "expected": "SR announces \"menu, ${label}, ${itemCount} items\" on entry — label from `aria-label` or `aria-labelledby`. The first menuitem then announces its own accessible name plus role (\"menuitem, Save\")."
    },
    {
      "trigger": "SR navigates to a menuitemcheckbox",
      "expected": "SR announces \"${label}, menu item check box, ${checked|not checked|mixed}\". The `aria-checked` value drives the state announcement."
    },
    {
      "trigger": "SR navigates to a menuitemradio",
      "expected": "SR announces \"${label}, menu item radio, ${checked|not checked}, ${groupLabel}\". The group's `aria-labelledby` provides the group context."
    },
    {
      "trigger": "SR navigates to a submenu-trigger",
      "expected": "SR announces \"${label}, menu item, submenu, ${expanded|collapsed}\". The `aria-haspopup` plus `aria-expanded` attributes drive the announcement."
    },
    {
      "trigger": "User activates a submenu (ArrowRight)",
      "expected": "SR announces the submenu open and the first item — \"${submenuLabel}, menu, ${itemCount} items, ${firstItemLabel}, menu item\"."
    },
    {
      "trigger": "User activates a menuitemcheckbox",
      "expected": "SR announces the new state — \"checked\" or \"not checked\" — via `aria-checked` change. The menu does not close; the user can toggle multiple items in sequence."
    }
  ],
  "axeRules": [
    "aria-allowed-attr",
    "aria-allowed-role",
    "aria-required-attr",
    "aria-required-children",
    "aria-required-parent",
    "aria-valid-attr-value",
    "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."
}