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

component: CheckboxGroup
summary: Lets people pick any number of options from a short list. Its legend names it, and each option is a Checkbox.
standard: https://opencomponents.dev/docs/components/checkbox-group
element: fieldset # its first child is the legend that names it. It takes every attribute that isn't a prop
role: group
props:
  options: { type: "{ value: string, label: string }[]" } # a Checkbox for each, in this order
  name: { type: string } # shared by every box, so the form sends one field with a value per checked option
  value: { type: "string[]", default: [] } # the checked options' values, in their order, when it's bound. Without it, the group keeps its own
  size: { type: [xs, sm, md, lg, xl], default: md }
  orientation: { type: [vertical, horizontal], default: vertical }
  disabled: { type: boolean, default: false } # sets disabled on the fieldset
  selectAll: { type: string } # the label of a box above the options that checks or clears them all
slots:
  default: The legend, which names the group.
events:
  change: Event, from the box that changed, which bubbles up to the fieldset.
states:
  disabled: ":disabled, from <fieldset disabled>, which every box inside it matches too"
  invalid: "[aria-invalid=true], which each option gets too, never :invalid"
  select-all: "> [data-slot=select-all] input:checked when every option is, :indeterminate when only some are"
keyboard:
  Tab: Moves focus to the next box, select-all box included. There's no roving focus.
  Space: Checks or unchecks the focused box. On the select-all box, it checks every option, or clears them all.
