{
  "componentId": "table",
  "componentName": "Table",
  "lastReviewed": "2026-05-04",
  "keyboardWalk": [
    {
      "keys": "Tab",
      "expected": "Focus moves into the scrollable-region (if the table overflows). Subsequent Tab moves through each interactive descendant in DOM order — sort-buttons in column headers, then row checkboxes, then any in-cell links / buttons, then pagination controls. The table cells themselves are NOT in the tab order; only their interactive contents are."
    },
    {
      "keys": "Shift+Tab",
      "expected": "Reverse focus through the same interactive descendants. Standard tab-order — no table-specific behaviour."
    },
    {
      "keys": "Arrow keys (focus on scrollable-region)",
      "expected": "Scroll the region horizontally / vertically. The region's `tabindex=\"0\"` plus implicit keyboard-scroll behaviour drives this. Distinct from the APG `grid` pattern where arrows navigate cells; the canonical Table pattern does not move focus between cells."
    },
    {
      "keys": "Space (focus on row-checkbox-cell)",
      "expected": "Toggles the row's selection state. `selected` flips on the checkbox; `selectedRowIds` updates; selectionChange fires. The header checkbox's indeterminate state recomputes based on the new selection."
    },
    {
      "keys": "Space (focus on row-checkbox-header)",
      "expected": "Toggles the bulk-selection state. Cycles mixed → all-selected → none-selected per canonical contract. selectionChange fires with the new full selection set."
    },
    {
      "keys": "Enter / Space (focus on sort-button)",
      "expected": "Cycles the column's sort direction (none → ascending → descending → none, or ascending ↔ descending for two-state sorts). The parent `<th>`'s `aria-sort` updates; sortChange fires."
    }
  ],
  "announcements": [
    {
      "trigger": "SR enters the scrollable-region (when present)",
      "expected": "SR announces \"region, ${captionText}\" so users identify the scrollable area. Followed by the table's own announcement on focus into a cell."
    },
    {
      "trigger": "SR enters the table",
      "expected": "SR announces \"table, ${captionText}, ${rowCount} rows, ${columnCount} columns\" using the `<caption>` text plus the `aria-rowcount` and column-derived counts. For virtualized tables, `aria-rowcount` provides the true total."
    },
    {
      "trigger": "SR navigates to a data cell",
      "expected": "SR announces \"${columnHeader}, ${cellValue}, row ${rowIndex} of ${rowCount}\". Column header comes from the `<th scope=\"col\">` association; row index from `aria-rowindex` or the implicit DOM position."
    },
    {
      "trigger": "User activates a sort-button",
      "expected": "SR announces the sort direction change (\"ascending\" / \"descending\"). The activated button's accessible name plus the `aria-sort` update on the parent `<th>` produce the announcement on focus return."
    },
    {
      "trigger": "User toggles row-checkbox-header",
      "expected": "SR announces the new state (\"checked\", \"not checked\", \"mixed\"). When transitioning from mixed to all-selected, SR may announce the count of newly-selected rows (\"47 rows selected\") via an `aria-live` region or the implicit checkbox-state announcement."
    }
  ],
  "axeRules": [
    "aria-required-attr",
    "aria-required-children",
    "aria-required-parent",
    "aria-valid-attr-value",
    "color-contrast",
    "scope-attr-valid",
    "td-headers-attr",
    "th-has-data-cells",
    "empty-table-header",
    "table-fake-caption"
  ],
  "_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."
}