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

component: Input
summary: Takes a single line of text, like a name, an email address or a search. Its label names it.
standard: https://opencomponents.dev/docs/components/input
element: input # inside a div, which draws the field and takes class and style
role: textbox # searchbox, with type="search". ARIA has none for type="password"
props:
  size: { type: [xs, sm, md, lg, xl], default: md }
  type: { type: [text, email, password, search, tel, url], default: text }
  disabled: { type: boolean, default: false }
  value: { type: string } # when it's bound. Without it, the input keeps its own
slots:
  leading: An icon or a prefix before the value, hidden from assistive technologies.
  trailing: An icon or a suffix after the value, hidden from assistive technologies.
  actions: Buttons that act on the value, which Tab reaches after the input.
events:
  input: Event, from the <input>. Fires as people type.
  change: Event, from the <input>. Fires when people commit a change, like when they leave it.
states:
  hover: ":hover, in @media (hover: hover)"
  focus: ":has(> input:focus-visible)"
  disabled: ":has(> input:disabled)"
  read-only: ":has(> input[readonly])"
  invalid: ":has(> input[aria-invalid=true]), never :invalid"
keyboard:
  Tab: Moves focus to the input, then to its actions.
  Enter: Submits its form.
parts: [leading, control, trailing, actions] # data-slot, in this order. The control is the <input>
tokens:
  theme: ["--color--neutral-text", "--color--error-text", "--color--focus"]
  variables: [--input--height, --input--padding, --input--gap, --input--font-size, --input--icon--size, --input--radius, --input--border-color]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: input/contrast
    layer: ui
    level: must
    scope: component
    requirement: "The value, the placeholder, prefixes and suffixes 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: input/boundary-contrast
    layer: ui
    level: must
    scope: component
    requirement: "The border has a contrast of 3:1 against the page, so an empty input stays visible"
    basis: "WCAG 1.4.11 (AA)"
    check: "Contrast checker"
  - id: input/focus-visible
    layer: ui
    level: must
    scope: component
    requirement: "Focus shows an `outline` at least 2px thick around the whole field, outside its border"
    basis: "WCAG 2.4.7 (AA), 2.4.13 (AAA)"
    check: "Keyboard"
  - id: input/target-size
    layer: ui
    level: must
    scope: component
    requirement: "The field is at least 24px tall, with a hit area at least 44px tall on touch screens"
    basis: "WCAG 2.5.8 (AA), 2.5.5 (AAA)"
    check: "Emulation"
  - id: input/click-to-focus
    layer: ui
    level: should
    scope: component
    requirement: "Pressing its icons, prefixes or padding focuses the input, and an input that already has focus keeps it, without a blur"
    basis: "Open Components"
    check: "Unit test"
  - id: input/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 input gets focus"
    basis: "Open Components"
    check: "Emulation"
  - id: input/resizable
    layer: ui
    level: must
    scope: component
    requirement: "The field is sized with `min-height` and 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: input/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: input/state-selectors
    layer: ui
    level: should
    scope: component
    requirement: "States are styled from the attributes on the `<input>` that expose them, and never from `:invalid` or `:user-invalid`"
    basis: "Open Components"
    check: "Review"
  - id: input/hover-capable
    layer: ui
    level: should
    scope: component
    requirement: "Hover styles only apply inside `@media (hover: hover)`"
    basis: "Open Components"
    check: "Review"
  - id: input/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: input/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: input/fits-value
    layer: ui
    level: should
    scope: usage
    requirement: "Its width fits the value it expects, like a short one for a postcode, and never outgrows the screen"
    basis: "Open Components, beyond WCAG 1.4.10 (AA)"
    check: "Review"
  - id: input/native-element
    layer: ux
    level: must
    scope: component
    requirement: "It's a native `<input>`, and never a `contenteditable` element"
    basis: "Open Components, beyond WCAG 2.1.1, 4.1.2 (A)"
    check: "Unit test"
  - id: input/visible-label
    layer: ux
    level: must
    scope: usage
    requirement: "Every input has a visible `<label>`, linked with `for`, rather than a placeholder alone, unless it's a search next to its Search button, named with `aria-label`"
    basis: "WCAG 1.3.1, 3.3.2, 4.1.2 (A)"
    check: "axe `label`"
  - id: input/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: input/described
    layer: ux
    level: should
    scope: usage
    requirement: "Hints, like the format it expects, are linked with `aria-describedby`, and a placeholder only ever shows an example"
    basis: "WCAG 1.3.1 (A), Open Components"
    check: "Screen reader"
  - id: input/decorative-slots
    layer: ux
    level: must
    scope: component
    requirement: "`leading` and `trailing` are hidden from assistive technologies with `aria-hidden`, while `actions` stay reachable"
    basis: "WCAG 1.1.1 (A)"
    check: "Unit test"
  - id: input/units-in-label
    layer: ux
    level: must
    scope: usage
    requirement: "A prefix or a suffix, like a currency or a unit, is in the label or the hint as well"
    basis: "WCAG 1.3.1 (A)"
    check: "Review"
  - id: input/input-purpose
    layer: ux
    level: must
    scope: usage
    requirement: "An input that asks about the person filling it in has the matching `autocomplete`, like `email` or `tel`"
    basis: "WCAG 1.3.5 (AA)"
    check: "axe `autocomplete-valid`, review"
  - id: input/fitting-type
    layer: ux
    level: should
    scope: usage
    requirement: "The `type` or `inputmode` fits the value, so phones show the right keyboard, digits that aren't amounts use `inputmode=\"numeric\"` rather than `type=\"number\"`, and values that aren't words turn off spellcheck"
    basis: "Open Components"
    check: "Review"
  - id: input/keyboard
    layer: ux
    level: must
    scope: component
    requirement: "`Tab` reaches the input and then its actions, and the browser's text editing keys work as they do in any text field"
    basis: "WCAG 2.1.1 (A)"
    check: "Unit test"
  - id: input/allow-paste
    layer: ux
    level: must
    scope: both
    requirement: "Pasting, autofill and password managers are never blocked"
    basis: "WCAG 3.3.8 (AA)"
    check: "Unit test, review"
  - id: input/enter-submits
    layer: ux
    level: should
    scope: both
    requirement: "`Enter` submits the input's form, which has a submit button"
    basis: "HTML"
    check: "Unit test"
  - id: input/no-truncation
    layer: ux
    level: should
    scope: usage
    requirement: "A length limit is stated in the hint and checked with an error message, rather than enforced with `maxlength`"
    basis: "Open Components"
    check: "Review"
  - id: input/error-message
    layer: ux
    level: must
    scope: usage
    requirement: "An invalid input 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: input/invalid-state
    layer: ux
    level: must
    scope: both
    requirement: "An input 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: input/validate-late
    layer: ux
    level: should
    scope: usage
    requirement: "Errors show when people leave the input or submit, never while they're first typing, and clear as soon as the value is fixed"
    basis: "Open Components"
    check: "Review"
  - id: input/focus-first-error
    layer: ux
    level: should
    scope: usage
    requirement: "When a submit fails, focus moves to the first invalid input, or to a summary of the errors, once the error messages are in the page"
    basis: "Open Components"
    check: "Keyboard, screen reader"
  - id: input/disabled-inert
    layer: ux
    level: must
    scope: component
    requirement: "A disabled input can't be focused, edited or submitted"
    basis: "HTML"
    check: "Unit test"
  - id: input/read-only-values
    layer: ux
    level: should
    scope: usage
    requirement: "A value people need to see but can't change is read-only rather than disabled, so they can focus it and copy it, and its form sends it"
    basis: "Open Components"
    check: "Review"
  - id: input/keep-focus
    layer: ux
    level: must
    scope: usage
    requirement: "An input is never disabled while it has focus, so while its form is sent, it's read-only instead"
    basis: "Open Components"
    check: "Keyboard"
  - id: input/action-buttons
    layer: ux
    level: must
    scope: both
    requirement: "Buttons in `actions` are named Buttons that `Tab` reaches after the input, and one that goes away moves focus back to the input"
    basis: "WCAG 2.1.1, 2.4.3, 4.1.2 (A)"
    check: "Unit test, keyboard"
  - id: input/show-password
    layer: ux
    level: should
    scope: usage
    requirement: "A password input has a Show password toggle in `actions`, which sets `aria-pressed` and keeps its label"
    basis: "Open Components"
    check: "Review"
  - id: input/focus-not-obscured
    layer: ux
    level: must
    scope: usage
    requirement: "Sticky headers and footers never cover a focused input, for example thanks to `scroll-padding`"
    basis: "WCAG 2.4.11 (AA), 2.4.12 (AAA)"
    check: "Keyboard"
  - id: input/mirrors-in-rtl
    layer: ux
    level: should
    scope: both
    requirement: "It uses logical properties, and values that always run left to right, like email addresses, get `dir=\"ltr\"` in right-to-left layouts"
    basis: "Open Components"
    check: "Review"
  - id: input/shared-vocabulary
    layer: dx
    level: must
    scope: component
    requirement: "It uses the shared names: `size`, `disabled`, `leading`, `trailing` and `actions`"
    basis: "Open Components"
    check: "Review"
  - id: input/typed
    layer: dx
    level: must
    scope: component
    requirement: "Props, events and slots are typed, with unions rather than `string`, and `type` only takes the text types"
    basis: "Open Components"
    check: "Type check"
  - id: input/control-attributes
    layer: dx
    level: must
    scope: component
    requirement: "Attributes, listeners and refs land on the `<input>`, while classes and styles land on the root"
    basis: "Open Components"
    check: "Unit test"
  - id: input/form-control
    layer: dx
    level: must
    scope: component
    requirement: "Its form sends its value under its `name`, whether the value is bound or not"
    basis: "HTML"
    check: "Unit test"
  - id: input/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: input/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found by its role and label alone, with `getByRole(\"textbox\", { name })`, or with `getByLabel` for a password"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: input/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: input/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "The size is mirrored as `data-size`, and parts are marked with `data-slot`"
    basis: "Open Components"
    check: "Unit test"
  - id: input/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when an input has no label"
    basis: "Open Components"
    check: "Unit test"
