{
  "componentId": "textarea",
  "componentName": "Textarea",
  "lastReviewed": "2026-05-03",
  "keyboardWalk": [
    {
      "keys": "Tab",
      "expected": "Focus moves to the textarea. Focus ring renders on the field. The textarea is its own tab stop."
    },
    {
      "keys": "Enter (focus inside textarea)",
      "expected": "Inserts a newline in the value. Does NOT submit the form (unlike single-line `<input>` where Enter submits). For ctrl+Enter or cmd+Enter to submit, the consumer wires the keydown handler externally — this is convention in chat / comment surfaces but not canonical to the textarea primitive."
    },
    {
      "keys": "Shift+Tab",
      "expected": "Moves focus to the previous focusable element. The textarea does not capture Tab — Tab inserts a tab character only if the consumer explicitly opts in (rare; generally Tab moves focus per HTML default)."
    },
    {
      "keys": "Resize handle drag (mouse)",
      "expected": "When `resize: vertical | horizontal | both`, the bottom-end corner shows a resize affordance. Drag adjusts the textarea height (and width if applicable). Keyboard users have no equivalent canonical mechanism — auto-resize variants make this point moot by growing automatically with content."
    }
  ],
  "announcements": [
    {
      "trigger": "SR encounters an empty textarea",
      "expected": "\"[label text], edit text, multi-line, blank\" or the SR-equivalent. The role (textbox), the multi-line property, and the empty state all announce."
    },
    {
      "trigger": "SR encounters a filled textarea",
      "expected": "\"[label text], edit text, multi-line, [first line of content]\" — most SR read the first line plus position; users navigate with Down arrow to read further lines."
    },
    {
      "trigger": "User types and reaches the character-count threshold",
      "expected": "Character-counter announces via `aria-live=\"polite\"` on typing-pause — \"20 characters remaining\" or \"5 characters over the limit\". The textarea's `aria-invalid` flips when the limit is crossed."
    },
    {
      "trigger": "SR encounters a textarea with validation error after submit",
      "expected": "\"[label text], edit text, multi-line, invalid entry, [error message]\". The error-message announces via `aria-describedby` plus `aria-invalid`."
    }
  ],
  "axeRules": [
    "aria-allowed-attr",
    "aria-required-attr",
    "label",
    "color-contrast",
    "autocomplete-valid"
  ],
  "_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."
}