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

component: Combobox
summary: Filters a list of suggestions as people type into a text field, and picks one option or several, keeping typed text only where you allow it, with a hidden select doing the form's work.
standard: https://opencomponents.dev/docs/components/combobox
element: div # the field, holding a text input, a hidden select, a toggle button and a popover panel
role: combobox # on the input, with aria-autocomplete="list"
props:
  options: { type: "SelectItem[]" } # required. The Select's 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 }
  allowCustomValue: { type: boolean, default: false } # keeps typed text as the value, for one value only
  creatable: { type: boolean, default: false } # ends the list with an option that adds the typed text
  createLabel: { type: function } # (text) => 'Create "Lyon"'
  filter: { type: function } # (option, text) => boolean. Any part of the label, ignoring case and accents
  emptyLabel: { type: string, default: No results }
  resultsLabel: { type: function } # (count) => "3 results"
  toggleLabel: { type: string, default: Suggestions }
  removeLabel: { type: function } # (label) => "Remove France"
  removedLabel: { type: function } # (label) => "France removed"
slots:
  leading: An icon before the text, hidden from assistive technologies.
  arrow: The chevron in the toggle, which turns as the panel opens.
  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 option's label while the text shows it and in each tag, hidden from assistive technologies.
events:
  create: "People picked the create option, with the text they typed."
  change: "The native change event, from the select, once the value changes, never on every key."
  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]"
  creating: "[data-slot=option][data-create]"
keyboard:
  Letters: Type and filter, opening it with nothing highlighted.
  ArrowDown: Opens it on the first option, or moves the highlight, skipping disabled options. With Alt, opens it with nothing highlighted.
  ArrowUp: Opens it on the last option, or moves the highlight. With Alt, closes it and keeps the text.
  PageDown: Moves ten options, stopping at the end.
  PageUp: Moves ten options, stopping at the start.
  Enter: Picks the highlighted option and closes it, or with multiple, toggles it. With nothing highlighted, or closed, settles the text, and closed, then submits the form.
  Tab: Picks the highlighted option, or settles the text, closes it and moves on. With multiple, settles the text and keeps what's toggled.
  Escape: Closes it and keeps the text, then clears the text, and goes no further until there's nothing to clear.
  Backspace: Deletes text. With multiple and no text, removes the last value.
  Home: Moves the caret to the start.
  End: Moves the caret to the end.
