{
  "name": "Combobox",
  "source": "src/components/Combobox.tsx",
  "propsType": "ComboboxProps",
  "description": "Combobox — a text field that filters a list, with one selected value.\n\nDrawn in Figma as `Combobox` (1370:2359) — three sizes by seven states, the\nsame axes `Input` carries, because the field IS Input's box. The open list is\na second Figma component, `Combobox Menu`, with no React export behind it.\n\nWHY THIS IS NOT `Select` WITH A SEARCH BOX\n\n`Select` wraps a native `<select>`, whose list the browser owns: it cannot be\nfiltered, and its rows can only hold text. Everything above about twenty\noptions needs filtering, and `Select`'s own contract has pointed at \"a\ncombobox — not yet in this system\" since it was written. This is it.\n\nThe cost is that everything the native control gave away for free — keyboard\nhandling, the mobile picker, screen-reader semantics — now has to be built.\nReact Aria's `useComboBox` builds it: `role=\"combobox\"` with `aria-expanded`,\n`aria-controls` and `aria-activedescendant`, arrow keys that move a virtual\nfocus while the real focus stays in the input, and Escape that reverts.\n\nFILTERING IS LOCALE-AWARE, NOT `toLowerCase().includes()`\n\n`useFilter({ sensitivity: 'base' })` is `Intl.Collator` underneath, so\n\"resume\" matches \"résumé\" and Turkish dotted/dotless I behave the way a\nTurkish reader expects. The lowercase-and-includes version fails both, and\nfails them silently in exactly the locales least likely to be tested.",
  "import": "import { Combobox } from 'ionbase-ui';",
  "status": "stable",
  "summary": "A text field that filters a list of options down to one selected value.",
  "useWhen": [
    "the list is long enough that scanning it is worse than typing",
    "the user picks exactly one value from a known set",
    "the options carry a second line of context the native <select> cannot render"
  ],
  "useInstead": [
    {
      "when": "the list is short and fully scannable",
      "use": "Select",
      "why": "the native control brings keyboard handling and the mobile platform picker for free; this rebuilds all of it"
    },
    {
      "when": "more than one value may be picked",
      "use": "MultiSelect",
      "why": "its input holds only the filter text, the list stays open between picks, and the chosen values show as tags"
    },
    {
      "when": "there are two or three mutually exclusive choices worth showing at once",
      "use": "RadioGroup"
    },
    {
      "when": "the value is free text and the list is only a convenience",
      "use": "Input",
      "why": "or set `allowsCustomValue`, which is the same decision made explicitly"
    },
    {
      "when": "the rows are commands rather than values",
      "use": "Menu",
      "why": "options are selected and stay selected; menu items are invoked and the menu closes"
    }
  ],
  "variants": {
    "size": {
      "sm": {
        "use": "dense forms, table filters"
      },
      "md": {
        "use": "the default"
      },
      "lg": {
        "use": "prominent single-field forms"
      }
    }
  },
  "a11y": {
    "guarantees": [
      "role=combobox with aria-expanded, aria-controls and aria-activedescendant, from react-aria's useComboBox",
      "real focus never leaves the input; arrow keys move a virtual focus through the list, which is what keeps typing and browsing possible at once",
      "the popover is non-modal, so the page behind stays readable and focus stays in the field",
      "Escape reverts to the last committed value rather than clearing the field",
      "filtering is Intl.Collator-based, so accents and locale-specific casing behave correctly",
      "the empty message is a sibling of the listbox, not an option in it, so it is never counted, focused or selectable",
      "the menu matches the field's width, clamped to the space right of it so a full-bleed field cannot push the menu past the viewport edge"
    ],
    "requires": [
      "`label`, or an `aria-label` when no visible label is rendered",
      "`emptyLabel` wording that says why nothing is listed, if the default is too terse"
    ],
    "notes": [
      "While the list is open, React Aria hides everything outside the field and the list from assistive technology. That is the platform pattern for a combobox; a whole-page accessibility scan taken with the list open reports the hidden page, not this control."
    ]
  },
  "antiPatterns": [
    {
      "dont": "giving the disclosure button a tab stop",
      "why": "the input already opens the list with ArrowDown. A second tab stop doubles the cost of tabbing past every combobox in a form and adds nothing"
    },
    {
      "dont": "styling the focused option with :hover or :focus",
      "why": "no option is ever either — focus is virtual. Use [data-focused], which react-aria sets for both pointer and keyboard"
    },
    {
      "dont": "posting the input's text in a form",
      "why": "the text is the label and the value is the key; pass `name` and the component posts the key in a hidden input"
    },
    {
      "dont": "filtering with toLowerCase().includes()",
      "why": "it fails on accents and on Turkish dotted/dotless I, silently, in the locales least likely to be tested"
    },
    {
      "dont": "writing a width onto the menu from a ResizeObserver or any effect that runs while it is open",
      "why": "a positioned popover that changes size feeds back into react-aria's positioning, and any scrollbar that results is read by useCloseOnScroll as a real scroll. The menu opens and closes within about a hundred milliseconds, which presents as a keyboard bug rather than a layout one"
    }
  ],
  "slots": {
    "emptyLabel": {
      "expects": "what to show when the filter matches nothing"
    }
  },
  "stylesheet": "src/styles/combobox.css",
  "tokens": [
    "--border-subtle",
    "--border-width-default",
    "--font-family-sans",
    "--font-weight-medium",
    "--icon-disabled",
    "--icon-secondary",
    "--ion-duration-base",
    "--ion-ease-out",
    "--ion-input-icon-size",
    "--ion-shadow-shadow-lg",
    "--radius-md",
    "--radius-sm",
    "--spacing-12",
    "--spacing-2",
    "--spacing-4",
    "--spacing-6",
    "--spacing-8",
    "--surface-hover",
    "--surface-primary-subtle",
    "--surface-raised",
    "--text-default",
    "--text-disabled",
    "--text-tertiary",
    "--type-body",
    "--type-body-line-height",
    "--type-body-sm",
    "--type-body-sm-line-height"
  ],
  "props": {
    "options": {
      "type": "readonly ComboboxOption[]",
      "required": true,
      "origin": "own",
      "description": "The full option list. Filtering happens here, against what is typed."
    },
    "label": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Field label. Required for a usable control — see `a11y.requires`."
    },
    "description": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Helper text below the field."
    },
    "errorMessage": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Replaces the helper text when `isInvalid` is set."
    },
    "isInvalid": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own"
    },
    "isDisabled": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own"
    },
    "isReadOnly": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own"
    },
    "size": {
      "type": "ComboboxSize | undefined",
      "required": false,
      "origin": "own",
      "description": "Matches Input's `Size` variant: Small, Medium, Large.",
      "values": [
        "sm",
        "md",
        "lg"
      ]
    },
    "placeholder": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "selectedKey": {
      "type": "string | null | undefined",
      "required": false,
      "origin": "own",
      "description": "Controlled selection. `null` means nothing is selected."
    },
    "defaultSelectedKey": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "onSelectionChange": {
      "type": "((key: string | null) => void) | undefined",
      "required": false,
      "origin": "own"
    },
    "inputValue": {
      "type": "string | undefined",
      "required": false,
      "origin": "own",
      "description": "Controlled text. Usually only needed for async/remote filtering."
    },
    "defaultInputValue": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "onInputChange": {
      "type": "((value: string) => void) | undefined",
      "required": false,
      "origin": "own"
    },
    "allowsCustomValue": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own",
      "description": "Accept text that matches no option. Off by default: a combobox whose value\nis a free string is a text field with suggestions, and the caller should\nhave to say that is what they want."
    },
    "menuTrigger": {
      "type": "\"focus\" | \"input\" | \"manual\" | undefined",
      "required": false,
      "origin": "own",
      "description": "What opens the list. `input` — react-aria's default and this one — opens it\non the first keystroke; the chevron opens it on demand. `focus` opens it\nthe moment the field is tabbed into, which is loud in a long form.",
      "values": [
        "focus",
        "input",
        "manual"
      ]
    },
    "emptyLabel": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Shown in place of the list when nothing matches."
    },
    "buttonLabel": {
      "type": "string | undefined",
      "required": false,
      "origin": "own",
      "description": "Accessible label for the disclosure button."
    },
    "name": {
      "type": "string | undefined",
      "required": false,
      "origin": "own",
      "description": "Posts the selected value under this name, for an uncontrolled form."
    },
    "className": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "wrapperClassName": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "id": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    }
  },
  "propCounts": {
    "own": 23,
    "aria": 0,
    "dom": 0,
    "other": 0
  }
}
