{
  "componentId": "tree-grid",
  "componentName": "Tree Grid",
  "lastReviewed": "2026-05-04",
  "keyboardWalk": [
    {
      "keys": "Tab",
      "expected": "Focus enters the treegrid at the active cell (the one carrying `tabindex=\"0\"`). Inherits Grid's Tab behaviour. Tab again exits the treegrid to the next document focusable."
    },
    {
      "keys": "ArrowUp / ArrowDown",
      "expected": "Moves focus to the cell in the previous / next row, same column. Skips collapsed children — the navigation respects the currently-visible row set, not the underlying total. The roving-tabindex shifts."
    },
    {
      "keys": "ArrowLeft (focus on cell in expanded parent row)",
      "expected": "Collapses the parent row. `aria-expanded` flips to false; child rows hide (display:none or DOM-unmount per implementation); focus stays on the same cell. expandChange fires."
    },
    {
      "keys": "ArrowLeft (focus on cell in collapsed parent row OR leaf row)",
      "expected": "For collapsed parent or leaf row at depth ≥ 2, moves focus to the parent row's same column. The treegrid's ancestor-traversal contract. For root- level rows (depth 1), no-op."
    },
    {
      "keys": "ArrowRight (focus on cell in collapsed parent row)",
      "expected": "Expands the parent row. `aria-expanded` flips to true; child rows render; focus stays on the same cell. expandChange fires."
    },
    {
      "keys": "ArrowRight (focus on cell in expanded parent row)",
      "expected": "Moves focus to the first child row's same column. The expansion is already-true; ArrowRight advances into the children. Roving-tabindex shifts."
    },
    {
      "keys": "ArrowRight (focus on cell in leaf row)",
      "expected": "For leaf rows or rows where ArrowRight is not directionally meaningful, no-op OR moves to the next column (per implementation choice — APG documents both as acceptable)."
    },
    {
      "keys": "* (asterisk, focus on row)",
      "expected": "Expands all sibling parent rows at the current row's level. Useful for tree-wide expansion in deeply-nested structures. APG canonical shortcut."
    },
    {
      "keys": "Home / End",
      "expected": "Home moves focus to the first cell in the current row; End to the last cell. Inherits Grid's Home / End."
    },
    {
      "keys": "Ctrl+Home / Ctrl+End",
      "expected": "Ctrl+Home moves focus to the first cell of the entire treegrid (typically first column of root-level row 1); Ctrl+End to the last cell of the currently-expanded row set."
    },
    {
      "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. Inherits Grid's edit-mode lifecycle."
    },
    {
      "keys": "Enter / Escape (focus inside cell-editor)",
      "expected": "Enter commits + exits edit-mode; Escape cancels + reverts. Inherits Grid's edit-mode contract."
    },
    {
      "keys": "Space (focus on cell)",
      "expected": "Toggles cell or row selection per the multiSelect property. Inherits Grid's selection contract."
    }
  ],
  "announcements": [
    {
      "trigger": "SR enters the treegrid",
      "expected": "SR announces \"tree grid, ${captionText}, ${rowCount} rows, ${columnCount} columns\" using the caption text plus `aria-rowcount` for virtualized treegrids. Some SR / browser combinations announce \"treegrid\" explicitly, others announce \"grid\" (older AT). Both are acceptable per APG."
    },
    {
      "trigger": "SR navigates to a row",
      "expected": "SR announces \"row, level ${level}, ${expansionState if applicable}, ${posinset} of ${setsize}, ${cellValues}\" — level from `aria-level`, expansion from `aria-expanded` (omitted on leaf rows), set position from `aria-setsize` / `aria-posinset`."
    },
    {
      "trigger": "User expands a row (ArrowRight or click)",
      "expected": "SR announces \"expanded\" via the `aria-expanded` change. Some SR / browser combinations also announce the count of newly-visible child rows (\"3 rows added\") via implicit live-region behaviour or explicit `aria-live` authoring."
    },
    {
      "trigger": "User collapses a row (ArrowLeft)",
      "expected": "SR announces \"collapsed\" via the `aria-expanded` change. The previously- visible children hide; the next row in the navigation order is the sibling-or-parent of the collapsed row."
    },
    {
      "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 focus- change. Inherits Grid's commit announcement."
    }
  ],
  "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."
}