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

component: Switch
summary: Turns a setting on or off, with an effect that applies right away. Its label names the setting.
standard: https://opencomponents.dev/docs/components/switch
element: input # type=checkbox, inside a span with the track and thumb, which takes class and style
role: switch
props:
  size: { type: [xs, sm, md, lg, xl], default: md }
  disabled: { type: boolean, default: false }
  readonly: { type: boolean, default: false } # sets aria-readonly, and cancels the click that would flip it
  checked: { type: boolean, default: false } # when it's bound. Without it, the switch keeps its own
events:
  change: Event, from the <input>. Fires when people flip it.
states:
  checked: "input:checked"
  hover: ":hover, in @media (hover: hover)"
  focus: "input:focus-visible"
  disabled: "input:disabled"
  read-only: "input[aria-readonly=true]"
  invalid: "input[aria-invalid=true]"
keyboard:
  Tab: Moves focus to the switch.
  Space: Flips it on or off.
parts: [control, track, thumb] # data-slot, in this order. The control is the <input>, and the thumb is inside the track
tokens:
  theme: ["--color--neutral-text", "--color--primary", "--color--primary-contrast", "--color--error-text", "--color--focus"]
  variables: [--switch--track--width, --switch--track--height, --switch--track--border-color, --switch--track--background-color, --switch--thumb--size, --switch--thumb--offset, --switch--thumb--background-color]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: switch/contrast
    layer: ui
    level: must
    scope: component
    requirement: "The track and the thumb have a contrast of 3:1 against what's next to them, on and off, and so does the focus ring"
    basis: "WCAG 1.4.11 (AA)"
    check: "Contrast checker"
  - id: switch/more-than-colour
    layer: ui
    level: must
    scope: component
    requirement: "On and off differ by more than colour: the thumb moves to the other end, and the track fills"
    basis: "WCAG 1.4.1 (A)"
    check: "Review"
  - id: switch/focus-visible
    layer: ui
    level: must
    scope: component
    requirement: "Focus shows an `outline` at least 2px thick around the track, outside it"
    basis: "WCAG 2.4.7 (AA), 2.4.13 (AAA)"
    check: "Keyboard"
  - id: switch/target-size
    layer: ui
    level: must
    scope: component
    requirement: "The hit area is at least 24 × 24px, and at least 44 × 44px on touch screens"
    basis: "WCAG 2.5.8 (AA), 2.5.5 (AAA)"
    check: "Emulation"
  - id: switch/resizable
    layer: ui
    level: must
    scope: component
    requirement: "The track and the thumb are sized in `rem`, so they grow with the text"
    basis: "Open Components, beyond WCAG 1.4.4 (AA)"
    check: "200% zoom"
  - id: switch/stable-size
    layer: ui
    level: must
    scope: component
    requirement: "No state changes the switch's size or position, and the thumb only moves inside the track"
    basis: "Open Components"
    check: "Visual regression test"
  - id: switch/state-selectors
    layer: ui
    level: should
    scope: component
    requirement: "States are styled from the `<input>` that exposes them, with `:checked`, `:disabled`, `aria-readonly` and `aria-invalid`"
    basis: "Open Components"
    check: "Review"
  - id: switch/hover-capable
    layer: ui
    level: should
    scope: component
    requirement: "Hover styles only apply inside `@media (hover: hover)`"
    basis: "Open Components"
    check: "Review"
  - id: switch/reduced-motion
    layer: ui
    level: must
    scope: component
    requirement: "The thumb doesn't slide when `prefers-reduced-motion: reduce` is set"
    basis: "WCAG 2.3.3 (AAA)"
    check: "Emulation"
  - id: switch/forced-colors
    layer: ui
    level: must
    scope: component
    requirement: "The track, the thumb and the on, focus, disabled and read-only states stay visible in forced colours mode"
    basis: "Open Components"
    check: "Emulation"
  - id: switch/more-contrast
    layer: ui
    level: should
    scope: component
    requirement: "The track and the thumb are drawn at full strength when `prefers-contrast: more` is set"
    basis: "Open Components"
    check: "Emulation"
  - id: switch/native-element
    layer: ux
    level: must
    scope: component
    requirement: "It's a native `<input type=\"checkbox\">` with `role=\"switch\"`, and never a `<div>`"
    basis: "APG, Open Components, beyond WCAG 2.1.1, 4.1.2 (A)"
    check: "Unit test"
  - id: switch/checked-state
    layer: ux
    level: must
    scope: component
    requirement: "Its state is the checkbox's own `checked`, announced as on or off, with no pressed or mixed state"
    basis: "WAI-ARIA, APG"
    check: "Unit test"
  - id: switch/visible-label
    layer: ux
    level: must
    scope: usage
    requirement: "Every switch has a visible `<label>`, linked with `for`"
    basis: "WCAG 1.3.1, 3.3.2, 4.1.2 (A)"
    check: "axe `label`"
  - id: switch/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: switch/stable-label
    layer: ux
    level: must
    scope: usage
    requirement: "The label names the setting and stays the same whether the switch is on or off"
    basis: "APG"
    check: "Review"
  - id: switch/described
    layer: ux
    level: should
    scope: usage
    requirement: "Hints, like what the setting changes, are linked with `aria-describedby`"
    basis: "WCAG 1.3.1 (A)"
    check: "Screen reader"
  - id: switch/keyboard
    layer: ux
    level: must
    scope: component
    requirement: "`Tab` reaches the switch, and `Space` and a click on its label flip it"
    basis: "WCAG 2.1.1 (A), APG"
    check: "Unit test"
  - id: switch/applies-immediately
    layer: ux
    level: must
    scope: usage
    requirement: "Flipping it applies its setting right away, with no Save button, and a setting that waits for one is a checkbox instead"
    basis: "Open Components"
    check: "Review"
  - id: switch/announce-saved
    layer: ux
    level: should
    scope: usage
    requirement: "While its change is saved, it keeps its new state and stays enabled, and a status message says when it's saved"
    basis: "WCAG 4.1.3 (AA), Open Components"
    check: "Screen reader"
  - id: switch/no-context-change
    layer: ux
    level: must
    scope: usage
    requirement: "Flipping it never moves focus, opens a page or submits a form, unless its hint says so first"
    basis: "WCAG 3.2.2 (A)"
    check: "Review"
  - id: switch/error-message
    layer: ux
    level: must
    scope: usage
    requirement: "An invalid switch has an error message, in text and linked with `aria-describedby`, that says what happened and what to do"
    basis: "WCAG 1.4.1, 3.3.1 (A)"
    check: "Screen reader"
  - id: switch/invalid-state
    layer: ux
    level: must
    scope: both
    requirement: "A switch 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: switch/revert-on-failure
    layer: ux
    level: should
    scope: usage
    requirement: "When its change fails, it goes back to how it was, and its error message says so"
    basis: "Open Components"
    check: "Review"
  - id: switch/disabled-inert
    layer: ux
    level: must
    scope: component
    requirement: "A disabled switch can't be focused, flipped or submitted"
    basis: "HTML"
    check: "Unit test"
  - id: switch/read-only
    layer: ux
    level: must
    scope: component
    requirement: "A read-only switch can be focused and is sent with its form, but doesn't flip, and sets `aria-readonly=\"true\"`"
    basis: "WAI-ARIA"
    check: "Unit test"
  - id: switch/read-only-values
    layer: ux
    level: should
    scope: usage
    requirement: "A setting people need to see but can't change is read-only rather than disabled, with a hint that says who can change it"
    basis: "Open Components"
    check: "Review"
  - id: switch/keep-focus
    layer: ux
    level: must
    scope: usage
    requirement: "A switch is never disabled while it has focus, like while its change is saved"
    basis: "Open Components"
    check: "Keyboard"
  - id: switch/mirrors-in-rtl
    layer: ux
    level: should
    scope: component
    requirement: "The thumb moves from the inline start to the inline end, so on is on the left in right-to-left layouts"
    basis: "Open Components"
    check: "Review"
  - id: switch/shared-vocabulary
    layer: dx
    level: must
    scope: component
    requirement: "It uses the shared names: `size`, `disabled`, `readonly` and `checked`"
    basis: "Open Components"
    check: "Review"
  - id: switch/typed
    layer: dx
    level: must
    scope: component
    requirement: "Props and events are typed, with unions rather than `string`"
    basis: "Open Components"
    check: "Type check"
  - id: switch/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: switch/form-control
    layer: dx
    level: must
    scope: component
    requirement: "Its form sends its `value` under its `name` while it's on, whether `checked` is bound or not"
    basis: "HTML"
    check: "Unit test"
  - id: switch/controllable
    layer: dx
    level: should
    scope: component
    requirement: "Whether it's on 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: switch/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found by its role, its label and its state alone, with `getByRole(\"switch\", { name, checked })`"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: switch/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: switch/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: switch/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when a switch has no label"
    basis: "Open Components"
    check: "Unit test"
