{
  "componentId": "pagination",
  "componentName": "Pagination",
  "lastReviewed": "2026-05-04",
  "keyboardWalk": [
    {
      "keys": "Tab",
      "expected": "Focus moves through the pagination items in DOM order — first-button (if present), previous- button, page-buttons (with current-marker included as a tab-stop unless rendered as non-interactive span), next-button, last-button (if present), page-size-selector (if present). Disabled buttons retain their tab-stop position; focus lands on them and SR announces \"dimmed\"."
    },
    {
      "keys": "Shift+Tab",
      "expected": "Reverse focus through the same items. Standard tab-order — no pagination-specific behaviour."
    },
    {
      "keys": "Enter / Space (focus on page-button / previous / next)",
      "expected": "Activates the button. For `<a href>` page- buttons, Enter navigates (Space does nothing — anchors do not respond to Space by spec). For `<button>`, both Enter and Space activate. pageChange fires."
    },
    {
      "keys": "Enter / Space (focus on disabled button)",
      "expected": "No-op. The element is in DOM with `aria-disabled=\"true\"` so AT announces \"unavailable\"; activation is suppressed via `event.preventDefault` in the handler. Tab-order remains stable."
    },
    {
      "keys": "Enter / Space (focus on load-more-button, load-more variant)",
      "expected": "Activates the load-more action. `aria-busy=\"true\"` while fetching; on resolve, focus moves to the first newly-appended row OR an `aria-live` region announces the delta. Without one of these signals, keyboard users are stranded."
    }
  ],
  "announcements": [
    {
      "trigger": "SR enters the pagination landmark",
      "expected": "SR announces \"navigation, Pagination\" (the landmark plus its label). For the numbered variant, the announcement continues with \"list, N items\" (the items-list) and the user can navigate from there."
    },
    {
      "trigger": "SR encounters the current-marker",
      "expected": "SR announces \"current page, Page 3\" — the `aria-current=\"page\"` semantic plus the accessible name. Distinct from page-buttons which announce \"Page 4, link\"."
    },
    {
      "trigger": "SR encounters a disabled previous / next",
      "expected": "SR announces \"Previous, dimmed\" or \"unavailable\". The element retains its position in the announcement stream so users learn the pattern stays put."
    },
    {
      "trigger": "User activates a page-button (server-paginated)",
      "expected": "Server fetch begins. `aria-busy=\"true\"` on the nav-root or on the data surface; SR may announce \"loading\" via the live-region. On resolve, the new page renders and `aria-current=\"page\"` moves to the activated button. Focus stays on the activated button so users can continue navigation without re-finding their place."
    },
    {
      "trigger": "User activates load-more-button (load-more variant)",
      "expected": "SR announces \"loading\" (via aria-busy or live- region). On resolve, an aria-live region announces the row-count delta (\"20 more results loaded, 60 total\"). Focus moves to the first newly-appended row OR stays on the load-more button (per the implementation's chosen pattern; both are canonical, document the choice)."
    }
  ],
  "axeRules": [
    "aria-allowed-attr",
    "aria-required-attr",
    "aria-valid-attr-value",
    "landmark-unique",
    "link-name",
    "button-name",
    "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."
}