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

component: Select
summary: Picks one option, or several, out of a list, in a panel you can restyle, with a native select doing the form's work and showing the device's own picker on touch screens.
standard: https://opencomponents.dev/docs/components/select
element: div # the field, holding a button, a select and a popover panel
role: combobox # on the button
props:
  options: { type: "SelectItem[]" } # required. Options, { label, options } groups and { separator: true }
  value: { type: string } # or string[] with multiple, bound or not
  multiple: { type: boolean, default: false }
  placeholder: { type: string }
  size: { type: [xs, sm, md, lg, xl], default: md }
  disabled: { type: boolean, default: false }
  readonly: { type: boolean, default: false }
  required: { type: boolean, default: false }
  nativeOnTouch: { type: boolean, default: true } # the native select on a coarse primary pointer
  removeLabel: { type: function } # (label) => "Remove France"
  removedLabel: { type: function } # (label) => "France removed"
  countLabel: { type: function } # (count) => "4 selected"
slots:
  leading: An icon before the value, hidden from assistive technologies. Stays in the native rendering.
  value: How the picked value reads, given selected.
  arrow: The chevron, hidden from assistive technologies. Stays in the native rendering.
  option: How an option reads, given option, selected and active. The check mark stays beside it.
  tag: How a picked value reads in its tag, given option. The remove button stays beside it.
  group-label: How a group's heading reads, given label.
  separator: The rule between options or groups.
  icon: An option's icon, given option, shown in the panel, beside the picked value and in each tag, hidden from assistive technologies.
events:
  change: "The native change event, from the select, for a choice in the panel or the native picker."
  input: "The native input event, from the select, before change."
states:
  open: "[data-state=open]"
  closed: "[data-state=closed]"
  disabled: "[data-slot=control]:disabled"
  read-only: "[data-slot=control][aria-readonly=true]"
  invalid: "[data-slot=control][aria-invalid=true]"
  highlighted: "[data-slot=option][data-active=true]"
  selected: "[data-slot=option][aria-selected=true]"
  native: "[data-native], where the primary pointer is coarse"
keyboard:
  ArrowDown: Opens it, or moves the highlight, skipping disabled options.
  ArrowUp: Opens it on the first option, or moves the highlight. With Alt, picks and closes, or with multiple, closes and keeps what's toggled.
  Home: Opens it on the first option, or moves there.
  End: Opens it on the last option, or moves there.
  PageDown: Moves ten options, stopping at the end.
  PageUp: Moves ten options, stopping at the start.
  Enter: Opens it, or picks and closes. With multiple, toggles and stays open.
  Space: The same as Enter, unless typing.
  Tab: Picks the highlighted option and moves on. With multiple, closes and keeps what's toggled.
  Escape: Closes it, keeps the value and goes no further.
  Backspace: With multiple, on the closed control, removes the last value.
