# yaml-language-server: $schema=https://opencomponents.dev/schemas/contract.json
# The Textarea contract, with every rule from its checklist.
# The page explains why each one exists: https://opencomponents.dev/raw/docs/components/textarea.md

component: Textarea
summary: Takes text over several lines, like a comment, a message or a description. Its label names it.
standard: https://opencomponents.dev/docs/components/textarea
element: textarea # inside a div, which holds the count and takes class and style
role: textbox # multi-line, which the <textarea> implies
props:
  size: { type: [xs, sm, md, lg, xl], default: md }
  rows: { type: number, default: 3 } # the lines it shows, and is never shorter than
  autoGrow: { type: boolean, default: false } # grows with its text, and can't be resized
  maxRows: { type: number } # with autoGrow. Without it, it keeps growing
  limit: { type: number } # shows a count, and never cuts people off
  countLabel: { type: "(remaining: number) => string" } # negative over the limit
  disabled: { type: boolean, default: false }
  value: { type: string } # when it's bound. Without it, the textarea keeps its own
events:
  input: Event, from the <textarea>. Fires as people type.
  change: Event, from the <textarea>. Fires when people commit a change, like when they leave it.
states:
  hover: ":hover, in @media (hover: hover)"
  focus: ":focus-visible"
  disabled: ":disabled"
  read-only: "[readonly]"
  invalid: "[aria-invalid=true], never :invalid. Set by the Textarea over its limit too"
  over-limit: "[data-slot=count][data-over-limit]"
keyboard:
  Tab: Moves focus to and from the textarea.
  Enter: Starts a new line. Never submits its form.