parts: [legend, select-all, options] # data-slot, in this order. Only select-all is optional
tokens:
  theme: ["--color--neutral-text"] # and the Checkbox's, through its boxes
  variables: [--checkbox-group--legend--font-size, --checkbox-group--legend--margin-bottom, --checkbox-group--indent, --checkbox-group--row-gap, --checkbox-group--column-gap]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: checkbox-group/legend-contrast
    layer: ui
    level: must
    scope: component
    requirement: "The legend has a contrast of 4.5:1 against the page"
    basis: "WCAG 1.4.3 (AA)"
    check: "axe `color-contrast`, contrast checker"
  - id: checkbox-group/vertical-by-default
    layer: ui
    level: should
    scope: component
    requirement: "Options run in a column unless it's set to a row, and a row wraps rather than overflowing"
    basis: "Open Components"
    check: "Visual regression test"
  - id: checkbox-group/option-spacing
    layer: ui
    level: should
    scope: component
    requirement: "Options sit at least 8px apart in a column and 24px apart in a row"
    basis: "Open Components"
    check: "Visual regression test"
  - id: checkbox-group/select-all-indent
    layer: ui
    level: should
    scope: component
    requirement: "Options under a select-all box sit in from it by its box and gap, so their boxes line up with its label"
    basis: "Open Components"
    check: "Visual regression test"
  - id: checkbox-group/resizable
    layer: ui
    level: must
    scope: component
    requirement: "The legend, the gaps and the indent are sized in `rem`, and the legend and a row of options wrap instead of being clipped or scrolling sideways"
    basis: "Open Components, beyond WCAG 1.4.4, 1.4.10, 1.4.12 (AA)"
    check: "200% zoom"
  - id: checkbox-group/disabled-look
    layer: ui
    level: must
    scope: component
    requirement: "A disabled group shows its legend as disabled along with its boxes, in `GrayText` in forced colours mode"
    basis: "Open Components"
    check: "Visual regression test, emulation"
  - id: checkbox-group/stable-size
    layer: ui
    level: must
    scope: component
    requirement: "No state changes the group's size or position"
    basis: "Open Components"
    check: "Visual regression test"
  - id: checkbox-group/fieldset
    layer: ux
    level: must
    scope: component
    requirement: "It's a `<fieldset>` whose first child is the `<legend>` that names it, so it has the `group` role and the legend as its name"
    basis: "WCAG 1.3.1, 4.1.2 (A), HTML"
    check: "Unit test"
  - id: checkbox-group/legend-question
    layer: ux
    level: must
    scope: usage
    requirement: "Every group has a visible legend that says what its options are for, ideally as the question they answer"
    basis: "WCAG 1.3.1, 3.3.2 (A)"
    check: "Review"
  - id: checkbox-group/described
    layer: ux
    level: should
    scope: both
    requirement: "A hint and an error message sit outside the legend and are linked to the `<fieldset>` with `aria-describedby`, the hint's ID first"
    basis: "WCAG 1.3.1 (A), APG"
    check: "Unit test, screen reader"
  - id: checkbox-group/keyboard
    layer: ux
    level: must
    scope: component
    requirement: "`Tab` reaches each box in turn, select-all box included, `Space` toggles the focused one, and the arrow keys never move focus between them"
    basis: "WCAG 2.1.1 (A), APG"
    check: "Unit test"
  - id: checkbox-group/select-all-state
    layer: ux
    level: must
    scope: component
    requirement: "The select-all box is checked when every option is, unchecked when none is, and mixed when only some are, worked out from them whenever they change"
    basis: "WCAG 4.1.2 (A), APG"
    check: "Unit test"
  - id: checkbox-group/select-all-action
    layer: ux
    level: must
    scope: component
    requirement: "Checking the select-all box, from unchecked or mixed, checks every option, and unchecking it clears them all, without going back to an earlier selection"
    basis: "APG, Open Components"
    check: "Unit test"
  - id: checkbox-group/select-all-label
    layer: ux
    level: should
    scope: usage
    requirement: "A select-all box says what it selects, like \"All emails\", and only comes with lists that are long or often picked in full"
    basis: "Open Components"
    check: "Review"
  - id: checkbox-group/vertical
    layer: ux
    level: should
    scope: usage
    requirement: "Options run in a column, and only two or three short ones run in a row"
    basis: "Open Components"
    check: "Review"
  - id: checkbox-group/predictable-order
    layer: ux
    level: should
    scope: usage
    requirement: "Options come in an order people can predict, like most to least common or alphabetical, and keep it every time"
    basis: "Open Components"
    check: "Review"
  - id: checkbox-group/at-least-one
    layer: ux
    level: must
    scope: usage
    requirement: "A group people have to pick from says so in its legend or hint, and is checked in code when the form is sent, never with `required` on its boxes"
    basis: "HTML, WCAG 3.3.2 (A)"
    check: "Review"
  - id: checkbox-group/error-message
    layer: ux
    level: must
    scope: usage
    requirement: "An invalid group has an error message, in text and linked to the `<fieldset>` with `aria-describedby`, that says what to pick"
    basis: "WCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA)"
    check: "Screen reader"
  - id: checkbox-group/invalid-state
    layer: ux
    level: must
    scope: both
    requirement: "The group sets `aria-invalid=\"true\"` while its error message shows, and each of its options looks and reads invalid with it"
    basis: "WCAG 4.1.2 (A), Open Components"
    check: "Unit test"
  - id: checkbox-group/focus-on-error
    layer: ux
    level: should
    scope: usage
    requirement: "A failed submit moves focus to the group's first box, once the error is in the page"
    basis: "Open Components, beyond WCAG 3.3.1 (A)"
    check: "Keyboard"
  - id: checkbox-group/disabled-inert
    layer: ux
    level: must
    scope: component
    requirement: "A disabled group sets `disabled` on its `<fieldset>`, so no box in it, select-all box included, can be focused, toggled or sent"
    basis: "HTML"
    check: "Unit test"
  - id: checkbox-group/mirrors-in-rtl
    layer: ux
    level: should
    scope: component
    requirement: "The indent and a row of options follow `dir`, with logical properties"
    basis: "Open Components"
    check: "Review"
  - id: checkbox-group/shared-vocabulary
    layer: dx
    level: must
    scope: component
    requirement: "It uses the shared names: `size`, `disabled`, `name`, `value`, `options`, `orientation`, `selectAll`, the default slot for its legend and `change`"
    basis: "Open Components"
    check: "Review"
  - id: checkbox-group/typed
    layer: dx
    level: must
    scope: component
    requirement: "Props, events and slots are typed, with unions rather than `string`, and the value is a list of strings"
    basis: "Open Components"
    check: "Type check"
  - id: checkbox-group/composes-checkbox
    layer: dx
    level: must
    scope: component
    requirement: "Each option and the select-all box is a Checkbox, so every Checkbox rule holds for each of them"
    basis: "Open Components"
    check: "Review"
  - id: checkbox-group/form-control
    layer: dx
    level: must
    scope: component
    requirement: "Every option shares one `name`, and its form sends a value for each checked option, in the options' order, and nothing for the select-all box"
    basis: "HTML"
    check: "Unit test"
  - id: checkbox-group/controllable
    layer: dx
    level: should
    scope: component
    requirement: "`value` is a list that can be bound or left to the group, and a bound group always shows the value you hold"
    basis: "Open Components"
    check: "Unit test"
  - id: checkbox-group/fieldset-attributes
    layer: dx
    level: must
    scope: component
    requirement: "Attributes, listeners and refs land on the `<fieldset>`, and each box's `change` bubbles up to it"
    basis: "Open Components"
    check: "Unit test"
  - id: checkbox-group/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found by its legend alone, with `getByRole(\"group\", { name })`, and each option inside it with `getByRole(\"checkbox\", { name })`"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: checkbox-group/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup, with no generated IDs, and states change properties and attributes, never elements"
    basis: "Open Components"
    check: "Unit test"
  - id: checkbox-group/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "The size and the orientation are mirrored as `data-size` and `data-orientation`, and parts are marked with `data-slot`"
    basis: "Open Components"
    check: "Unit test"
  - id: checkbox-group/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when a group has no legend"
    basis: "Open Components"
    check: "Unit test"