parts: [native, control, icon, tags, tag, tag-label, tag-remove, leading, arrow, panel, group, group-label, separator, option, option-label, check, empty] # data-slot, in this order. arrow is the toggle button. icon also comes before tag-label and option-label, and empty takes the create option's place when nothing matches
tokens:
  theme: ["--color--neutral-text", "--color--focus", "--color--error-text"]
  variables: [--combobox--empty--padding-block, --combobox--empty--padding-inline, --combobox--empty--color]
  shared: [--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] # the Select's, set on both roots
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: combobox/visible-focus
    layer: ui
    level: must
    scope: component
    requirement: "The field shows a focus ring while its text has focus, and the highlighted option an outline at least 2px thick"
    basis: "WCAG 2.4.7 (AA)"
    check: "Keyboard, Visual regression test"
  - id: combobox/selected-not-colour
    layer: ui
    level: must
    scope: component
    requirement: "A picked option shows a check mark as well as its weight, and its tag, with `multiple`"
    basis: "WCAG 1.4.1 (A)"
    check: "Visual regression test"
  - id: combobox/contrast
    layer: ui
    level: must
    scope: both
    requirement: "The text, the placeholder and \"No results\" have 4.5:1, and the border, the chevron and the focus ring 3:1, in light and dark mode"
    basis: "WCAG 1.4.3, 1.4.11 (AA)"
    check: "Contrast checker"
  - id: combobox/target-size
    layer: ui
    level: must
    scope: component
    requirement: "The toggle and each remove button are at least 24 × 24px to press, and 44px tall where a pointer is coarse, like the options"
    basis: "Open Components, beyond WCAG 2.5.8 (AA)"
    check: "Emulation, Review"
  - id: combobox/shared-tokens
    layer: ui
    level: should
    scope: component
    requirement: "The field, the panel, the options, the group labels, the separators and the tags read the Select's variables, so one class restyles both"
    basis: "Open Components"
    check: "Review"
  - id: combobox/sizes
    layer: ui
    level: should
    scope: component
    requirement: "Its five sizes are the Input's and the Select's, and the panel's text and the tags follow the field's size"
    basis: "Open Components"
    check: "Visual regression test"
  - id: combobox/text-size-touch
    layer: ui
    level: must
    scope: component
    requirement: "Its text is at least 16px where any pointer is coarse, so Safari doesn't zoom in on focus"
    basis: "Open Components"
    check: "Emulation"
  - id: combobox/no-layout-shift
    layer: ui
    level: must
    scope: component
    requirement: "Opening the panel, filtering and picking move nothing else on the page"
    basis: "Open Components"
    check: "Visual regression test"
  - id: combobox/reflow
    layer: ui
    level: must
    scope: component
    requirement: "The field and the panel fit a 320px-wide screen without scrolling sideways, and long labels wrap"
    basis: "WCAG 1.4.10 (AA)"
    check: "Emulation"
  - id: combobox/reduced-motion
    layer: ui
    level: should
    scope: component
    requirement: "The chevron and the panel don't animate when people ask for less motion"
    basis: "WCAG 2.3.3 (AAA)"
    check: "Emulation"
  - id: combobox/forced-colors
    layer: ui
    level: should
    scope: component
    requirement: "Focus, the highlight, the panel's edge and disabled options stay visible in forced colours"
    basis: "Open Components"
    check: "Emulation"
  - id: combobox/editable-pattern
    layer: ux
    level: must
    scope: component
    requirement: "The text field has the `combobox` role, `aria-autocomplete=\"list\"`, `aria-expanded` and `aria-controls`, which points at a `listbox`"
    basis: "APG"
    check: "Unit test, ARIA snapshot"
  - id: combobox/focus-in-text
    layer: ux
    level: must
    scope: component
    requirement: "Focus stays in the text field, and `aria-activedescendant` points at the highlighted option while the panel is open"
    basis: "APG"
    check: "Unit test"
  - id: combobox/name
    layer: ux
    level: must
    scope: usage
    requirement: "It has a visible label, from a Field or a `<label for>`"
    basis: "WCAG 1.3.1, 3.3.2 (A)"
    check: "axe `label`, Review"
  - id: combobox/keyboard
    layer: ux
    level: must
    scope: component
    requirement: "Every key in the contract does what it says, and nothing needs a pointer"
    basis: "WCAG 2.1.1 (A)"
    check: "Unit test, Keyboard"
  - id: combobox/typing-edits
    layer: ux
    level: must
    scope: component
    requirement: "Letters, `Space`, `Home`, `End` and the side arrows edit the text, and never move the highlight"
    basis: "APG"
    check: "Unit test"
  - id: combobox/manual-selection
    layer: ux
    level: must
    scope: component
    requirement: "Typing highlights nothing, so `Enter` and blur never pick an option people didn't choose"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/open-on-request
    layer: ux
    level: should
    scope: component
    requirement: "Focus alone doesn't open the panel, while typing, the arrow keys and the toggle do"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/press-outside
    layer: ux
    level: should
    scope: component
    requirement: "A press outside the field closes the panel, and a press in its text doesn't"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/escape
    layer: ux
    level: must
    scope: both
    requirement: "`Escape` closes the panel, then clears the text, and goes no further, so a Dialog around it stays open"
    basis: "APG"
    check: "Unit test"
  - id: combobox/filter
    layer: ux
    level: should
    scope: component
    requirement: "The default filter matches any part of a label, ignoring case and accents, and hides groups and separators with no match"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/no-results
    layer: ux
    level: must
    scope: component
    requirement: "With no match, the panel says so, in text that isn't an option, and screen readers hear it"
    basis: "WCAG 4.1.3 (AA)"
    check: "Unit test, axe `aria-required-children`"
  - id: combobox/result-count
    layer: ux
    level: should
    scope: component
    requirement: "The number of matches is announced once typing pauses, never on every key"
    basis: "WCAG 4.1.3 (AA)"
    check: "Unit test, Screen reader"
  - id: combobox/settle-text
    layer: ux
    level: must
    scope: component
    requirement: "Text that's the label of an option the panel lists picks it, and other text goes back to the value's text when people leave, unless a prop keeps it"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/opt-in-values
    layer: ux
    level: must
    scope: both
    requirement: "Typed values are kept only with `allowCustomValue` or `creatable`, which are off by default, and the page says what each submits"
    basis: "Open Components"
    check: "Unit test, Review"
  - id: combobox/native-form
    layer: ux
    level: must
    scope: component
    requirement: "It submits under its `name`, an option as its `value` and a typed value as typed, blocks a submit while required and empty, and resets with its form"
    basis: "HTML"
    check: "Unit test"
  - id: combobox/removable-tags
    layer: ux
    level: must
    scope: component
    requirement: "With `multiple`, each tag has a named button that removes it, focus moves on to the next one, and `Backspace` in empty text removes the last"
    basis: "WCAG 2.1.1, 4.1.2 (A)"
    check: "Unit test"
  - id: combobox/announce-removal
    layer: ux
    level: should
    scope: component
    requirement: "Removing a value is announced"
    basis: "WCAG 4.1.3 (AA)"
    check: "Unit test"
  - id: combobox/disabled-readonly
    layer: ux
    level: must
    scope: component
    requirement: "Disabled leaves the form and can't open, and read-only keeps the value in the form and can't change it"
    basis: "HTML"
    check: "Unit test"
  - id: combobox/keyboard-fit
    layer: ux
    level: should
    scope: component
    requirement: "While an on-screen keyboard is up, the panel opens in the part of the screen it leaves"
    basis: "Open Components"
    check: "Unit test, Emulation"
  - id: combobox/autocomplete
    layer: ux
    level: should
    scope: usage
    requirement: "`autocomplete` names the field's purpose where it asks people about themselves, and stays `off` elsewhere"
    basis: "WCAG 1.3.5 (AA)"
    check: "Review"
  - id: combobox/select-names
    layer: dx
    level: should
    scope: component
    requirement: "It takes the Select's names for what the two share: `options`, `multiple`, `size`, `placeholder`, `disabled`, `readonly`, `required` and the slots"
    basis: "Open Components"
    check: "Review"
  - id: combobox/typed
    layer: dx
    level: must
    scope: component
    requirement: "Its props, events and slots are typed, and it takes the Select's exported option types"
    basis: "Open Components"
    check: "Type check"
  - id: combobox/value
    layer: dx
    level: must
    scope: component
    requirement: "The value is a string, or an array with `multiple`, bound or unbound from a `value` attribute, and the text follows a bound value as it changes"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/native-events
    layer: dx
    level: should
    scope: component
    requirement: "`change` and `input` fire from its `<select>` when the value changes, never on every key"
    basis: "HTML"
    check: "Unit test"
  - id: combobox/attributes
    layer: dx
    level: must
    scope: component
    requirement: "Classes and styles go on the root, `name` and `form` on the `<select>`, and `id`, ARIA and `autocomplete` on the text field"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/in-a-field
    layer: dx
    level: must
    scope: both
    requirement: "In a Field, it takes the label, description, invalid and disabled state"
    basis: "WCAG 1.3.1 (A)"
    check: "Unit test"
  - id: combobox/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: combobox/create-event
    layer: dx
    level: must
    scope: component
    requirement: "Picking the create option fires `create` with the text people typed"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/custom-filter
    layer: dx
    level: should
    scope: component
    requirement: "A `filter` prop replaces how options match what people typed"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/translatable
    layer: dx
    level: should
    scope: component
    requirement: "Every string it shows or announces comes through a prop"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/exposed
    layer: dx
    level: should
    scope: component
    requirement: "A ref gets the text field and the `<select>`"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found with `getByRole(\"combobox\", { name })`, and its options with `getByRole(\"option\", { name })`"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test, ARIA snapshot"
  - id: combobox/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup, every ID is made from its own `id` or `name`, and options keep their IDs as the list narrows"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "Its state and size are mirrored as `data-state` and `data-size`, and parts are marked with `data-slot`"
    basis: "Open Components"
    check: "Unit test"
  - id: combobox/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when it has no name, no `id` or `name`, or `allowCustomValue` with `multiple`"
    basis: "Open Components"
    check: "Unit test"