parts: [control, count] # data-slot, in this order. The control is the <textarea>
tokens:
  theme: ["--color--neutral-text", "--color--error-text", "--color--focus"]
  variables: [--textarea--height, --textarea--padding, --textarea--padding-block, --textarea--font-size, --textarea--radius, --textarea--border-color, --textarea--min-height, --textarea--max-height, --textarea--count--color, --textarea--rows, --textarea--max-rows]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: textarea/contrast
    layer: ui
    level: must
    scope: component
    requirement: "The text, the placeholder and the count have a contrast of 4.5:1, and the focus ring has 3:1 against the page"
    basis: "WCAG 1.4.3, 1.4.11 (AA)"
    check: "axe `color-contrast`, contrast checker"
  - id: textarea/boundary-contrast
    layer: ui
    level: must
    scope: component
    requirement: "The border has a contrast of 3:1 against the page, so an empty textarea stays visible"
    basis: "WCAG 1.4.11 (AA)"
    check: "Contrast checker"
  - id: textarea/focus-visible
    layer: ui
    level: must
    scope: component
    requirement: "Focus shows an `outline` at least 2px thick around the field, outside its border"
    basis: "WCAG 2.4.7 (AA), 2.4.13 (AAA)"
    check: "Keyboard"
  - id: textarea/target-size
    layer: ui
    level: must
    scope: component
    requirement: "The field is at least 24px tall, and at least 44px tall on touch screens"
    basis: "WCAG 2.5.8 (AA), 2.5.5 (AAA)"
    check: "Emulation"
  - id: textarea/no-zoom-on-focus
    layer: ui
    level: should
    scope: component
    requirement: "On touch screens, the text is at least 16px, so Safari on iOS doesn't zoom in when the textarea gets focus"
    basis: "Open Components"
    check: "Emulation"
  - id: textarea/min-rows
    layer: ui
    level: must
    scope: component
    requirement: "It's never shorter than its `rows`, whether people resize it or it grows on its own"
    basis: "Open Components"
    check: "Visual regression test"
  - id: textarea/resizable
    layer: ui
    level: must
    scope: component
    requirement: "Its height is a minimum, in lines of its own line height plus padding in `rem`, so it grows with its text instead of clipping it"
    basis: "Open Components, beyond WCAG 1.4.4, 1.4.12 (AA)"
    check: "200% zoom"
  - id: textarea/block-resize
    layer: ui
    level: should
    scope: component
    requirement: "People resize it on the block axis only, and resizing is only turned off when it grows on its own or is disabled"
    basis: "Open Components"
    check: "Visual regression test"
  - id: textarea/auto-grow
    layer: ui
    level: should
    scope: component
    requirement: "With `autoGrow`, it grows with its text up to `maxRows`, then scrolls, with `field-sizing: content` or a script where that's not supported"
    basis: "Open Components"
    check: "Visual regression test, 200% zoom"
  - id: textarea/stable-size
    layer: ui
    level: must
    scope: component
    requirement: "No state changes the field's size or position"
    basis: "Open Components"
    check: "Visual regression test"
  - id: textarea/state-selectors
    layer: ui
    level: should
    scope: component
    requirement: "States are styled from the attributes on the `<textarea>` that expose them, and never from `:invalid` or `:user-invalid`"
    basis: "Open Components"
    check: "Review"
  - id: textarea/hover-capable
    layer: ui
    level: should
    scope: component
    requirement: "Hover styles only apply inside `@media (hover: hover)`"
    basis: "Open Components"
    check: "Review"
  - id: textarea/forced-colors
    layer: ui
    level: must
    scope: component
    requirement: "The border, and the focus, disabled and read-only states, stay visible in forced colours mode"
    basis: "Open Components"
    check: "Emulation"
  - id: textarea/more-contrast
    layer: ui
    level: should
    scope: component
    requirement: "The border is drawn at full strength when `prefers-contrast: more` is set"
    basis: "Open Components"
    check: "Emulation"
  - id: textarea/fits-text
    layer: ui
    level: should
    scope: usage
    requirement: "Its `rows` fit the text it expects, and it's never wider than its column"
    basis: "Open Components, beyond WCAG 1.4.10 (AA)"
    check: "Review"
  - id: textarea/native-element
    layer: ux
    level: must
    scope: component
    requirement: "It's a native `<textarea>`, and never a `contenteditable` element or a `role=\"textbox\"`"
    basis: "Open Components, beyond WCAG 2.1.1, 4.1.2 (A)"
    check: "Unit test"
  - id: textarea/visible-label
    layer: ux
    level: must
    scope: usage
    requirement: "Every textarea has a visible `<label>`, linked with `for`, rather than a placeholder alone"
    basis: "WCAG 1.3.1, 3.3.2, 4.1.2 (A)"
    check: "axe `label`"
  - id: textarea/label-in-name
    layer: ux
    level: must
    scope: usage
    requirement: "An `aria-label` or `aria-labelledby` includes the visible label, ideally at the start"
    basis: "WCAG 2.5.3 (A)"
    check: "axe `label-content-name-mismatch`"
  - id: textarea/described
    layer: ux
    level: should
    scope: usage
    requirement: "Hints, like what to include, are linked with `aria-describedby`, and a placeholder only ever shows a short example"
    basis: "WCAG 1.3.1 (A), Open Components"
    check: "Screen reader"
  - id: textarea/keyboard
    layer: ux
    level: must
    scope: component
    requirement: "`Tab` moves focus to and from it, and the browser's text editing keys work as they do in any textarea"
    basis: "WCAG 2.1.1, 2.1.2 (A)"
    check: "Unit test"
  - id: textarea/enter-new-line
    layer: ux
    level: must
    scope: both
    requirement: "`Enter` starts a new line and never submits the form, which has a submit button"
    basis: "HTML"
    check: "Unit test"
  - id: textarea/allow-paste
    layer: ux
    level: must
    scope: both
    requirement: "Pasting and autofill are never blocked"
    basis: "Open Components"
    check: "Unit test, review"
  - id: textarea/no-truncation
    layer: ux
    level: must
    scope: both
    requirement: "A limit is counted rather than enforced with `maxlength`, so people can type and paste past it"
    basis: "Open Components"
    check: "Unit test"
  - id: textarea/limit-in-hint
    layer: ux
    level: should
    scope: usage
    requirement: "A limit is stated in the hint, linked with `aria-describedby`, before people start writing"
    basis: "WCAG 3.3.2 (A)"
    check: "Screen reader"
  - id: textarea/count
    layer: ux
    level: should
    scope: component
    requirement: "The count says how many characters are left, or how many too many, describes the textarea, and over the limit sets `aria-invalid=\"true\"`"
    basis: "WCAG 3.3.1 (A), Open Components"
    check: "Unit test"
  - id: textarea/announce-count
    layer: ux
    level: should
    scope: component
    requirement: "The count is announced through one polite live region once people pause near the limit, and never on every keystroke"
    basis: "WCAG 4.1.3 (AA), Open Components"
    check: "Unit test, screen reader"
  - id: textarea/input-purpose
    layer: ux
    level: must
    scope: usage
    requirement: "A textarea that asks for the person's own address has `autocomplete=\"street-address\"`"
    basis: "WCAG 1.3.5 (AA)"
    check: "axe `autocomplete-valid`, review"
  - id: textarea/error-message
    layer: ux
    level: must
    scope: usage
    requirement: "An invalid textarea has an error message, in text and linked with `aria-describedby`, that says what's wrong and how to fix it"
    basis: "WCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA)"
    check: "Screen reader"
  - id: textarea/invalid-state
    layer: ux
    level: must
    scope: both
    requirement: "A textarea sets `aria-invalid=\"true\"` while its error message shows, and only looks invalid when it does"
    basis: "WCAG 4.1.2 (A), Open Components"
    check: "Unit test"
  - id: textarea/validate-late
    layer: ux
    level: should
    scope: usage
    requirement: "Errors show when people submit or leave the textarea, never while they're first writing, and clear as soon as the text is fixed"
    basis: "Open Components"
    check: "Review"
  - id: textarea/focus-first-error
    layer: ux
    level: should
    scope: usage
    requirement: "When a submit fails, focus moves to the first invalid field, or to a summary of the errors, once the error messages are in the page"
    basis: "Open Components"
    check: "Keyboard, screen reader"
  - id: textarea/disabled-inert
    layer: ux
    level: must
    scope: component
    requirement: "A disabled textarea can't be focused, edited or submitted"
    basis: "HTML"
    check: "Unit test"
  - id: textarea/read-only-values
    layer: ux
    level: should
    scope: usage
    requirement: "Text people need to read but can't change is read-only rather than disabled, so they can focus it, scroll it and copy it, and its form sends it"
    basis: "Open Components"
    check: "Review"
  - id: textarea/keep-focus
    layer: ux
    level: must
    scope: usage
    requirement: "A textarea is never disabled while it has focus, so while its form is sent, it's read-only instead"
    basis: "Open Components"
    check: "Keyboard"
  - id: textarea/focus-not-obscured
    layer: ux
    level: must
    scope: usage
    requirement: "Sticky headers and footers never cover a focused textarea, for example thanks to `scroll-padding`"
    basis: "WCAG 2.4.11 (AA), 2.4.12 (AAA)"
    check: "Keyboard"
  - id: textarea/mirrors-in-rtl
    layer: ux
    level: should
    scope: both
    requirement: "It uses logical properties, and a textarea for free text has `dir=\"auto\"`"
    basis: "Open Components"
    check: "Review"
  - id: textarea/shared-vocabulary
    layer: dx
    level: must
    scope: component
    requirement: "It uses the shared names `size`, `disabled` and `value`, with the Input's sizes"
    basis: "Open Components"
    check: "Review"
  - id: textarea/typed
    layer: dx
    level: must
    scope: component
    requirement: "Props and events are typed, with unions rather than `string`, and `rows`, `maxRows` and `limit` take numbers"
    basis: "Open Components"
    check: "Type check"
  - id: textarea/control-attributes
    layer: dx
    level: must
    scope: component
    requirement: "Attributes, listeners and refs land on the `<textarea>`, while classes and styles land on the root"
    basis: "Open Components"
    check: "Unit test"
  - id: textarea/form-control
    layer: dx
    level: must
    scope: component
    requirement: "Its form sends its text under its `name`, whether the value is bound or not"
    basis: "HTML"
    check: "Unit test"
  - id: textarea/controllable
    layer: dx
    level: should
    scope: component
    requirement: "Its value can be bound or left to the browser, and every other state is a prop or an attribute"
    basis: "Open Components"
    check: "Unit test"
  - id: textarea/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found by its role and label alone, with `getByRole(\"textbox\", { name })`"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: textarea/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup, with no generated IDs, and states change attributes, never elements"
    basis: "Open Components"
    check: "Unit test"
  - id: textarea/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "The size, `rows`, `autoGrow`, `maxRows` and `limit` are mirrored as `data-size`, `data-rows`, `data-auto-grow`, `data-max-rows` and `data-limit`, with `data-max-rows` only alongside `autoGrow`, and parts are marked with `data-slot`"
    basis: "Open Components"
    check: "Unit test"
  - id: textarea/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when a textarea has no label, or a limit without an `id`"
    basis: "Open Components"
    check: "Unit test"
