{
  "componentId": "grid-pattern",
  "componentName": "Grid",
  "lastReviewed": "2026-05-04",
  "keyboardWalk": [
    {
      "keys": "Tab",
      "expected": "Focus enters the grid at the active cell (the one carrying `tabindex=\"0\"`). On first entry, the active cell is the first data cell or the most-recently- focused cell from the prior visit. Tab again exits the grid to the next document focusable — Tab does NOT navigate between cells."
    },
    {
      "keys": "Shift+Tab",
      "expected": "Reverse focus — Shift+Tab from inside the grid exits to the previous document focusable. Shift+Tab from outside the grid enters at the active cell from the end of the document."
    },
    {
      "keys": "ArrowUp / ArrowDown",
      "expected": "Moves focus to the cell in the previous / next row, same column. The roving-tabindex shifts — the leaving cell drops to `tabindex=\"-1\"`, the entering cell gains `tabindex=\"0\"`. Wraps at boundaries per APG (or stops; document the canonical choice — wrapping is more common in spreadsheet-style grids, stopping in data-display grids)."
    },
    {
      "keys": "ArrowLeft / ArrowRight",
      "expected": "Moves focus to the previous / next cell in the same row. Same roving-tabindex shift as vertical navigation."
    },
    {
      "keys": "Home / End",
      "expected": "Home moves focus to the first cell in the current row; End to the last cell in the current row. Standard APG grid keyboard model."
    },
    {
      "keys": "Ctrl+Home / Ctrl+End",
      "expected": "Ctrl+Home moves focus to the first cell of the entire grid (typically row 1, column 1); Ctrl+End to the last cell. For virtualized grids, the virtualizer scrolls the target into view before focus lands."
    },
    {
      "keys": "Page Up / Page Down",
      "expected": "Moves focus by a viewport's worth of rows (the visible-row-count at the current size). For virtualized grids, the virtualizer scrolls accordingly."
    },
    {
      "keys": "F2 (focus on editable cell)",
      "expected": "Editable variant only. Enters edit-mode — the cell-editor mounts, focus moves from the cell to the editor. Static variant: F2 is a no-op (or focuses the first interactive descendant if any)."
    },
    {
      "keys": "Enter (focus on editable cell)",
      "expected": "Same as F2 — enters edit-mode. From inside the cell-editor, Enter commits the new value and exits edit-mode; focus returns to the cell with the new value. From inside a `<textarea>` cell-editor, Enter inserts a newline instead — Ctrl+Enter or Tab commits."
    },
    {
      "keys": "Escape (focus inside cell-editor)",
      "expected": "Cancels the edit. The cell's value reverts to its pre-edit state; the cell-editor unmounts; focus returns to the cell."
    },
    {
      "keys": "Tab (focus inside cell-editor)",
      "expected": "Commits the new value AND advances focus to the next cell (or to the next document focusable if the active cell is the last in the grid). The commit-on-Tab behaviour mirrors spreadsheet-style edit lifecycle."
    },
    {
      "keys": "Space (focus on cell)",
      "expected": "Toggles cell selection when cell-level selection is supported. For row-level selection grids, Space on a focused cell toggles the row's selection. `aria-selected` flips; selectionChange fires."
    },
    {
      "keys": "Shift+Arrow (focus on cell)",
      "expected": "Extends the selection rectangle in the direction of the arrow. Multi- cell rectangular selection semantic. The selection-overlay visual updates; every cell in the rectangle gets `aria-selected=\"true\"`."
    }
  ],
  "announcements": [
    {
      "trigger": "SR enters the grid",
      "expected": "SR announces \"grid, ${captionText}, ${rowCount} rows, ${columnCount} columns\" using the caption text plus `aria-rowcount` / `aria-colcount` for virtualized grids. The user can navigate from there."
    },
    {
      "trigger": "SR navigates to a cell",
      "expected": "SR announces \"${columnHeader}, ${rowHeader}, ${cellValue}, row ${rowIndex} of ${rowCount}, column ${colIndex} of ${colCount}\". Header context comes from `role=\"columnheader\"` / `role=\"rowheader\"` association; the row / column index from `aria-rowindex` / `aria-colindex`."
    },
    {
      "trigger": "User enters edit-mode (editable variant)",
      "expected": "SR announces the cell-editor's role and accessible name — \"edit, text input, Quantity, 12\" (the editor is a `<input type=\"number\">` with the column-header- derived label). Focus inside the editor; SR continues with the editor's keyboard model."
    },
    {
      "trigger": "User commits an edit (Enter or Tab from editor)",
      "expected": "Edit-mode exits; focus returns to the cell with the new value. SR announces the cell's updated content via the focus-change announcement. Some SR / browser combinations also announce the commit explicitly via the cell-editor's `aria-live` confirmation if authored."
    },
    {
      "trigger": "User cancels an edit (Escape)",
      "expected": "Edit-mode exits; focus returns to the cell with the original value preserved. SR announces the cell's content (the pre-edit value) via focus-change."
    },
    {
      "trigger": "User toggles cell selection (Space)",
      "expected": "SR announces the new state — \"selected\" or \"not selected\". `aria-selected` flips on the cell or row; the visual treatment updates."
    }
  ],
  "axeRules": [
    "aria-allowed-attr",
    "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"
  ],
  "_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."
}