parts: [native, control, icon, value, tags, tag, tag-label, tag-remove, leading, arrow, panel, group, group-label, separator, option, option-label, check] # data-slot, in this order. control is on the button in the custom rendering and the select in the native one, and native on the select in the custom rendering
tokens:
  theme: ["--color--neutral-text", "--color--focus", "--color--error-text"]
  variables: [--select--height, --select--padding, --select--gap, --select--font-size, --select--radius, --select--border-color, --select--background, --select--icon--size, --select--leading--color, --select--value--placeholder-color, --select--arrow--size, --select--arrow--color, --select--panel--offset, --select--panel--padding, --select--panel--max-height, --select--panel--radius, --select--panel--border-color, --select--panel--background, --select--panel--box-shadow, --select--option--padding-block, --select--option--padding-inline, --select--option--radius, --select--option--color, --select--option--hover--background, --select--option--selected--background, --select--option--selected--font-weight, --select--option--disabled--opacity, --select--tag--height, --select--tag--padding, --select--tag--radius, --select--tag--font-size, --select--tag--background, --select--tag--color, --select--tag--icon--size, --select--group-label--padding-block, --select--group-label--padding-inline, --select--group-label--font-size, --select--group-label--font-weight, --select--group-label--color, --select--separator--color, --select--separator--margin]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: select/sizes
    layer: ui
    level: should
    scope: component
    requirement: "It takes the Input's five sizes, with the same heights, so it lines up with an Input and a Button"
    basis: "Open Components"
    check: "Unit test, Visual regression test"
  - id: select/target-size
    layer: ui
    level: must
    scope: component
    requirement: "The field, each option and each remove button are at least 24 × 24px, and at least 44px tall on a touch screen"
    basis: "WCAG 2.5.8 (AA)"
    check: "Emulation"
  - id: select/focus-visible
    layer: ui
    level: must
    scope: component
    requirement: "Focus shows a ring around the field, and the highlighted option an outline of its own"
    basis: "WCAG 2.4.7 (AA)"
    check: "Keyboard"
  - id: select/contrast
    layer: ui
    level: must
    scope: both
    requirement: "Text has at least 4.5:1 and the field's border at least 3:1 against what's behind them"
    basis: "WCAG 1.4.3, 1.4.11 (AA)"
    check: "Contrast checker"
  - id: select/not-colour-alone
    layer: ui
    level: must
    scope: component
    requirement: "A selected option has a check mark and a heavier weight, not only a colour"
    basis: "WCAG 1.4.1 (A)"
    check: "Review"
  - id: select/tokens
    layer: ui
    level: must
    scope: component
    requirement: "Each part sets its look through `--select--…` token paths in the `components` layer"
    basis: "Open Components"
    check: "Stylelint"
  - id: select/decorative-icons
    layer: ui
    level: must
    scope: component
    requirement: "Icons in the panel, the field and the tags are hidden from assistive technologies, so the label alone names each option"
    basis: "WCAG 1.1.1 (A)"
    check: "Unit test, ARIA snapshot"
  - id: select/stable-size
    layer: ui
    level: should
    scope: component
    requirement: "Opening the panel, hovering and focusing don't change the field's size, and only tags can grow it"
    basis: "Open Components"
    check: "Visual regression test"
  - id: select/combobox-pattern
    layer: ux
    level: must
    scope: component
    requirement: "The control is a `<button role=\"combobox\">` linked to a `listbox` with `aria-expanded`, `aria-controls` and `aria-activedescendant`"
    basis: "WAI-ARIA, APG, ARIA in HTML"
    check: "Unit test, ARIA snapshot"
  - id: select/has-a-name
    layer: ux
    level: must
    scope: both
    requirement: "It has a name, from a Field's label or an `aria-label`, and the panel has the same name"
    basis: "WCAG 4.1.2 (A), 3.3.2 (A)"
    check: "Unit test"
  - id: select/reads-its-value
    layer: ux
    level: must
    scope: component
    requirement: "The control's text is its value, as a label, a list of labels or a count past three"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test, ARIA snapshot"
  - id: select/label-press-focuses
    layer: ux
    level: must
    scope: component
    requirement: "A press on its label gives it focus without opening the panel"
    basis: "HTML, Open Components"
    check: "Unit test"
  - id: select/keyboard
    layer: ux
    level: must
    scope: component
    requirement: "Every key in the keyboard tables does what they say"
    basis: "WCAG 2.1.1 (A), APG"
    check: "Unit test, Keyboard"
  - id: select/typeahead
    layer: ux
    level: must
    scope: component
    requirement: "Letters typed in quick succession search the enabled options, spaces included, and one letter repeated cycles through them"
    basis: "APG"
    check: "Unit test"
  - id: select/focus-stays
    layer: ux
    level: must
    scope: component
    requirement: "DOM focus stays on the control, and `aria-activedescendant` follows the highlighted option"
    basis: "APG, WCAG 2.4.3 (A)"
    check: "Unit test"
  - id: select/tab-commits
    layer: ux
    level: should
    scope: component
    requirement: "`Tab` picks the highlighted single option, or keeps what's toggled, and moves on"
    basis: "APG"
    check: "Unit test"
  - id: select/escape
    layer: ux
    level: must
    scope: component
    requirement: "`Escape` closes the panel without changing the value and stops there"
    basis: "Open Components, APG"
    check: "Unit test"
  - id: select/light-dismiss
    layer: ux
    level: must
    scope: component
    requirement: "A press outside closes it, and a press on the control while it's open doesn't reopen it"
    basis: "HTML"
    check: "Unit test"
  - id: select/remove-tag
    layer: ux
    level: must
    scope: component
    requirement: "A value can be removed by pointer and by keyboard, with `Backspace` on the closed control too"
    basis: "WCAG 2.1.1 (A)"
    check: "Unit test"
  - id: select/focus-after-remove
    layer: ux
    level: must
    scope: component
    requirement: "Focus goes to the next remove button, or the control when there's none after it"
    basis: "WCAG 2.4.3 (A)"
    check: "Unit test"
  - id: select/announce-removal
    layer: ux
    level: should
    scope: component
    requirement: "A removed value is announced through a polite live region"
    basis: "WCAG 4.1.3 (AA)"
    check: "Unit test, Screen reader"
  - id: select/form-submit
    layer: ux
    level: must
    scope: component
    requirement: "It submits under its `name` in both renderings, several values included"
    basis: "HTML"
    check: "Unit test"
  - id: select/form-reset
    layer: ux
    level: must
    scope: component
    requirement: "A form's reset returns it to its first value, or to nothing, in both renderings"
    basis: "HTML"
    check: "Unit test"
  - id: select/required
    layer: ux
    level: must
    scope: component
    requirement: "`required` reports a missing value in both renderings, and focus reaches the control"
    basis: "HTML"
    check: "Unit test"
  - id: select/validation-focus
    layer: ux
    level: must
    scope: component
    requirement: "Focus on the hidden `<select>` moves to the control in the same handler"
    basis: "HTML, Open Components"
    check: "Unit test"
  - id: select/no-auto-pick
    layer: ux
    level: must
    scope: component
    requirement: "With no value it starts blank, submits nothing and fires nothing on mount"
    basis: "Open Components, beyond HTML"
    check: "Unit test"
  - id: select/change-event
    layer: ux
    level: must
    scope: component
    requirement: "A selection in the panel fires a bubbling `change` from the `<select>`"
    basis: "HTML"
    check: "Unit test"
  - id: select/disabled
    layer: ux
    level: must
    scope: component
    requirement: "`disabled` goes on both controls, removes the tags' buttons and leaves its form"
    basis: "HTML"
    check: "Unit test"
  - id: select/read-only
    layer: ux
    level: must
    scope: component
    requirement: "`readonly` keeps the value in its form, opens nothing and turns off the fallback"
    basis: "Open Components"
    check: "Unit test"
  - id: select/native-fallback
    layer: ux
    level: must
    scope: component
    requirement: "`nativeOnTouch` shows the `<select>` on a coarse primary pointer, with the same value, form participation, validation and Field wiring"
    basis: "HTML, Open Components"
    check: "Unit test, Emulation"
  - id: select/no-hydration-mismatch
    layer: ux
    level: must
    scope: component
    requirement: "Both renderings are in the markup, CSS chooses what paints and the script moves only attributes, so there's no mismatch or shift"
    basis: "Open Components"
    check: "Unit test, Emulation"
  - id: select/in-a-dialog
    layer: ux
    level: must
    scope: both
    requirement: "Rendered inside a Dialog, its panel opens on top and `Escape` closes only the panel"
    basis: "HTML, Open Components"
    check: "Unit test"
  - id: select/panel-fits
    layer: ux
    level: must
    scope: component
    requirement: "The panel flips and shrinks to stay inside the screen, down to 320px wide"
    basis: "WCAG 1.4.10 (AA)"
    check: "Emulation, 200% zoom"
  - id: select/touch-text-size
    layer: ux
    level: should
    scope: component
    requirement: "On touch screens its text is at least 16px, so Safari on iOS doesn't zoom in"
    basis: "Open Components"
    check: "Emulation"
  - id: select/reduced-motion
    layer: ux
    level: should
    scope: component
    requirement: "The arrow and the panel don't animate for people who asked for less motion"
    basis: "WCAG 2.3.3 (AAA)"
    check: "Emulation"
  - id: select/forced-colors
    layer: ux
    level: must
    scope: component
    requirement: "Focus, the highlight, tags and disabled options stay visible in forced colours mode"
    basis: "Open Components"
    check: "Emulation"
  - id: select/right-component
    layer: ux
    level: should
    scope: usage
    requirement: "It's for picking out of many, while a few options use radios and a few with several answers use checkboxes"
    basis: "Open Components"
    check: "Review"
  - id: select/options-prop
    layer: dx
    level: must
    scope: component
    requirement: "Options, groups and separators are one typed `options` list, the same in both renderings"
    basis: "Open Components"
    check: "Unit test, Type check"
  - id: select/value
    layer: dx
    level: must
    scope: component
    requirement: "The value is a string, or an array with `multiple`, bound or unbound"
    basis: "Open Components"
    check: "Unit test"
  - id: select/typed
    layer: dx
    level: should
    scope: component
    requirement: "Its props and slots are typed, and the option types are exported"
    basis: "Open Components"
    check: "Type check"
  - id: select/same-names
    layer: dx
    level: should
    scope: component
    requirement: "It uses the names and sizes the other form controls use"
    basis: "Open Components"
    check: "Review"
  - id: select/attributes
    layer: dx
    level: must
    scope: component
    requirement: "Classes and styles go on the root, form attributes on the `<select>`, and the rest on the active control"
    basis: "Open Components"
    check: "Unit test"
  - id: select/in-a-field
    layer: dx
    level: must
    scope: both
    requirement: "In a Field, it takes the label, description, invalid and disabled state in both renderings"
    basis: "WCAG 1.3.1 (A)"
    check: "Unit test"
  - id: select/slots
    layer: dx
    level: should
    scope: component
    requirement: "Each part has a slot or tokens to restyle it, and the check mark and remove button stay"
    basis: "Open Components"
    check: "Unit test"
  - id: select/translatable
    layer: dx
    level: should
    scope: component
    requirement: "Every string it announces or adds comes through a prop"
    basis: "Open Components"
    check: "Unit test"
  - id: select/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found with `getByRole(\"combobox\", { name })`, in both renderings"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test, ARIA snapshot"
  - id: select/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup, with every ID made from its own `id` or `name`, and states change attributes, never elements"
    basis: "Open Components"
    check: "Unit test"
  - id: select/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "Its state, size and native mode are mirrored as `data-state`, `data-size` and `data-native`, and parts are marked with `data-slot`"
    basis: "Open Components"
    check: "Unit test"
  - id: select/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when it has no name, or no `id` or `name`"
    basis: "Open Components"
    check: "Unit test"
