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

component: Field
summary: Puts a label, a hint and an error message around one form control, and links them to it with its id, aria-describedby and aria-invalid. It renders nothing itself around a Radio Group, which takes its hint and error as props.
standard: https://opencomponents.dev/docs/components/field
element: div # around the control, with data-control naming its kind. None around a Radio Group
props:
  id: { type: string } # the control's. Without it, the control's own id, then its name. The hint's and error's IDs add -hint and -error
  label: { type: string } # for a text control or a Switch. A Checkbox and the groups name themselves
  hint: { type: string }
  error: { type: string } # while it's set, the control is invalid
  required: { type: boolean, default: false } # the control's native required, never a Checkbox Group's
  disabled: { type: boolean, default: false }
  size: { type: [xs, sm, md, lg, xl] } # also goes to a component control, never a native one. Without it, the control's own, then md
slots:
  default: One control, an Input, a Textarea, a Checkbox, a Switch, a Checkbox Group, a Radio Group or a native input, textarea or select.
states:
  required: "[required] on the control, or on each radio in a Radio Group"
  disabled: ":has(> :disabled, > * > [data-slot=control]:disabled), the control only"
  invalid: "[aria-invalid=true] on the control, never :invalid"
parts: [label, hint, error] # data-slot. In the DOM, around a text control: label, hint, control, error. Around a Switch: label, control, hint, error. Around a Checkbox or a Checkbox Group: control, hint, error
tokens:
  theme: ["--color--neutral-text", "--color--error-text"]
  variables: [--field--gap, --field--font-size, --field--column-gap, --field--indent]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: field/label-position
    layer: ui
    level: should
    scope: component
    requirement: "The label sits above a text control, with the hint between them, before a Switch on the same row, and after a Checkbox's box"
    basis: "Open Components"
    check: "Unit test"
  - id: field/error-after
    layer: ui
    level: should
    scope: component
    requirement: "The error comes after a single control, so showing it never moves the control"
    basis: "Open Components"
    check: "Unit test"
  - id: field/text-contrast
    layer: ui
    level: must
    scope: component
    requirement: "The label, the hint and the error have a contrast of 4.5:1 against the page"
    basis: "WCAG 1.4.3 (AA)"
    check: "axe `color-contrast`, contrast checker"
  - id: field/error-in-text
    layer: ui
    level: must
    scope: both
    requirement: "An error is a message in text, never a colour alone"
    basis: "WCAG 1.4.1 (A)"
    check: "Review"
  - id: field/like-groups
    layer: ui
    level: should
    scope: component
    requirement: "The hint and the error have the same text size, colour and spacing as the Radio Group's"
    basis: "Open Components"
    check: "Review"
  - id: field/checkbox-indent
    layer: ui
    level: should
    scope: component
    requirement: "A Checkbox's hint and error line up with its label, rather than its box"
    basis: "Open Components"
    check: "Visual regression test"
  - id: field/resizable
    layer: ui
    level: must
    scope: component
    requirement: "Its text and spacing are set in `rem`, so they grow with the text size rather than clip"
    basis: "Open Components, beyond WCAG 1.4.4, 1.4.12 (AA)"
    check: "200% zoom"
  - id: field/forced-colors
    layer: ui
    level: must
    scope: component
    requirement: "A disabled label stays visible in forced colours mode"
    basis: "Open Components"
    check: "Emulation"
  - id: field/visible-label
    layer: ux
    level: must
    scope: usage
    requirement: "Every text control and switch in a Field has a visible label that says what to fill in"
    basis: "WCAG 1.3.1, 3.3.2, 4.1.2 (A), 2.4.6 (AA)"
    check: "axe `label`, review"
  - id: field/label-for
    layer: ux
    level: must
    scope: component
    requirement: "The label's `for` is the control's `id`, so a click on it focuses or toggles the control"
    basis: "WCAG 1.3.1 (A)"
    check: "Unit test"
  - id: field/one-name
    layer: ux
    level: must
    scope: component
    requirement: "A Checkbox, a Checkbox Group and a Radio Group get no label from the Field, so each keeps its own name"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: field/group-legend
    layer: ux
    level: must
    scope: usage
    requirement: "A Checkbox Group and a Radio Group are named by a legend that asks the question their options answer"
    basis: "WCAG 1.3.1 (A)"
    check: "Review"
  - id: field/described
    layer: ux
    level: must
    scope: component
    requirement: "The control's `aria-describedby` lists the hint, then the error, then the control's own"
    basis: "WCAG 1.3.1 (A)"
    check: "Unit test"
  - id: field/no-empty-error
    layer: ux
    level: should
    scope: component
    requirement: "The error element is only in the page while there's an error"
    basis: "Open Components"
    check: "Unit test"
  - id: field/error-message
    layer: ux
    level: must
    scope: usage
    requirement: "An error says what's wrong and how to fix it, in text"
    basis: "WCAG 3.3.1 (A), 3.3.3 (AA)"
    check: "Screen reader"
  - id: field/invalid-state
    layer: ux
    level: must
    scope: component
    requirement: "The Field sets `aria-invalid=\"true\"` on the control while it has an error, and never sets it otherwise"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: field/not-live
    layer: ux
    level: should
    scope: component
    requirement: "The error message isn't a live region, so it never talks over the control people move to"
    basis: "Open Components"
    check: "Unit test"
  - id: field/stable-id
    layer: ux
    level: must
    scope: component
    requirement: "The control's `id` comes from the Field's `id`, the control's `id` or its `name`, and the hint's and error's from it, with nothing generated"
    basis: "Open Components"
    check: "Unit test"
  - id: field/required-native
    layer: ux
    level: must
    scope: component
    requirement: "`required` sets the control's native `required`, or each radio's in a Radio Group, and never a Checkbox Group's"
    basis: "HTML"
    check: "Unit test"
  - id: field/required-in-words
    layer: ux
    level: should
    scope: usage
    requirement: "Required or optional fields are marked in words, on whichever there are fewer of, like \"(optional)\" in the label"
    basis: "Open Components, beyond WCAG 3.3.2 (A)"
    check: "Review"
  - id: field/validate-late
    layer: ux
    level: should
    scope: usage
    requirement: "Errors show when people leave a control or submit, never while they're first typing, and clear as soon as the value is fixed"
    basis: "Open Components"
    check: "Review"
  - id: field/focus-first-error
    layer: ux
    level: should
    scope: usage
    requirement: "When a submit fails, focus moves to the first invalid control, or to a summary of the errors, once the errors are in the page"
    basis: "Open Components"
    check: "Keyboard, screen reader"
  - id: field/status-message
    layer: ux
    level: should
    scope: usage
    requirement: "A failed submit says what happened in a status message that screen readers announce"
    basis: "WCAG 4.1.3 (AA)"
    check: "Screen reader"
  - id: field/novalidate
    layer: ux
    level: should
    scope: usage
    requirement: "The form has `novalidate`, while its controls keep `required` and `type`, and the browser's messages are never shown as they are"
    basis: "Open Components"
    check: "Review"
  - id: field/disabled
    layer: ux
    level: must
    scope: component
    requirement: "`disabled` sets the control's own `disabled`, so it can't be focused, changed or sent"
    basis: "HTML"
    check: "Unit test"
  - id: field/mirrors-in-rtl
    layer: ux
    level: should
    scope: component
    requirement: "It uses logical properties, so it mirrors in right-to-left layouts"
    basis: "Open Components"
    check: "Review"
  - id: field/consistent-names
    layer: dx
    level: should
    scope: component
    requirement: "Its props use the names the controls use, like `hint`, `error`, `required`, `disabled` and `size`, with the same meanings"
    basis: "Open Components"
    check: "Review"
  - id: field/typed
    layer: dx
    level: should
    scope: component
    requirement: "Its props and slot are typed, and `FieldProps` is exported"
    basis: "Open Components"
    check: "Type check"
  - id: field/controls-unchanged
    layer: dx
    level: must
    scope: component
    requirement: "It works with every form control through the attributes they already take, without a prop, a context or code of theirs that knows about it"
    basis: "Open Components"
    check: "Unit test"
  - id: field/own-attributes
    layer: dx
    level: should
    scope: component
    requirement: "An `aria-describedby` set on the control is kept after the Field's, and an `aria-invalid` set on it is kept while the Field has no error"
    basis: "Open Components"
    check: "Unit test"
  - id: field/one-control
    layer: dx
    level: should
    scope: usage
    requirement: "A Field holds exactly one control"
    basis: "Open Components"
    check: "Unit test"
  - id: field/stateless
    layer: dx
    level: should
    scope: component
    requirement: "It holds no state of its own, so it shows exactly the label, hint and error it's given"
    basis: "Open Components"
    check: "Review"
  - id: field/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "Its control can be found by its role and the Field's label, or by its own label or legend, with `getByRole`"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: field/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup on the server and the client, with no generated IDs, and states never replace the control"
    basis: "Open Components"
    check: "Unit test"
  - id: field/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "The size is mirrored as `data-size`, the kind of control as `data-control`, and parts are marked with `data-slot`"
    basis: "Open Components"
    check: "Unit test"
  - id: field/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when it has no `id`, a `label` its control doesn't take, a native checkbox or radio, or other than one control"
    basis: "Open Components"
    check: "Unit test"
