@commercetools/nimbus-mcp 3.5.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/data/docs/route-manifest.json +275 -9
  2. package/data/docs/routes/components-buttons-toggle-button-group.json +143 -18
  3. package/data/docs/routes/components-buttons-toggle-button.json +72 -15
  4. package/data/docs/routes/components-chat.json +36 -0
  5. package/data/docs/routes/components-content.json +36 -0
  6. package/data/docs/routes/components-data-display-data-table.json +1 -1
  7. package/data/docs/routes/components-feedback-popover.json +643 -0
  8. package/data/docs/routes/components-inputs-combo-box.json +33 -11
  9. package/data/docs/routes/components-inputs-search-input.json +29 -7
  10. package/data/docs/routes/components-inputs-select-input.json +23 -1
  11. package/data/docs/routes/components-layout-stack.json +1 -1
  12. package/data/docs/routes/components-navigation-toolbar.json +1 -1
  13. package/data/docs/routes/components-navigation-tree.json +2 -2
  14. package/data/docs/routes/home-contribute.json +29 -8
  15. package/data/docs/routes/home-getting-started-collaboration.json +657 -0
  16. package/data/docs/search-index.json +1 -1
  17. package/data/docs/types/AccordionContent.json +12 -12
  18. package/data/docs/types/AccordionHeader.json +46 -46
  19. package/data/docs/types/AccordionItem.json +20 -20
  20. package/data/docs/types/AccordionRoot.json +12 -12
  21. package/data/docs/types/ActivityIndicator.json +3 -2
  22. package/data/docs/types/AlertDismissButton.json +42 -42
  23. package/data/docs/types/Button.json +47 -46
  24. package/data/docs/types/Calendar.json +43 -42
  25. package/data/docs/types/Checkbox.json +30 -29
  26. package/data/docs/types/Code.json +2 -1
  27. package/data/docs/types/ComboBoxListBox.json +34 -34
  28. package/data/docs/types/ComboBoxOption.json +20 -20
  29. package/data/docs/types/ComboBoxPopover.json +67 -86
  30. package/data/docs/types/ComboBoxRoot.json +15 -0
  31. package/data/docs/types/ComboBoxSection.json +14 -14
  32. package/data/docs/types/DataTableBody.json +10 -10
  33. package/data/docs/types/DataTableCell.json +18 -18
  34. package/data/docs/types/DataTableColumn.json +28 -28
  35. package/data/docs/types/DataTableHeader.json +10 -10
  36. package/data/docs/types/DataTableManager.json +3 -5
  37. package/data/docs/types/DataTableRow.json +24 -24
  38. package/data/docs/types/DateInput.json +33 -32
  39. package/data/docs/types/DatePicker.json +32 -32
  40. package/data/docs/types/DateRangePicker.json +46 -46
  41. package/data/docs/types/DateRangePickerField.json +46 -46
  42. package/data/docs/types/DialogCloseTrigger.json +42 -42
  43. package/data/docs/types/DraggableListField.json +28 -28
  44. package/data/docs/types/DraggableListItem.json +18 -18
  45. package/data/docs/types/DraggableListRoot.json +30 -30
  46. package/data/docs/types/DrawerCloseTrigger.json +42 -42
  47. package/data/docs/types/DropZone.json +14 -14
  48. package/data/docs/types/FloatingActionButton.json +44 -44
  49. package/data/docs/types/Group.json +9 -7
  50. package/data/docs/types/Heading.json +3 -2
  51. package/data/docs/types/Icon.json +3 -2
  52. package/data/docs/types/IconButton.json +47 -46
  53. package/data/docs/types/IconToggleButton.json +71 -35
  54. package/data/docs/types/Link.json +3 -2
  55. package/data/docs/types/LoadingSpinner.json +3 -2
  56. package/data/docs/types/LocalizedField.json +3 -2
  57. package/data/docs/types/MakeElementFocusable.json +2 -2
  58. package/data/docs/types/MenuItem.json +22 -22
  59. package/data/docs/types/MenuRoot.json +31 -31
  60. package/data/docs/types/MenuSection.json +16 -16
  61. package/data/docs/types/MenuSubmenuTrigger.json +4 -4
  62. package/data/docs/types/MenuTrigger.json +46 -46
  63. package/data/docs/types/MoneyInput.json +4 -2
  64. package/data/docs/types/MultilineTextInput.json +27 -26
  65. package/data/docs/types/MultilineTextInputField.json +27 -26
  66. package/data/docs/types/NumberInput.json +14 -12
  67. package/data/docs/types/NumberInputField.json +10 -10
  68. package/data/docs/types/PasswordInput.json +24 -24
  69. package/data/docs/types/PasswordInputField.json +24 -24
  70. package/data/docs/types/Popover.json +13 -0
  71. package/data/docs/types/PopoverContent.json +42 -0
  72. package/data/docs/types/PopoverRoot.json +651 -0
  73. package/data/docs/types/PopoverTrigger.json +914 -0
  74. package/data/docs/types/ProgressBar.json +17 -15
  75. package/data/docs/types/RadioInputOption.json +17 -17
  76. package/data/docs/types/RadioInputRoot.json +10 -10
  77. package/data/docs/types/RangeCalendar.json +42 -42
  78. package/data/docs/types/RangeSlider.json +14 -14
  79. package/data/docs/types/Region.json +3 -2
  80. package/data/docs/types/RichTextInput.json +4 -2
  81. package/data/docs/types/SearchInput.json +60 -28
  82. package/data/docs/types/SearchInputField.json +60 -28
  83. package/data/docs/types/SelectOption.json +20 -20
  84. package/data/docs/types/SelectOptionGroup.json +14 -14
  85. package/data/docs/types/SelectOptions.json +34 -34
  86. package/data/docs/types/SelectRoot.json +67 -52
  87. package/data/docs/types/Separator.json +4 -2
  88. package/data/docs/types/Slider.json +18 -16
  89. package/data/docs/types/SplitButton.json +12 -10
  90. package/data/docs/types/Switch.json +11 -10
  91. package/data/docs/types/TagGroupRoot.json +12 -12
  92. package/data/docs/types/TagGroupTag.json +16 -16
  93. package/data/docs/types/TagGroupTagList.json +12 -12
  94. package/data/docs/types/TextInput.json +27 -26
  95. package/data/docs/types/TextInputField.json +27 -26
  96. package/data/docs/types/TimeInput.json +30 -28
  97. package/data/docs/types/ToastOutlet.json +4 -2
  98. package/data/docs/types/ToggleButton.json +74 -37
  99. package/data/docs/types/ToggleButtonGroup.json +4 -2
  100. package/data/docs/types/ToggleButtonGroupButton.json +547 -55
  101. package/data/docs/types/ToggleButtonGroupRoot.json +48 -16
  102. package/data/docs/types/Toolbar.json +11 -9
  103. package/data/docs/types/Tooltip.json +3 -2
  104. package/data/docs/types/TooltipContent.json +26 -26
  105. package/data/docs/types/TooltipRoot.json +16 -16
  106. package/data/docs/types/TreeIndicator.json +40 -40
  107. package/data/docs/types/TreeItem.json +20 -20
  108. package/data/docs/types/TreeRoot.json +26 -26
  109. package/data/docs/types/VisuallyHidden.json +4 -4
  110. package/data/docs/types/createItemsFromCsvDrop.json +2 -2
  111. package/data/docs/types/createItemsFromDirectoryDrop.json +2 -2
  112. package/data/docs/types/createItemsFromFileDrop.json +2 -2
  113. package/data/docs/types/createItemsFromImageDrop.json +2 -2
  114. package/data/docs/types/createItemsFromJsonDrop.json +2 -2
  115. package/data/docs/types/manifest.json +4 -0
  116. package/data/docs/types/useTree.json +8 -8
  117. package/data/style-props-summary.json +408 -0
  118. package/dist/index.js +156 -43
  119. package/package.json +8 -8
@@ -291,7 +291,7 @@
291
291
  ]
292
292
  },
293
293
  "dev": {
294
- "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { ComboBox, type ComboBoxRootProps } from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nComboBox supports two patterns for defining options:\n\n#### With items prop (recommended for dynamic data)\n\nUse the `items` prop when working with data from APIs, state, or dynamic\nsources:\n\n```jsx live-dev\nconst App = () => {\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n { id: \"giraffe\", name: \"Giraffe\" },\n ];\n\n return (\n <ComboBox.Root\n items={animals}\n placeholder=\"Select an animal...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n#### With static children (requires textValue)\n\nFor static lists, you **must** include the `textValue` prop on each option for\nselections to display correctly:\n\n```jsx live-dev\nconst App = () => (\n <ComboBox.Root\n placeholder=\"Select an animal...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\" textValue=\"Koala\">\n Koala\n </ComboBox.Option>\n <ComboBox.Option id=\"panda\" textValue=\"Panda\">\n Panda\n </ComboBox.Option>\n <ComboBox.Option id=\"giraffe\" textValue=\"Giraffe\">\n Giraffe\n </ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n);\n```\n\n> [!WARNING]\\\n> Without `textValue` on static children, selected values will not display in\n> the input field or as tags. Always include `textValue` when not using the\n> `items` prop.\n\n## Usage examples\n\n### Size options\n\nThe `sm` and `md` size variants are available to match your interface density:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n size=\"sm\"\n items={options}\n placeholder=\"Small size...\"\n aria-label=\"Small combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <ComboBox.Root\n size=\"md\"\n items={options}\n placeholder=\"Medium size...\"\n aria-label=\"Medium combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Stack>\n );\n};\n```\n\n### Visual variants\n\nChoose between `solid` and `ghost` variants to match your design context:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n variant=\"solid\"\n items={options}\n placeholder=\"Solid variant...\"\n aria-label=\"Solid combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <ComboBox.Root\n variant=\"ghost\"\n items={options}\n placeholder=\"Ghost variant...\"\n aria-label=\"Ghost combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Stack>\n );\n};\n```\n\n### Single-select mode\n\nBy default, ComboBox operates in single-select mode where users can select one\noption at a time:\n\n```jsx live-dev\nconst App = () => {\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const fruits = [\n { id: \"apple\", name: \"Apple\" },\n { id: \"banana\", name: \"Banana\" },\n { id: \"orange\", name: \"Orange\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n items={fruits}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"single\"\n placeholder=\"Select a fruit...\"\n aria-label=\"Select a fruit\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">\n {selectedKeys.length > 0\n ? `Selected: ${selectedKeys[0]}`\n : \"No selection\"}\n </Text>\n </Stack>\n );\n};\n```\n\n### Multi-select mode\n\nEnable multi-select to allow users to choose multiple options displayed as tags:\n\n```jsx live-dev\nconst App = () => {\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n { id: \"giraffe\", name: \"Giraffe\" },\n { id: \"elephant\", name: \"Elephant\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n items={animals}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"multiple\"\n placeholder=\"Select animals...\"\n aria-label=\"Select animals\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">\n Selected: {selectedKeys.length} item\n {selectedKeys.length !== 1 ? \"s\" : \"\"}\n </Text>\n </Stack>\n );\n};\n```\n\n### Leading element\n\nAdd a leading icon to enhance the visual context:\n\n```jsx live-dev\nconst App = () => {\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n { id: \"giraffe\", name: \"Giraffe\" },\n ];\n\n return (\n <ComboBox.Root\n items={animals}\n leadingElement={<Icons.Search aria-hidden=\"true\" />}\n placeholder=\"Search...\"\n aria-label=\"Search for an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Disabled state\n\nUse `isDisabled` to disable the combobox:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <ComboBox.Root\n items={options}\n isDisabled\n placeholder=\"Disabled...\"\n aria-label=\"Disabled combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Invalid state\n\nMark the combobox as invalid for form validation:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <ComboBox.Root\n items={options}\n isInvalid\n placeholder=\"Select an option...\"\n aria-label=\"Invalid combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Read-only state\n\nUse `isReadOnly` for display-only scenarios:\n\n```jsx live-dev\nconst App = () => {\n const fruits = [\n { id: \"apple\", name: \"Apple\" },\n { id: \"banana\", name: \"Banana\" },\n ];\n\n return (\n <ComboBox.Root\n items={fruits}\n isReadOnly\n selectedKeys={[\"apple\"]}\n aria-label=\"Read-only combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Option groups\n\nOrganize options into sections with headers:\n\n```jsx live-dev\nconst App = () => {\n const foodCategories = [\n {\n id: \"fruits\",\n name: \"Fruits\",\n children: [\n { id: \"apple\", name: \"Apple\" },\n { id: \"banana\", name: \"Banana\" },\n { id: \"orange\", name: \"Orange\" },\n ],\n },\n {\n id: \"vegetables\",\n name: \"Vegetables\",\n children: [\n { id: \"carrot\", name: \"Carrot\" },\n { id: \"broccoli\", name: \"Broccoli\" },\n { id: \"spinach\", name: \"Spinach\" },\n ],\n },\n ];\n\n return (\n <ComboBox.Root\n items={foodCategories}\n placeholder=\"Select a food...\"\n aria-label=\"Select a food\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(category) => (\n <ComboBox.Section\n id={category.id}\n label={category.name}\n items={category.children}\n >\n {(item) => (\n <ComboBox.Option id={item.id} textValue={item.name}>\n {item.name}\n </ComboBox.Option>\n )}\n </ComboBox.Section>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Dynamic options with items\n\n`ComboBox.ListBox` supports the `items` prop, a pattern unique to React Aria\nComponents (inherited from React Aria's `ListBox` component). This pattern\nprovides an efficient way to render dynamic or large lists of options.\n\n**When to use `items` vs static children:**\n\n- **Use `items` when:**\n - Rendering options from dynamic data (API responses, filtered lists, computed\n arrays)\n - Working with large lists (better performance through virtualization support)\n - Options change frequently based on user input or state\n - You need to map over data structures\n\n- **Use static children when:**\n - Options are known at compile time\n - You have a small, fixed set of options\n - You prefer the simpler JSX syntax for readability\n\n**Static children example:**\n\n```jsx live-dev\nconst App = () => (\n <ComboBox.Root\n placeholder=\"Select an animal...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\" textValue=\"Koala\">\n Koala\n </ComboBox.Option>\n <ComboBox.Option id=\"panda\" textValue=\"Panda\">\n Panda\n </ComboBox.Option>\n <ComboBox.Option id=\"giraffe\" textValue=\"Giraffe\">\n Giraffe\n </ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n);\n```\n\n**Dynamic items example:**\n\nWhen using `items`, children must be a render function that receives each item\nand returns a `ComboBox.Option`. This example demonstrates using `items` with\ndata that could come from an API, state, or computed values:\n\n```jsx live-dev\nconst App = () => {\n // In a real app, this data might come from an API, props, or state.\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n { id: \"giraffe\", name: \"Giraffe\" },\n { id: \"elephant\", name: \"Elephant\" },\n ];\n\n return (\n <ComboBox.Root\n items={animals}\n placeholder=\"Select an animal...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\nThe `items` pattern enables React Aria to optimize rendering and provides better\nsupport for features like virtualization, which can improve performance with\nlarge option lists.\n\n> [!TIP]\\\n> See [React Aria's ComboBox documentation](https://react-spectrum.adobe.com/react-aria/ComboBox.html#sections)\n> for complete API reference and advanced usage, including static and dynamic\n> items.\n\n### Custom filtering\n\nCustomize how options are filtered using the `filter` prop:\n\n```jsx live-dev\nconst App = () => {\n const animals = [\n { id: 1, name: \"Koala\", category: \"Marsupial\" },\n { id: 2, name: \"Panda\", category: \"Bear\" },\n { id: 3, name: \"Giraffe\", category: \"Mammal\" },\n { id: 4, name: \"Elephant\", category: \"Mammal\" },\n ];\n\n // Use the built-in multi-property filter\n const multiPropertyFilter = useCallback(\n ComboBox.filters.createMultiPropertyFilter([\"name\", \"category\"]),\n []\n );\n\n return (\n <ComboBox.Root\n items={animals}\n filter={multiPropertyFilter}\n aria-label=\"Search animals\"\n placeholder=\"Search by name or category...\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name} ({item.category})\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Custom options creation\n\nAllow users to create new options by typing and pressing Enter:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { id: 1, name: \"Apple\" },\n { id: 2, name: \"Banana\" },\n { id: 3, name: \"Orange\" },\n ]);\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const handleCreateOption = useCallback((newItem) => {\n console.log(\"Created:\", newItem.name);\n }, []);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n items={items}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"multiple\"\n allowsCustomOptions\n getNewOptionData={(inputValue) => ({\n id: Date.now(),\n name: inputValue,\n })}\n onCreateOption={handleCreateOption}\n aria-label=\"Select or create fruits\"\n placeholder=\"Type to search or create...\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name}\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">\n Type a custom fruit and press Enter to create it!\n </Text>\n </Stack>\n );\n};\n```\n\n### Async data loading\n\nLoad options asynchronously with built-in debouncing and loading states:\n\n```jsx live-dev\nconst App = () => {\n // Simulated API call\n const loadPokemon = useCallback(async (filterText, signal) => {\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n const allPokemon = [\n { id: 1, name: \"Bulbasaur\" },\n { id: 2, name: \"Charmander\" },\n { id: 3, name: \"Squirtle\" },\n { id: 4, name: \"Pikachu\" },\n { id: 5, name: \"Jigglypuff\" },\n ];\n\n return allPokemon.filter((p) =>\n p.name.toLowerCase().includes(filterText.toLowerCase())\n );\n }, []);\n\n return (\n <ComboBox.Root\n async={{\n load: loadPokemon,\n minSearchLength: 2,\n debounce: 300,\n }}\n aria-label=\"Search Pokemon\"\n placeholder=\"Type to search...\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name}\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Controlled input value\n\nControl the input text for advanced scenarios:\n\n```jsx live-dev\nconst App = () => {\n const [inputValue, setInputValue] = useState(\"\");\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const fruits = [\n { id: \"apple\", name: \"Apple\" },\n { id: \"banana\", name: \"Banana\" },\n { id: \"orange\", name: \"Orange\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n items={fruits}\n inputValue={inputValue}\n onInputChange={setInputValue}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"single\"\n placeholder=\"Select a fruit...\"\n aria-label=\"Controlled combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">Input: {inputValue || \"(empty)\"}</Text>\n </Stack>\n );\n};\n```\n\n### Menu trigger behavior\n\nControl when the dropdown menu opens using `menuTrigger`:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Box>\n <Text fontSize=\"sm\" fontWeight=\"medium\" mb=\"200\">\n menuTrigger=\"focus\" (default) - Opens when clicking input or toggle\n button\n </Text>\n <ComboBox.Root\n items={options}\n menuTrigger=\"focus\"\n placeholder=\"Click to open...\"\n aria-label=\"Focus trigger\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Box>\n <Box>\n <Text fontSize=\"sm\" fontWeight=\"medium\" mb=\"200\">\n menuTrigger=\"input\" - Opens only when typing (try typing in the input)\n </Text>\n <ComboBox.Root\n items={options}\n menuTrigger=\"input\"\n placeholder=\"Type to open...\"\n aria-label=\"Input trigger\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Box>\n <Box>\n <Text fontSize=\"sm\" fontWeight=\"medium\" mb=\"200\">\n menuTrigger=\"manual\" - Opens only via toggle button (input click does\n nothing)\n </Text>\n <ComboBox.Root\n items={options}\n menuTrigger=\"manual\"\n placeholder=\"Use button to open...\"\n aria-label=\"Manual trigger\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n## Component requirements\n\n### Accessibility\n\nThe ComboBox handles most accessibility requirements internally via React Aria.\n\nIf your use case requires tracking and analytics for this component, it is good\npractice to add a **persistent**, **unique** id:\n\n```tsx\nconst PERSISTENT_ID = \"example-combobox\";\n\nexport const Example = () => (\n <ComboBox.Root id={PERSISTENT_ID} aria-label=\"Select an option\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"1\">Option 1</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n\n- `Tab` / `Shift+Tab`: Navigate to/from the combobox\n- `Arrow Down`: Opens the menu and moves focus to the next option\n- `Arrow Up`: Opens the menu and moves focus to the previous option\n- `Enter`: Selects the focused option (or creates custom option if\n `allowsCustomOptions` is enabled)\n- `Escape`: Closes the menu\n- `Home` / `End`: Jump to first/last option\n- Type to filter: Type characters to filter options in real-time\n\n## API reference\n\n<PropsTable id=\"ComboBox\" />\n\n## Common patterns\n\n### Product search with categories\n\nA common e-commerce pattern combining filtering, grouping, and multi-select:\n\n```jsx live-dev\nconst App = () => {\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const products = [\n { id: 1, name: \"MacBook Pro\", category: \"Electronics\" },\n { id: 2, name: \"iPhone\", category: \"Electronics\" },\n { id: 3, name: \"Desk Chair\", category: \"Furniture\" },\n { id: 4, name: \"Standing Desk\", category: \"Furniture\" },\n { id: 5, name: \"Coffee Maker\", category: \"Appliances\" },\n ];\n\n // Group products by category\n const groupedByCategory = products.reduce((acc, product) => {\n if (!acc[product.category]) {\n acc[product.category] = [];\n }\n acc[product.category].push(product);\n return acc;\n }, {});\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"multiple\"\n placeholder=\"Search products...\"\n aria-label=\"Select products\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {Object.entries(groupedByCategory).map(([category, items]) => (\n <ComboBox.Section key={category} label={category}>\n {items.map((item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name}\n </ComboBox.Option>\n ))}\n </ComboBox.Section>\n ))}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">\n {selectedKeys.length} product{selectedKeys.length !== 1 ? \"s\" : \"\"}{\" \"}\n selected\n </Text>\n </Stack>\n );\n};\n```\n\n### Tag selection with creation\n\nEnable users to select from existing tags or create new ones:\n\n```jsx live-dev\nconst App = () => {\n const [tags, setTags] = useState([\n { id: 1, label: \"React\" },\n { id: 2, label: \"TypeScript\" },\n { id: 3, label: \"JavaScript\" },\n ]);\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const handleCreateTag = useCallback((newTag) => {\n setTags((prev) => [...prev, newTag]);\n }, []);\n\n return (\n <ComboBox.Root\n items={tags}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"multiple\"\n allowsCustomOptions\n getNewOptionData={(inputValue) => ({\n id: Date.now(),\n label: inputValue,\n })}\n onCreateOption={handleCreateTag}\n getTextValue={(item) => item.label}\n placeholder=\"Add tags...\"\n aria-label=\"Select or create tags\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.label}\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using ComboBox\nwithin your application. As the component's internal functionality is already\ntested by Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic Rendering Tests\n\nVerify the component renders with expected elements\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"ComboBox - Basic rendering\", () => {\n it(\"renders combobox input\", async () => {\n render(\n <NimbusProvider>\n <ComboBox.Root aria-label=\"Select an animal\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n await waitFor(() => {\n expect(screen.getByRole(\"combobox\")).toBeInTheDocument();\n });\n });\n\n it(\"renders with placeholder text\", async () => {\n render(\n <NimbusProvider>\n <ComboBox.Root\n placeholder=\"Search animals...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n await waitFor(() => {\n expect(\n screen.getByPlaceholderText(\"Search animals...\")\n ).toBeInTheDocument();\n });\n });\n});\n```\n\n### Interaction Tests\n\nTest user interactions with the component\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"ComboBox - Interactions\", () => {\n it(\"opens dropdown when input is clicked\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <ComboBox.Root aria-label=\"Select an animal\" menuTrigger=\"focus\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n const combobox = screen.getByRole(\"combobox\");\n await user.click(combobox);\n\n // The listbox is rendered in a portal outside the component tree\n // Query document directly and wait for it to appear\n await waitFor(() => {\n expect(document.querySelector('[role=\"listbox\"]')).toBeInTheDocument();\n });\n });\n\n it(\"calls onInputChange when typing\", async () => {\n const user = userEvent.setup();\n const handleInputChange = vi.fn();\n\n render(\n <NimbusProvider>\n <ComboBox.Root\n onInputChange={handleInputChange}\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n const combobox = screen.getByRole(\"combobox\");\n await user.type(combobox, \"test\");\n\n expect(handleInputChange).toHaveBeenCalled();\n });\n\n it(\"selects option when clicked\", async () => {\n const user = userEvent.setup();\n const handleSelectionChange = vi.fn();\n render(\n <NimbusProvider>\n <ComboBox.Root\n onSelectionChange={handleSelectionChange}\n menuTrigger=\"focus\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n const combobox = screen.getByRole(\"combobox\");\n await user.click(combobox);\n\n // Wait for options to appear in the portal\n await waitFor(() => {\n expect(\n document.querySelectorAll('[role=\"option\"]').length\n ).toBeGreaterThan(0);\n });\n\n // Get options after they've rendered\n const options = document.querySelectorAll('[role=\"option\"]');\n await user.click(options[0]);\n\n expect(handleSelectionChange).toHaveBeenCalledWith([\"koala\"]);\n });\n});\n```\n\n### Single-Select Mode Tests\n\nTest single-select behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"ComboBox - Single-select mode\", () => {\n it(\"supports single selection mode\", async () => {\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <ComboBox.Root\n selectionMode=\"single\"\n onSelectionChange={handleSelectionChange}\n aria-label=\"Select a fruit\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"apple\">Apple</ComboBox.Option>\n <ComboBox.Option id=\"banana\">Banana</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n // Verify single-select mode renders correctly\n await waitFor(() => {\n expect(screen.getByRole(\"combobox\")).toBeInTheDocument();\n });\n expect(handleSelectionChange).not.toHaveBeenCalled();\n });\n});\n```\n\n### Multi-Select Mode Tests\n\nTest multi-select behavior with tags\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"ComboBox - Multi-select mode\", () => {\n it(\"supports multiple selection mode\", async () => {\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <ComboBox.Root\n selectionMode=\"multiple\"\n onSelectionChange={handleSelectionChange}\n aria-label=\"Select animals\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n // Verify multi-select mode renders correctly\n await waitFor(() => {\n expect(screen.getByRole(\"combobox\")).toBeInTheDocument();\n });\n expect(handleSelectionChange).not.toHaveBeenCalled();\n });\n});\n```\n\n### Disabled State Tests\n\nTest that the combobox is properly disabled\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"ComboBox - Disabled state\", () => {\n it(\"renders disabled combobox\", async () => {\n render(\n <NimbusProvider>\n <ComboBox.Root isDisabled aria-label=\"Select an animal\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n await waitFor(() => {\n expect(screen.getByRole(\"combobox\")).toBeDisabled();\n });\n });\n});\n```\n\n### Invalid State Tests\n\nTest that the combobox is properly marked as invalid\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"ComboBox - Invalid state\", () => {\n it(\"renders invalid combobox\", async () => {\n render(\n <NimbusProvider>\n <ComboBox.Root isInvalid aria-label=\"Select an animal\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n // Invalid state is on the root element\n await waitFor(() => {\n const combobox = screen.getByRole(\"combobox\");\n const comboboxRoot = combobox.closest('[data-invalid=\"true\"]');\n expect(comboboxRoot).toBeInTheDocument();\n });\n });\n});\n```\n\n### Custom Options Creation Tests\n\nTest creating custom options with allowsCustomOptions\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"ComboBox - Custom options\", () => {\n it(\"creates new option when Enter is pressed\", async () => {\n const user = userEvent.setup();\n\n type FruitItem = {\n id: number;\n name: string;\n };\n\n const handleCreateOption = vi.fn();\n\n const TestComponent = () => {\n const [items, setItems] = useState<FruitItem[]>([\n { id: 1, name: \"Apple\" },\n { id: 2, name: \"Banana\" },\n ]);\n\n const getNewOptionData = useCallback(\n (inputValue: string): FruitItem => ({\n id: Date.now(),\n name: inputValue,\n }),\n []\n );\n\n const onCreateOption = useCallback((newItem: FruitItem) => {\n setItems((prev) => [...prev, newItem]);\n handleCreateOption(newItem);\n }, []);\n\n return (\n <NimbusProvider>\n <ComboBox.Root\n items={items}\n selectionMode=\"multiple\"\n allowsCustomOptions\n getNewOptionData={getNewOptionData}\n onCreateOption={onCreateOption}\n aria-label=\"Select or create fruits\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item: FruitItem) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name}\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n };\n\n render(<TestComponent />);\n\n const combobox = screen.getByRole(\"combobox\");\n await user.click(combobox);\n await user.type(combobox, \"Orange\");\n await user.keyboard(\"{Enter}\");\n\n expect(handleCreateOption).toHaveBeenCalled();\n // TypeScript knows the mock call argument type from the implementation\n const createdItem = handleCreateOption.mock.calls[0][0] as FruitItem;\n expect(createdItem.name).toBe(\"Orange\");\n });\n});\n```\n\n### Option Groups Tests\n\nTest rendering with option groups\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"ComboBox - Option groups\", () => {\n it(\"renders grouped options\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <ComboBox.Root menuTrigger=\"focus\" aria-label=\"Select a food\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Section label=\"Fruits\">\n <ComboBox.Option id=\"apple\">Apple</ComboBox.Option>\n <ComboBox.Option id=\"banana\">Banana</ComboBox.Option>\n </ComboBox.Section>\n <ComboBox.Section label=\"Vegetables\">\n <ComboBox.Option id=\"carrot\">Carrot</ComboBox.Option>\n </ComboBox.Section>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n const combobox = screen.getByRole(\"combobox\");\n await user.click(combobox);\n\n // Wait for listbox to appear in portal, then check for section labels\n await waitFor(() => {\n expect(document.querySelector('[role=\"listbox\"]')).toBeInTheDocument();\n });\n\n // Section labels should be visible in the document\n await waitFor(() => {\n expect(screen.getByText(\"Fruits\")).toBeInTheDocument();\n expect(screen.getByText(\"Vegetables\")).toBeInTheDocument();\n });\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-combobox--docs)\n- [React Aria ComboBox](https://react-spectrum.adobe.com/react-aria/ComboBox.html)\n- [ARIA Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/)\n",
294
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { ComboBox, type ComboBoxRootProps } from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nComboBox supports two patterns for defining options:\n\n#### With items prop (recommended for dynamic data)\n\nUse the `items` prop when working with data from APIs, state, or dynamic\nsources:\n\n```jsx live-dev\nconst App = () => {\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n { id: \"giraffe\", name: \"Giraffe\" },\n ];\n\n return (\n <ComboBox.Root\n items={animals}\n placeholder=\"Select an animal...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n#### With static children (requires textValue)\n\nFor static lists, you **must** include the `textValue` prop on each option for\nselections to display correctly:\n\n```jsx live-dev\nconst App = () => (\n <ComboBox.Root\n placeholder=\"Select an animal...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\" textValue=\"Koala\">\n Koala\n </ComboBox.Option>\n <ComboBox.Option id=\"panda\" textValue=\"Panda\">\n Panda\n </ComboBox.Option>\n <ComboBox.Option id=\"giraffe\" textValue=\"Giraffe\">\n Giraffe\n </ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n);\n```\n\n> [!WARNING]\\\n> Without `textValue` on static children, selected values will not display in\n> the input field or as tags. Always include `textValue` when not using the\n> `items` prop.\n\n## Usage examples\n\n### Size options\n\nThe `sm` and `md` size variants are available to match your interface density:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n size=\"sm\"\n items={options}\n placeholder=\"Small size...\"\n aria-label=\"Small combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <ComboBox.Root\n size=\"md\"\n items={options}\n placeholder=\"Medium size...\"\n aria-label=\"Medium combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Stack>\n );\n};\n```\n\n### Visual variants\n\nChoose between `solid` and `ghost` variants to match your design context:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n variant=\"solid\"\n items={options}\n placeholder=\"Solid variant...\"\n aria-label=\"Solid combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <ComboBox.Root\n variant=\"ghost\"\n items={options}\n placeholder=\"Ghost variant...\"\n aria-label=\"Ghost combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Stack>\n );\n};\n```\n\n### Single-select mode\n\nBy default, ComboBox operates in single-select mode where users can select one\noption at a time:\n\n```jsx live-dev\nconst App = () => {\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const fruits = [\n { id: \"apple\", name: \"Apple\" },\n { id: \"banana\", name: \"Banana\" },\n { id: \"orange\", name: \"Orange\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n items={fruits}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"single\"\n placeholder=\"Select a fruit...\"\n aria-label=\"Select a fruit\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">\n {selectedKeys.length > 0\n ? `Selected: ${selectedKeys[0]}`\n : \"No selection\"}\n </Text>\n </Stack>\n );\n};\n```\n\n### Multi-select mode\n\nEnable multi-select to allow users to choose multiple options displayed as tags:\n\n```jsx live-dev\nconst App = () => {\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n { id: \"giraffe\", name: \"Giraffe\" },\n { id: \"elephant\", name: \"Elephant\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n items={animals}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"multiple\"\n placeholder=\"Select animals...\"\n aria-label=\"Select animals\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">\n Selected: {selectedKeys.length} item\n {selectedKeys.length !== 1 ? \"s\" : \"\"}\n </Text>\n </Stack>\n );\n};\n```\n\n### Leading element\n\nAdd a leading icon to enhance the visual context:\n\n```jsx live-dev\nconst App = () => {\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n { id: \"giraffe\", name: \"Giraffe\" },\n ];\n\n return (\n <ComboBox.Root\n items={animals}\n leadingElement={<Icons.Search aria-hidden=\"true\" />}\n placeholder=\"Search...\"\n aria-label=\"Search for an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Trailing element\n\nAdd a trailing element to place content at the end of the field, before the\nbuilt-in clear and toggle buttons. Unlike the leading element, this position is\nsuited to controls — an inline filter button, for example:\n\n```jsx live-dev\nconst App = () => {\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n { id: \"giraffe\", name: \"Giraffe\" },\n ];\n\n return (\n <ComboBox.Root\n items={animals}\n leadingElement={<Icons.Search aria-hidden=\"true\" />}\n trailingElement={\n <IconButton\n size=\"2xs\"\n variant=\"ghost\"\n colorPalette=\"primary\"\n aria-label=\"Filter results\"\n onPress={() => {}}\n >\n <Icons.Tune />\n </IconButton>\n }\n placeholder=\"Search...\"\n aria-label=\"Search for an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\nA trailing control keeps its own behavior: pressing it runs its handler without\nopening the options popover or clearing the selection. Mark decorative content\n`aria-hidden=\"true\"`; interactive content needs its own `aria-label`.\n\nKeep the field readable — a leading icon, a trailing control, the clear button\nand the toggle button can all appear at once, so add only what earns its place.\n\n### Disabled state\n\nUse `isDisabled` to disable the combobox:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <ComboBox.Root\n items={options}\n isDisabled\n placeholder=\"Disabled...\"\n aria-label=\"Disabled combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Invalid state\n\nMark the combobox as invalid for form validation:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <ComboBox.Root\n items={options}\n isInvalid\n placeholder=\"Select an option...\"\n aria-label=\"Invalid combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Read-only state\n\nUse `isReadOnly` for display-only scenarios:\n\n```jsx live-dev\nconst App = () => {\n const fruits = [\n { id: \"apple\", name: \"Apple\" },\n { id: \"banana\", name: \"Banana\" },\n ];\n\n return (\n <ComboBox.Root\n items={fruits}\n isReadOnly\n selectedKeys={[\"apple\"]}\n aria-label=\"Read-only combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Option groups\n\nOrganize options into sections with headers:\n\n```jsx live-dev\nconst App = () => {\n const foodCategories = [\n {\n id: \"fruits\",\n name: \"Fruits\",\n children: [\n { id: \"apple\", name: \"Apple\" },\n { id: \"banana\", name: \"Banana\" },\n { id: \"orange\", name: \"Orange\" },\n ],\n },\n {\n id: \"vegetables\",\n name: \"Vegetables\",\n children: [\n { id: \"carrot\", name: \"Carrot\" },\n { id: \"broccoli\", name: \"Broccoli\" },\n { id: \"spinach\", name: \"Spinach\" },\n ],\n },\n ];\n\n return (\n <ComboBox.Root\n items={foodCategories}\n placeholder=\"Select a food...\"\n aria-label=\"Select a food\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(category) => (\n <ComboBox.Section\n id={category.id}\n label={category.name}\n items={category.children}\n >\n {(item) => (\n <ComboBox.Option id={item.id} textValue={item.name}>\n {item.name}\n </ComboBox.Option>\n )}\n </ComboBox.Section>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Dynamic options with items\n\n`ComboBox.ListBox` supports the `items` prop, a pattern unique to React Aria\nComponents (inherited from React Aria's `ListBox` component). This pattern\nprovides an efficient way to render dynamic or large lists of options.\n\n**When to use `items` vs static children:**\n\n- **Use `items` when:**\n - Rendering options from dynamic data (API responses, filtered lists, computed\n arrays)\n - Working with large lists (better performance through virtualization support)\n - Options change frequently based on user input or state\n - You need to map over data structures\n\n- **Use static children when:**\n - Options are known at compile time\n - You have a small, fixed set of options\n - You prefer the simpler JSX syntax for readability\n\n**Static children example:**\n\n```jsx live-dev\nconst App = () => (\n <ComboBox.Root\n placeholder=\"Select an animal...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\" textValue=\"Koala\">\n Koala\n </ComboBox.Option>\n <ComboBox.Option id=\"panda\" textValue=\"Panda\">\n Panda\n </ComboBox.Option>\n <ComboBox.Option id=\"giraffe\" textValue=\"Giraffe\">\n Giraffe\n </ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n);\n```\n\n**Dynamic items example:**\n\nWhen using `items`, children must be a render function that receives each item\nand returns a `ComboBox.Option`. This example demonstrates using `items` with\ndata that could come from an API, state, or computed values:\n\n```jsx live-dev\nconst App = () => {\n // In a real app, this data might come from an API, props, or state.\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n { id: \"giraffe\", name: \"Giraffe\" },\n { id: \"elephant\", name: \"Elephant\" },\n ];\n\n return (\n <ComboBox.Root\n items={animals}\n placeholder=\"Select an animal...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\nThe `items` pattern enables React Aria to optimize rendering and provides better\nsupport for features like virtualization, which can improve performance with\nlarge option lists.\n\n> [!TIP]\\\n> See [React Aria's ComboBox documentation](https://react-spectrum.adobe.com/react-aria/ComboBox.html#sections)\n> for complete API reference and advanced usage, including static and dynamic\n> items.\n\n### Custom filtering\n\nCustomize how options are filtered using the `filter` prop:\n\n```jsx live-dev\nconst App = () => {\n const animals = [\n { id: 1, name: \"Koala\", category: \"Marsupial\" },\n { id: 2, name: \"Panda\", category: \"Bear\" },\n { id: 3, name: \"Giraffe\", category: \"Mammal\" },\n { id: 4, name: \"Elephant\", category: \"Mammal\" },\n ];\n\n // Use the built-in multi-property filter\n const multiPropertyFilter = useCallback(\n ComboBox.filters.createMultiPropertyFilter([\"name\", \"category\"]),\n []\n );\n\n return (\n <ComboBox.Root\n items={animals}\n filter={multiPropertyFilter}\n aria-label=\"Search animals\"\n placeholder=\"Search by name or category...\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name} ({item.category})\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Custom options creation\n\nAllow users to create new options by typing and pressing Enter:\n\n```jsx live-dev\nconst App = () => {\n const [items, setItems] = useState([\n { id: 1, name: \"Apple\" },\n { id: 2, name: \"Banana\" },\n { id: 3, name: \"Orange\" },\n ]);\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const handleCreateOption = useCallback((newItem) => {\n console.log(\"Created:\", newItem.name);\n }, []);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n items={items}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"multiple\"\n allowsCustomOptions\n getNewOptionData={(inputValue) => ({\n id: Date.now(),\n name: inputValue,\n })}\n onCreateOption={handleCreateOption}\n aria-label=\"Select or create fruits\"\n placeholder=\"Type to search or create...\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name}\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">\n Type a custom fruit and press Enter to create it!\n </Text>\n </Stack>\n );\n};\n```\n\n### Async data loading\n\nLoad options asynchronously with built-in debouncing and loading states:\n\n```jsx live-dev\nconst App = () => {\n // Simulated API call\n const loadPokemon = useCallback(async (filterText, signal) => {\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n const allPokemon = [\n { id: 1, name: \"Bulbasaur\" },\n { id: 2, name: \"Charmander\" },\n { id: 3, name: \"Squirtle\" },\n { id: 4, name: \"Pikachu\" },\n { id: 5, name: \"Jigglypuff\" },\n ];\n\n return allPokemon.filter((p) =>\n p.name.toLowerCase().includes(filterText.toLowerCase())\n );\n }, []);\n\n return (\n <ComboBox.Root\n async={{\n load: loadPokemon,\n minSearchLength: 2,\n debounce: 300,\n }}\n aria-label=\"Search Pokemon\"\n placeholder=\"Type to search...\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name}\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n### Controlled input value\n\nControl the input text for advanced scenarios:\n\n```jsx live-dev\nconst App = () => {\n const [inputValue, setInputValue] = useState(\"\");\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const fruits = [\n { id: \"apple\", name: \"Apple\" },\n { id: \"banana\", name: \"Banana\" },\n { id: \"orange\", name: \"Orange\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n items={fruits}\n inputValue={inputValue}\n onInputChange={setInputValue}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"single\"\n placeholder=\"Select a fruit...\"\n aria-label=\"Controlled combobox\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">Input: {inputValue || \"(empty)\"}</Text>\n </Stack>\n );\n};\n```\n\n### Menu trigger behavior\n\nControl when the dropdown menu opens using `menuTrigger`:\n\n```jsx live-dev\nconst App = () => {\n const options = [\n { id: \"1\", name: \"Option 1\" },\n { id: \"2\", name: \"Option 2\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Box>\n <Text fontSize=\"sm\" fontWeight=\"medium\" mb=\"200\">\n menuTrigger=\"focus\" (default) - Opens when clicking input or toggle\n button\n </Text>\n <ComboBox.Root\n items={options}\n menuTrigger=\"focus\"\n placeholder=\"Click to open...\"\n aria-label=\"Focus trigger\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Box>\n <Box>\n <Text fontSize=\"sm\" fontWeight=\"medium\" mb=\"200\">\n menuTrigger=\"input\" - Opens only when typing (try typing in the input)\n </Text>\n <ComboBox.Root\n items={options}\n menuTrigger=\"input\"\n placeholder=\"Type to open...\"\n aria-label=\"Input trigger\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Box>\n <Box>\n <Text fontSize=\"sm\" fontWeight=\"medium\" mb=\"200\">\n menuTrigger=\"manual\" - Opens only via toggle button (input click does\n nothing)\n </Text>\n <ComboBox.Root\n items={options}\n menuTrigger=\"manual\"\n placeholder=\"Use button to open...\"\n aria-label=\"Manual trigger\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n## Component requirements\n\n### Accessibility\n\nThe ComboBox handles most accessibility requirements internally via React Aria.\n\nIf your use case requires tracking and analytics for this component, it is good\npractice to add a **persistent**, **unique** id:\n\n```tsx\nconst PERSISTENT_ID = \"example-combobox\";\n\nexport const Example = () => (\n <ComboBox.Root id={PERSISTENT_ID} aria-label=\"Select an option\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"1\">Option 1</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n\n- `Tab` / `Shift+Tab`: Navigate to/from the combobox\n- `Arrow Down`: Opens the menu and moves focus to the next option\n- `Arrow Up`: Opens the menu and moves focus to the previous option\n- `Enter`: Selects the focused option (or creates custom option if\n `allowsCustomOptions` is enabled)\n- `Escape`: Closes the menu\n- `Home` / `End`: Jump to first/last option\n- Type to filter: Type characters to filter options in real-time\n\n## API reference\n\n<PropsTable id=\"ComboBox\" />\n\n## Common patterns\n\n### Product search with categories\n\nA common e-commerce pattern combining filtering, grouping, and multi-select:\n\n```jsx live-dev\nconst App = () => {\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const products = [\n { id: 1, name: \"MacBook Pro\", category: \"Electronics\" },\n { id: 2, name: \"iPhone\", category: \"Electronics\" },\n { id: 3, name: \"Desk Chair\", category: \"Furniture\" },\n { id: 4, name: \"Standing Desk\", category: \"Furniture\" },\n { id: 5, name: \"Coffee Maker\", category: \"Appliances\" },\n ];\n\n // Group products by category\n const groupedByCategory = products.reduce((acc, product) => {\n if (!acc[product.category]) {\n acc[product.category] = [];\n }\n acc[product.category].push(product);\n return acc;\n }, {});\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <ComboBox.Root\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"multiple\"\n placeholder=\"Search products...\"\n aria-label=\"Select products\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {Object.entries(groupedByCategory).map(([category, items]) => (\n <ComboBox.Section key={category} label={category}>\n {items.map((item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name}\n </ComboBox.Option>\n ))}\n </ComboBox.Section>\n ))}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n <Text fontSize=\"sm\">\n {selectedKeys.length} product{selectedKeys.length !== 1 ? \"s\" : \"\"}{\" \"}\n selected\n </Text>\n </Stack>\n );\n};\n```\n\n### Tag selection with creation\n\nEnable users to select from existing tags or create new ones:\n\n```jsx live-dev\nconst App = () => {\n const [tags, setTags] = useState([\n { id: 1, label: \"React\" },\n { id: 2, label: \"TypeScript\" },\n { id: 3, label: \"JavaScript\" },\n ]);\n const [selectedKeys, setSelectedKeys] = useState([]);\n\n const handleCreateTag = useCallback((newTag) => {\n setTags((prev) => [...prev, newTag]);\n }, []);\n\n return (\n <ComboBox.Root\n items={tags}\n selectedKeys={selectedKeys}\n onSelectionChange={setSelectedKeys}\n selectionMode=\"multiple\"\n allowsCustomOptions\n getNewOptionData={(inputValue) => ({\n id: Date.now(),\n label: inputValue,\n })}\n onCreateOption={handleCreateTag}\n getTextValue={(item) => item.label}\n placeholder=\"Add tags...\"\n aria-label=\"Select or create tags\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.label}\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n );\n};\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using ComboBox\nwithin your application. As the component's internal functionality is already\ntested by Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic Rendering Tests\n\nVerify the component renders with expected elements\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"ComboBox - Basic rendering\", () => {\n it(\"renders combobox input\", async () => {\n render(\n <NimbusProvider>\n <ComboBox.Root aria-label=\"Select an animal\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n await waitFor(() => {\n expect(screen.getByRole(\"combobox\")).toBeInTheDocument();\n });\n });\n\n it(\"renders with placeholder text\", async () => {\n render(\n <NimbusProvider>\n <ComboBox.Root\n placeholder=\"Search animals...\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n await waitFor(() => {\n expect(\n screen.getByPlaceholderText(\"Search animals...\")\n ).toBeInTheDocument();\n });\n });\n});\n```\n\n### Interaction Tests\n\nTest user interactions with the component\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"ComboBox - Interactions\", () => {\n it(\"opens dropdown when input is clicked\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <ComboBox.Root aria-label=\"Select an animal\" menuTrigger=\"focus\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n const combobox = screen.getByRole(\"combobox\");\n await user.click(combobox);\n\n // The listbox is rendered in a portal outside the component tree\n // Query document directly and wait for it to appear\n await waitFor(() => {\n expect(document.querySelector('[role=\"listbox\"]')).toBeInTheDocument();\n });\n });\n\n it(\"calls onInputChange when typing\", async () => {\n const user = userEvent.setup();\n const handleInputChange = vi.fn();\n\n render(\n <NimbusProvider>\n <ComboBox.Root\n onInputChange={handleInputChange}\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n const combobox = screen.getByRole(\"combobox\");\n await user.type(combobox, \"test\");\n\n expect(handleInputChange).toHaveBeenCalled();\n });\n\n it(\"selects option when clicked\", async () => {\n const user = userEvent.setup();\n const handleSelectionChange = vi.fn();\n render(\n <NimbusProvider>\n <ComboBox.Root\n onSelectionChange={handleSelectionChange}\n menuTrigger=\"focus\"\n aria-label=\"Select an animal\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n const combobox = screen.getByRole(\"combobox\");\n await user.click(combobox);\n\n // Wait for options to appear in the portal\n await waitFor(() => {\n expect(\n document.querySelectorAll('[role=\"option\"]').length\n ).toBeGreaterThan(0);\n });\n\n // Get options after they've rendered\n const options = document.querySelectorAll('[role=\"option\"]');\n await user.click(options[0]);\n\n expect(handleSelectionChange).toHaveBeenCalledWith([\"koala\"]);\n });\n});\n```\n\n### Single-Select Mode Tests\n\nTest single-select behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"ComboBox - Single-select mode\", () => {\n it(\"supports single selection mode\", async () => {\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <ComboBox.Root\n selectionMode=\"single\"\n onSelectionChange={handleSelectionChange}\n aria-label=\"Select a fruit\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"apple\">Apple</ComboBox.Option>\n <ComboBox.Option id=\"banana\">Banana</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n // Verify single-select mode renders correctly\n await waitFor(() => {\n expect(screen.getByRole(\"combobox\")).toBeInTheDocument();\n });\n expect(handleSelectionChange).not.toHaveBeenCalled();\n });\n});\n```\n\n### Multi-Select Mode Tests\n\nTest multi-select behavior with tags\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"ComboBox - Multi-select mode\", () => {\n it(\"supports multiple selection mode\", async () => {\n const handleSelectionChange = vi.fn();\n\n render(\n <NimbusProvider>\n <ComboBox.Root\n selectionMode=\"multiple\"\n onSelectionChange={handleSelectionChange}\n aria-label=\"Select animals\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n <ComboBox.Option id=\"panda\">Panda</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n // Verify multi-select mode renders correctly\n await waitFor(() => {\n expect(screen.getByRole(\"combobox\")).toBeInTheDocument();\n });\n expect(handleSelectionChange).not.toHaveBeenCalled();\n });\n});\n```\n\n### Disabled State Tests\n\nTest that the combobox is properly disabled\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"ComboBox - Disabled state\", () => {\n it(\"renders disabled combobox\", async () => {\n render(\n <NimbusProvider>\n <ComboBox.Root isDisabled aria-label=\"Select an animal\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n await waitFor(() => {\n expect(screen.getByRole(\"combobox\")).toBeDisabled();\n });\n });\n});\n```\n\n### Invalid State Tests\n\nTest that the combobox is properly marked as invalid\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"ComboBox - Invalid state\", () => {\n it(\"renders invalid combobox\", async () => {\n render(\n <NimbusProvider>\n <ComboBox.Root isInvalid aria-label=\"Select an animal\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Option id=\"koala\">Koala</ComboBox.Option>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n // Invalid state is on the root element\n await waitFor(() => {\n const combobox = screen.getByRole(\"combobox\");\n const comboboxRoot = combobox.closest('[data-invalid=\"true\"]');\n expect(comboboxRoot).toBeInTheDocument();\n });\n });\n});\n```\n\n### Custom Options Creation Tests\n\nTest creating custom options with allowsCustomOptions\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"ComboBox - Custom options\", () => {\n it(\"creates new option when Enter is pressed\", async () => {\n const user = userEvent.setup();\n\n type FruitItem = {\n id: number;\n name: string;\n };\n\n const handleCreateOption = vi.fn();\n\n const TestComponent = () => {\n const [items, setItems] = useState<FruitItem[]>([\n { id: 1, name: \"Apple\" },\n { id: 2, name: \"Banana\" },\n ]);\n\n const getNewOptionData = useCallback(\n (inputValue: string): FruitItem => ({\n id: Date.now(),\n name: inputValue,\n }),\n []\n );\n\n const onCreateOption = useCallback((newItem: FruitItem) => {\n setItems((prev) => [...prev, newItem]);\n handleCreateOption(newItem);\n }, []);\n\n return (\n <NimbusProvider>\n <ComboBox.Root\n items={items}\n selectionMode=\"multiple\"\n allowsCustomOptions\n getNewOptionData={getNewOptionData}\n onCreateOption={onCreateOption}\n aria-label=\"Select or create fruits\"\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item: FruitItem) => (\n <ComboBox.Option key={item.id} id={item.id}>\n {item.name}\n </ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n };\n\n render(<TestComponent />);\n\n const combobox = screen.getByRole(\"combobox\");\n await user.click(combobox);\n await user.type(combobox, \"Orange\");\n await user.keyboard(\"{Enter}\");\n\n expect(handleCreateOption).toHaveBeenCalled();\n // TypeScript knows the mock call argument type from the implementation\n const createdItem = handleCreateOption.mock.calls[0][0] as FruitItem;\n expect(createdItem.name).toBe(\"Orange\");\n });\n});\n```\n\n### Option Groups Tests\n\nTest rendering with option groups\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"ComboBox - Option groups\", () => {\n it(\"renders grouped options\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <ComboBox.Root menuTrigger=\"focus\" aria-label=\"Select a food\">\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n <ComboBox.Section label=\"Fruits\">\n <ComboBox.Option id=\"apple\">Apple</ComboBox.Option>\n <ComboBox.Option id=\"banana\">Banana</ComboBox.Option>\n </ComboBox.Section>\n <ComboBox.Section label=\"Vegetables\">\n <ComboBox.Option id=\"carrot\">Carrot</ComboBox.Option>\n </ComboBox.Section>\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n const combobox = screen.getByRole(\"combobox\");\n await user.click(combobox);\n\n // Wait for listbox to appear in portal, then check for section labels\n await waitFor(() => {\n expect(document.querySelector('[role=\"listbox\"]')).toBeInTheDocument();\n });\n\n // Section labels should be visible in the document\n await waitFor(() => {\n expect(screen.getByText(\"Fruits\")).toBeInTheDocument();\n expect(screen.getByText(\"Vegetables\")).toBeInTheDocument();\n });\n });\n});\n```\n\n### Trailing Element Tests\n\nVerify trailing element rendering and that it owns its own clicks\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { useState, useCallback } from \"react\";\nimport { ComboBox, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"ComboBox - Trailing element\", () => {\n const animals = [\n { id: \"koala\", name: \"Koala\" },\n { id: \"panda\", name: \"Panda\" },\n ];\n\n const renderWithTrailing = (trailingElement: React.ReactNode) =>\n render(\n <NimbusProvider>\n <ComboBox.Root\n items={animals}\n aria-label=\"Search animals\"\n trailingElement={trailingElement}\n >\n <ComboBox.Trigger />\n <ComboBox.Popover>\n <ComboBox.ListBox>\n {(item: { id: string; name: string }) => (\n <ComboBox.Option id={item.id}>{item.name}</ComboBox.Option>\n )}\n </ComboBox.ListBox>\n </ComboBox.Popover>\n </ComboBox.Root>\n </NimbusProvider>\n );\n\n it(\"renders the trailing element in the trailingElement slot\", () => {\n renderWithTrailing(<Tune data-testid=\"trailing-icon\" />);\n\n expect(screen.getByTestId(\"trailing-icon\").parentElement).toHaveClass(\n \"nimbus-combobox__trailingElement\"\n );\n });\n\n it(\"runs a trailing button's handler without opening the options\", async () => {\n const user = userEvent.setup();\n const onFilter = vi.fn();\n\n renderWithTrailing(\n <IconButton\n size=\"2xs\"\n variant=\"ghost\"\n aria-label=\"Filter results\"\n onPress={onFilter}\n >\n <Tune />\n </IconButton>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Filter results\" }));\n\n expect(onFilter).toHaveBeenCalledTimes(1);\n expect(screen.queryByRole(\"listbox\")).not.toBeInTheDocument();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-combobox--docs)\n- [React Aria ComboBox](https://react-spectrum.adobe.com/react-aria/ComboBox.html)\n- [ARIA Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/)\n",
295
295
  "toc": [
296
296
  {
297
297
  "value": "Getting started",
@@ -414,6 +414,17 @@
414
414
  ],
415
415
  "parent": "root"
416
416
  },
417
+ {
418
+ "value": "Trailing element",
419
+ "href": "#trailing-element",
420
+ "depth": 3,
421
+ "numbering": [
422
+ 1,
423
+ 2,
424
+ 6
425
+ ],
426
+ "parent": "root"
427
+ },
417
428
  {
418
429
  "value": "Disabled state",
419
430
  "href": "#disabled-state",
@@ -421,7 +432,7 @@
421
432
  "numbering": [
422
433
  1,
423
434
  2,
424
- 6
435
+ 7
425
436
  ],
426
437
  "parent": "root"
427
438
  },
@@ -432,7 +443,7 @@
432
443
  "numbering": [
433
444
  1,
434
445
  2,
435
- 7
446
+ 8
436
447
  ],
437
448
  "parent": "root"
438
449
  },
@@ -443,7 +454,7 @@
443
454
  "numbering": [
444
455
  1,
445
456
  2,
446
- 8
457
+ 9
447
458
  ],
448
459
  "parent": "root"
449
460
  },
@@ -454,7 +465,7 @@
454
465
  "numbering": [
455
466
  1,
456
467
  2,
457
- 9
468
+ 10
458
469
  ],
459
470
  "parent": "root"
460
471
  },
@@ -465,7 +476,7 @@
465
476
  "numbering": [
466
477
  1,
467
478
  2,
468
- 10
479
+ 11
469
480
  ],
470
481
  "parent": "root"
471
482
  },
@@ -476,7 +487,7 @@
476
487
  "numbering": [
477
488
  1,
478
489
  2,
479
- 11
490
+ 12
480
491
  ],
481
492
  "parent": "root"
482
493
  },
@@ -487,7 +498,7 @@
487
498
  "numbering": [
488
499
  1,
489
500
  2,
490
- 12
501
+ 13
491
502
  ],
492
503
  "parent": "root"
493
504
  },
@@ -498,7 +509,7 @@
498
509
  "numbering": [
499
510
  1,
500
511
  2,
501
- 13
512
+ 14
502
513
  ],
503
514
  "parent": "root"
504
515
  },
@@ -509,7 +520,7 @@
509
520
  "numbering": [
510
521
  1,
511
522
  2,
512
- 14
523
+ 15
513
524
  ],
514
525
  "parent": "root"
515
526
  },
@@ -520,7 +531,7 @@
520
531
  "numbering": [
521
532
  1,
522
533
  2,
523
- 15
534
+ 16
524
535
  ],
525
536
  "parent": "root"
526
537
  },
@@ -697,6 +708,17 @@
697
708
  ],
698
709
  "parent": "root"
699
710
  },
711
+ {
712
+ "value": "Trailing Element Tests",
713
+ "href": "#trailing-element-tests",
714
+ "depth": 3,
715
+ "numbering": [
716
+ 1,
717
+ 6,
718
+ 9
719
+ ],
720
+ "parent": "root"
721
+ },
700
722
  {
701
723
  "value": "Resources",
702
724
  "href": "#resources",
@@ -219,7 +219,7 @@
219
219
  ]
220
220
  },
221
221
  "dev": {
222
- "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { SearchInput, type SearchInputProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nThe simplest implementation uses uncontrolled mode:\n\n```jsx live-dev\nconst App = () => (\n <SearchInput placeholder=\"Search...\" />\n)\n```\n\n## Usage examples\n\n### Size options\n\nThe `sm` and `md` size variants are available to match your interface density:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput size=\"sm\" placeholder=\"Small search\" />\n <SearchInput size=\"md\" placeholder=\"Medium search (default)\" />\n </Stack>\n)\n```\n\n### Visual variants\n\nChoose between `solid` and `ghost` variants to match your design context:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput variant=\"solid\" placeholder=\"Solid variant (default)\" />\n <SearchInput variant=\"ghost\" placeholder=\"Ghost variant\" />\n </Stack>\n)\n```\n\n### Clear button behavior\n\nSearchInput includes a built-in clear button that automatically appears when the input has a value:\n\n```jsx live-dev\nconst App = () => {\n const [value, setValue] = useState<string>('');\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value={value}\n onChange={setValue}\n placeholder=\"Type to see clear button...\"\n />\n <Text fontSize=\"sm\">\n Current value: {value || '(empty)'}\n </Text>\n </Stack>\n );\n}\n```\n\n**Behavioral notes:**\n- Clear button is always present in the DOM but hidden when input is empty\n- Clear button has `opacity: 0` and `pointerEvents: none` when hidden\n- Clear button automatically transitions when value changes\n- Clear button is disabled when input is disabled or read-only\n- Clear button has an internationalized aria-label (\"Clear search input\")\n\n### Disabled state\n\nDisable input interaction with the `isDisabled` prop:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput placeholder=\"Enabled search\" />\n <SearchInput placeholder=\"Disabled search\" isDisabled />\n <SearchInput\n value=\"Disabled with value\"\n isDisabled\n onChange={() => {}}\n />\n </Stack>\n)\n```\n\n### Invalid state\n\nMark inputs as invalid for validation feedback:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n placeholder=\"Valid search\"\n value=\"valid query\"\n onChange={() => {}}\n />\n <SearchInput\n placeholder=\"Invalid search\"\n value=\"error\"\n isInvalid\n onChange={() => {}}\n />\n </Stack>\n)\n```\n\n### Read-only state\n\nUse `isReadOnly` to display values without allowing editing:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value=\"Editable search\"\n onChange={() => {}}\n />\n <SearchInput\n value=\"Read-only search\"\n isReadOnly\n onChange={() => {}}\n />\n </Stack>\n)\n```\n\n### Event handlers\n\nSearchInput supports three event handlers for different interaction patterns:\n\n```jsx live-dev\nconst App = () => {\n const [value, setValue] = useState<string>('');\n const [submitted, setSubmitted] = useState<string>('');\n const [clearCount, setClearCount] = useState<number>(0);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value={value}\n onChange={(newValue) => {\n setValue(newValue);\n }}\n onSubmit={(submittedValue) => {\n setSubmitted(submittedValue);\n }}\n onClear={() => {\n setClearCount((count) => count + 1);\n }}\n placeholder=\"Type and press Enter...\"\n />\n <Stack direction=\"column\" gap=\"200\" fontSize=\"sm\">\n <Text>Current: {value || '(empty)'}</Text>\n <Text>Submitted: {submitted || '(none)'}</Text>\n <Text>Clear count: {clearCount}</Text>\n </Stack>\n </Stack>\n );\n}\n```\n\n**Event handler details:**\n- `onChange`: Called when input value changes (on every keystroke)\n- `onSubmit`: Called when user presses Enter\n- `onClear`: Called when user clicks the clear button or presses Escape\n\n### Uncontrolled mode\n\nFor simpler use cases, use uncontrolled mode with `defaultValue` and `onChange`:\n\n```jsx live-dev\nconst App = () => {\n const [displayValue, setDisplayValue] = useState<string>('');\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n placeholder=\"Type something...\"\n defaultValue=\"\"\n onChange={(value) => {\n setDisplayValue(value);\n }}\n />\n <Text fontSize=\"sm\">Current value: {displayValue || '(empty)'}</Text>\n </Stack>\n );\n}\n```\n\nUse uncontrolled mode when you need to capture the input value without managing state yourself.\n\n**Note:** SearchInput's `onChange` receives the string value directly, not an event object.\n\n### Controlled mode\n\nFor scenarios requiring programmatic control or coordination with other components, use controlled mode:\n\n```jsx live-dev\nconst App = () => {\n const [value, setValue] = useState<string>('');\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value={value}\n onChange={(value) => setValue(value)}\n placeholder=\"Controlled search\"\n />\n <Text fontSize=\"sm\">\n {value ? `Searching for: ${value}` : 'Start typing...'}\n </Text>\n <Button onClick={() => setValue('')}>\n Clear programmatically\n </Button>\n </Stack>\n );\n}\n```\n\nUse controlled mode when you need to:\n- Synchronize the value with external state\n- Validate or transform search queries\n- Clear or programmatically set the value\n- React to changes in real-time\n- Coordinate with other components (like displaying search results)\n\n## Component requirements\n\n## Accessibility\n\nThe SearchInput handles most accessibility requirements internally. However, you must always associate an internationalized label with the component. Visual labels are preferable, and can be set by:\n\n- Using the `SearchInputField` pattern component (if available, recommended)\n- Associating a `<label>` element with the `SearchInput` using `aria-labelledby`:\n\n```tsx\n<label id=\"search-label\">\n {msg.format(searchMessage)}\n</label>\n<SearchInput aria-labelledby=\"search-label\" />\n```\n\n- Associating a `<label>` element with the `SearchInput` using `htmlFor`:\n\n```tsx\n<label htmlFor=\"search-input\">\n {msg.format(searchMessage)}\n</label>\n<SearchInput id=\"search-input\" />\n```\n\nIf your design requires that the label should not be visible, the label should be set using the `aria-label` prop:\n\n```tsx\n<SearchInput aria-label={msg.format(searchMessage)} />\n```\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"product-search-input\";\n\nexport const ProductSearch = () => (\n <SearchInput id={PERSISTENT_ID} placeholder=\"Search products...\" />\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n- `Tab` / `Shift+Tab`: Navigate to/from the search field\n- `Arrow keys`: Move cursor within the text (Left/Right)\n- `Home` / `End`: Move cursor to start/end of text\n- `Ctrl+A` / `Cmd+A`: Select all text\n- `Enter`: Submit search (triggers `onSubmit` handler)\n- `Escape`: Clear the search input (triggers `onClear` handler)\n- Standard text editing shortcuts: Cut, Copy, Paste, Undo, Redo\n\n#### Click-to-focus behavior\n\nClicking anywhere within the input container (including the search icon area) will focus the input field, providing a larger interactive target for better usability.\n\n## API reference\n\n<PropsTable id=\"SearchInput\" />\n\n## Common patterns\n\n### Search with live filtering\n\nDisplay results that update as the user types:\n\n```jsx live-dev\nconst App = () => {\n const [query, setQuery] = useState<string>('');\n\n const products = [\n 'Laptop', 'Phone', 'Tablet', 'Monitor', 'Keyboard',\n 'Mouse', 'Headphones', 'Camera', 'Printer', 'Scanner'\n ];\n\n const filteredProducts = products.filter(product =>\n product.toLowerCase().includes(query.toLowerCase())\n );\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value={query}\n onChange={setQuery}\n placeholder=\"Search products...\"\n />\n <Stack direction=\"column\" gap=\"200\">\n {query && filteredProducts.length === 0 && (\n <Text fontSize=\"sm\" color=\"neutral.11\">\n No results found for \"{query}\"\n </Text>\n )}\n {query && filteredProducts.length > 0 && (\n <Text fontSize=\"sm\" fontWeight=\"medium\">\n Found {filteredProducts.length} result{filteredProducts.length !== 1 ? 's' : ''}:\n </Text>\n )}\n {query && filteredProducts.map(product => (\n <Text key={product} fontSize=\"sm\">\n • {product}\n </Text>\n ))}\n </Stack>\n </Stack>\n );\n}\n```\n\n### Search with submit action\n\nHandle search submission with Enter key or button click:\n\n```jsx live-dev\nconst App = () => {\n const [query, setQuery] = useState<string>('');\n const [results, setResults] = useState<string[]>([]);\n const [isSearching, setIsSearching] = useState<boolean>(false);\n\n const performSearch = (searchQuery: string) => {\n if (!searchQuery.trim()) {\n setResults([]);\n return;\n }\n\n setIsSearching(true);\n\n // Simulate API call delay\n setTimeout(() => {\n const mockResults = [\n `Result 1 for \"${searchQuery}\"`,\n `Result 2 for \"${searchQuery}\"`,\n `Result 3 for \"${searchQuery}\"`,\n ];\n setResults(mockResults);\n setIsSearching(false);\n }, 500);\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Stack direction=\"row\" gap=\"200\" alignItems=\"center\">\n <SearchInput\n value={query}\n onChange={setQuery}\n onSubmit={performSearch}\n placeholder=\"Type and press Enter...\"\n />\n <Button\n onClick={() => performSearch(query)}\n size=\"md\"\n >\n Search\n </Button>\n </Stack>\n\n {isSearching && (\n <Text fontSize=\"sm\" color=\"neutral.11\">\n Searching...\n </Text>\n )}\n\n {!isSearching && results.length > 0 && (\n <Stack direction=\"column\" gap=\"200\">\n <Text fontSize=\"sm\" fontWeight=\"medium\">\n Search Results:\n </Text>\n {results.map((result, index) => (\n <Text key={index} fontSize=\"sm\">\n • {result}\n </Text>\n ))}\n </Stack>\n )}\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using SearchInput within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the component renders with expected elements\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"SearchInput - Basic rendering\", () => {\n it(\"renders search input with search icon\", () => {\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toBeInTheDocument();\n expect(searchBox).toHaveAttribute(\"type\", \"search\");\n expect(searchBox).toHaveAttribute(\"placeholder\", \"Search...\");\n });\n\n it(\"renders with clear button (hidden when empty)\", () => {\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n // Clear button is always in DOM\n const clearButton = screen.getByRole(\"button\", { hidden: true });\n expect(clearButton).toBeInTheDocument();\n\n // But hidden visually when empty\n const computedStyle = window.getComputedStyle(clearButton);\n expect(computedStyle.opacity).toBe(\"0\");\n expect(computedStyle.pointerEvents).toBe(\"none\");\n });\n});\n```\n\n### Interaction Tests\n\nTest user interactions with the search input\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"SearchInput - Interactions\", () => {\n it(\"handles typing in search input\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test query\");\n\n expect(searchBox).toHaveValue(\"test query\");\n });\n\n it(\"calls onChange with string value on input\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n\n render(\n <NimbusProvider>\n <SearchInput\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test\");\n\n // onChange receives string, not event\n expect(handleChange).toHaveBeenCalledWith(\"test\");\n });\n\n it(\"shows clear button when input has value\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test\");\n\n const clearButton = screen.getByRole(\"button\");\n\n await waitFor(() => {\n const computedStyle = window.getComputedStyle(clearButton);\n expect(computedStyle.opacity).toBe(\"1\");\n expect(computedStyle.pointerEvents).toBe(\"auto\");\n });\n });\n\n it(\"clears input when clear button is clicked\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test query\");\n expect(searchBox).toHaveValue(\"test query\");\n\n const clearButton = screen.getByRole(\"button\");\n await user.click(clearButton);\n\n expect(searchBox).toHaveValue(\"\");\n });\n\n it(\"calls onClear when clear button is clicked\", async () => {\n const user = userEvent.setup();\n const handleClear = vi.fn();\n\n render(\n <NimbusProvider>\n <SearchInput\n defaultValue=\"test\"\n onClear={handleClear}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const clearButton = screen.getByRole(\"button\");\n await user.click(clearButton);\n\n expect(handleClear).toHaveBeenCalled();\n });\n});\n```\n\n### Keyboard Navigation Tests\n\nTest keyboard interactions and shortcuts\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"SearchInput - Keyboard navigation\", () => {\n it(\"focuses on tab key\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).not.toHaveFocus();\n\n await user.tab();\n expect(searchBox).toHaveFocus();\n });\n\n it(\"clears input on Escape key\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test query\");\n expect(searchBox).toHaveValue(\"test query\");\n\n await user.keyboard(\"{Escape}\");\n expect(searchBox).toHaveValue(\"\");\n });\n\n it(\"calls onSubmit on Enter key\", async () => {\n const user = userEvent.setup();\n const handleSubmit = vi.fn();\n\n render(\n <NimbusProvider>\n <SearchInput\n onSubmit={handleSubmit}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test query\");\n await user.keyboard(\"{Enter}\");\n\n expect(handleSubmit).toHaveBeenCalledWith(\"test query\");\n });\n});\n```\n\n### Controlled Mode Tests\n\nTest controlled component behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"SearchInput - Controlled mode\", () => {\n it(\"updates controlled value via onChange\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n\n const { rerender } = render(\n <NimbusProvider>\n <SearchInput\n value=\"\"\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test\");\n\n // onChange called once per character - receives strings\n expect(handleChange).toHaveBeenCalledTimes(4);\n\n // Verify all calls receive strings\n for (const call of handleChange.mock.calls) {\n expect(typeof call[0]).toBe(\"string\");\n }\n\n // Rerender with new value\n rerender(\n <NimbusProvider>\n <SearchInput\n value=\"test\"\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n expect(searchBox).toHaveValue(\"test\");\n });\n\n it(\"can be programmatically cleared\", async () => {\n let value = \"initial value\";\n const handleChange = vi.fn((newValue: string) => {\n value = newValue;\n });\n\n const { rerender } = render(\n <NimbusProvider>\n <SearchInput\n value={value}\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toHaveValue(\"initial value\");\n\n // Programmatically clear\n value = \"\";\n rerender(\n <NimbusProvider>\n <SearchInput\n value={value}\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n expect(searchBox).toHaveValue(\"\");\n });\n});\n```\n\n### State Tests\n\nTest disabled, invalid, and read-only states\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"SearchInput - States\", () => {\n it(\"disables input when isDisabled is true\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput isDisabled placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toBeDisabled();\n\n // Cannot type when disabled\n await user.type(searchBox, \"test\");\n expect(searchBox).toHaveValue(\"\");\n });\n\n it(\"marks input as invalid when isInvalid is true\", () => {\n render(\n <NimbusProvider>\n <SearchInput isInvalid placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n // Check parent container for data-invalid attribute\n const container = searchBox.closest(\"[data-invalid]\");\n expect(container).toHaveAttribute(\"data-invalid\", \"true\");\n });\n\n it(\"prevents editing when isReadOnly is true\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput\n isReadOnly\n defaultValue=\"read-only value\"\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toHaveAttribute(\"readonly\");\n expect(searchBox).toHaveValue(\"read-only value\");\n\n // Cannot type when read-only\n await user.click(searchBox);\n await user.type(searchBox, \"test\");\n expect(searchBox).toHaveValue(\"read-only value\");\n });\n\n it(\"disables clear button when input is disabled\", () => {\n render(\n <NimbusProvider>\n <SearchInput\n isDisabled\n defaultValue=\"test\"\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const clearButton = screen.getByRole(\"button\", { hidden: true });\n expect(clearButton).toBeDisabled();\n });\n\n it(\"disables clear button when input is read-only\", () => {\n render(\n <NimbusProvider>\n <SearchInput\n isReadOnly\n defaultValue=\"test\"\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const clearButton = screen.getByRole(\"button\");\n expect(clearButton).toBeDisabled();\n });\n});\n```\n\n### Event Handler Tests\n\nTest onChange, onSubmit, and onClear event handlers\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"SearchInput - Event handlers\", () => {\n it(\"calls all event handlers appropriately\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n const handleSubmit = vi.fn();\n const handleClear = vi.fn();\n\n render(\n <NimbusProvider>\n <SearchInput\n onChange={handleChange}\n onSubmit={handleSubmit}\n onClear={handleClear}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n\n // Type to trigger onChange\n await user.type(searchBox, \"test\");\n expect(handleChange).toHaveBeenCalled();\n expect(handleChange).toHaveBeenLastCalledWith(\"test\");\n\n // Press Enter to trigger onSubmit\n await user.keyboard(\"{Enter}\");\n expect(handleSubmit).toHaveBeenCalledWith(\"test\");\n\n // Click clear button to trigger onClear\n const clearButton = screen.getByRole(\"button\");\n await user.click(clearButton);\n expect(handleClear).toHaveBeenCalled();\n });\n\n it(\"clears value on Escape key when focused\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <SearchInput\n defaultValue=\"test value\"\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toHaveValue(\"test value\");\n\n // Focus the input first\n await user.click(searchBox);\n await user.keyboard(\"{Escape}\");\n\n expect(searchBox).toHaveValue(\"\");\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-searchinput--docs)\n- [React Aria SearchField](https://react-spectrum.adobe.com/react-aria/SearchField.html)\n- [ARIA Search Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/search.html)\n",
222
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { SearchInput, type SearchInputProps } from '@commercetools/nimbus';\n```\n\n### Basic usage\n\nThe simplest implementation uses uncontrolled mode:\n\n```jsx live-dev\nconst App = () => (\n <SearchInput placeholder=\"Search...\" />\n)\n```\n\n## Usage examples\n\n### Size options\n\nThe `sm` and `md` size variants are available to match your interface density:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput size=\"sm\" placeholder=\"Small search\" />\n <SearchInput size=\"md\" placeholder=\"Medium search (default)\" />\n </Stack>\n)\n```\n\n### Visual variants\n\nChoose between `solid` and `ghost` variants to match your design context:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput variant=\"solid\" placeholder=\"Solid variant (default)\" />\n <SearchInput variant=\"ghost\" placeholder=\"Ghost variant\" />\n </Stack>\n)\n```\n\n### Clear button behavior\n\nSearchInput includes a built-in clear button that automatically appears when the input has a value:\n\n```jsx live-dev\nconst App = () => {\n const [value, setValue] = useState<string>('');\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value={value}\n onChange={setValue}\n placeholder=\"Type to see clear button...\"\n />\n <Text fontSize=\"sm\">\n Current value: {value || '(empty)'}\n </Text>\n </Stack>\n );\n}\n```\n\n**Behavioral notes:**\n- Clear button is always present in the DOM but hidden when input is empty\n- Clear button has `opacity: 0` and `pointerEvents: none` when hidden\n- Clear button automatically transitions when value changes\n- Clear button is disabled when input is disabled or read-only\n- Clear button has an internationalized aria-label (\"Clear search input\")\n\n### Leading and trailing elements\n\nThe leading search icon is the default, and can be replaced with any element or\nremoved entirely by passing `null`. Use `trailingElement` to place content at the\nend of the field, before the clear button — an inline filter button, for example:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n placeholder=\"Default leading icon\"\n aria-label=\"Default leading icon\"\n />\n <SearchInput\n placeholder=\"Replaced leading icon\"\n aria-label=\"Replaced leading icon\"\n leadingElement={<Icons.Visibility aria-hidden=\"true\" />}\n />\n <SearchInput\n placeholder=\"No leading element\"\n aria-label=\"No leading element\"\n leadingElement={null}\n />\n <SearchInput\n placeholder=\"Filter results\"\n aria-label=\"Filter results\"\n trailingElement={\n <IconButton\n size=\"xs\"\n variant=\"ghost\"\n colorPalette=\"primary\"\n aria-label=\"Filter results\"\n onPress={() => {}}\n >\n <Icons.Tune />\n </IconButton>\n }\n />\n </Stack>\n)\n```\n\nDecorative content should be marked `aria-hidden=\"true\"`. Interactive content\nneeds its own `aria-label`, and keeps its own click behavior — pressing a\ntrailing button does not clear the field or move focus to the input.\n\n### Disabled state\n\nDisable input interaction with the `isDisabled` prop:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput placeholder=\"Enabled search\" />\n <SearchInput placeholder=\"Disabled search\" isDisabled />\n <SearchInput\n value=\"Disabled with value\"\n isDisabled\n onChange={() => {}}\n />\n </Stack>\n)\n```\n\n### Invalid state\n\nMark inputs as invalid for validation feedback:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n placeholder=\"Valid search\"\n value=\"valid query\"\n onChange={() => {}}\n />\n <SearchInput\n placeholder=\"Invalid search\"\n value=\"error\"\n isInvalid\n onChange={() => {}}\n />\n </Stack>\n)\n```\n\n### Read-only state\n\nUse `isReadOnly` to display values without allowing editing:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value=\"Editable search\"\n onChange={() => {}}\n />\n <SearchInput\n value=\"Read-only search\"\n isReadOnly\n onChange={() => {}}\n />\n </Stack>\n)\n```\n\n### Event handlers\n\nSearchInput supports three event handlers for different interaction patterns:\n\n```jsx live-dev\nconst App = () => {\n const [value, setValue] = useState<string>('');\n const [submitted, setSubmitted] = useState<string>('');\n const [clearCount, setClearCount] = useState<number>(0);\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value={value}\n onChange={(newValue) => {\n setValue(newValue);\n }}\n onSubmit={(submittedValue) => {\n setSubmitted(submittedValue);\n }}\n onClear={() => {\n setClearCount((count) => count + 1);\n }}\n placeholder=\"Type and press Enter...\"\n />\n <Stack direction=\"column\" gap=\"200\" fontSize=\"sm\">\n <Text>Current: {value || '(empty)'}</Text>\n <Text>Submitted: {submitted || '(none)'}</Text>\n <Text>Clear count: {clearCount}</Text>\n </Stack>\n </Stack>\n );\n}\n```\n\n**Event handler details:**\n- `onChange`: Called when input value changes (on every keystroke)\n- `onSubmit`: Called when user presses Enter\n- `onClear`: Called when user clicks the clear button or presses Escape\n\n### Uncontrolled mode\n\nFor simpler use cases, use uncontrolled mode with `defaultValue` and `onChange`:\n\n```jsx live-dev\nconst App = () => {\n const [displayValue, setDisplayValue] = useState<string>('');\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n placeholder=\"Type something...\"\n defaultValue=\"\"\n onChange={(value) => {\n setDisplayValue(value);\n }}\n />\n <Text fontSize=\"sm\">Current value: {displayValue || '(empty)'}</Text>\n </Stack>\n );\n}\n```\n\nUse uncontrolled mode when you need to capture the input value without managing state yourself.\n\n**Note:** SearchInput's `onChange` receives the string value directly, not an event object.\n\n### Controlled mode\n\nFor scenarios requiring programmatic control or coordination with other components, use controlled mode:\n\n```jsx live-dev\nconst App = () => {\n const [value, setValue] = useState<string>('');\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value={value}\n onChange={(value) => setValue(value)}\n placeholder=\"Controlled search\"\n />\n <Text fontSize=\"sm\">\n {value ? `Searching for: ${value}` : 'Start typing...'}\n </Text>\n <Button onClick={() => setValue('')}>\n Clear programmatically\n </Button>\n </Stack>\n );\n}\n```\n\nUse controlled mode when you need to:\n- Synchronize the value with external state\n- Validate or transform search queries\n- Clear or programmatically set the value\n- React to changes in real-time\n- Coordinate with other components (like displaying search results)\n\n## Component requirements\n\n## Accessibility\n\nThe SearchInput handles most accessibility requirements internally. However, you must always associate an internationalized label with the component. Visual labels are preferable, and can be set by:\n\n- Using the `SearchInputField` pattern component (if available, recommended)\n- Associating a `<label>` element with the `SearchInput` using `aria-labelledby`:\n\n```tsx\n<label id=\"search-label\">\n {msg.format(searchMessage)}\n</label>\n<SearchInput aria-labelledby=\"search-label\" />\n```\n\n- Associating a `<label>` element with the `SearchInput` using `htmlFor`:\n\n```tsx\n<label htmlFor=\"search-input\">\n {msg.format(searchMessage)}\n</label>\n<SearchInput id=\"search-input\" />\n```\n\nIf your design requires that the label should not be visible, the label should be set using the `aria-label` prop:\n\n```tsx\n<SearchInput aria-label={msg.format(searchMessage)} />\n```\n\nIf your use case requires tracking and analytics for this component, it is good practice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"product-search-input\";\n\nexport const ProductSearch = () => (\n <SearchInput id={PERSISTENT_ID} placeholder=\"Search products...\" />\n);\n```\n\n#### Keyboard navigation\n\nThe component supports full keyboard interaction:\n- `Tab` / `Shift+Tab`: Navigate to/from the search field\n- `Arrow keys`: Move cursor within the text (Left/Right)\n- `Home` / `End`: Move cursor to start/end of text\n- `Ctrl+A` / `Cmd+A`: Select all text\n- `Enter`: Submit search (triggers `onSubmit` handler)\n- `Escape`: Clear the search input (triggers `onClear` handler)\n- Standard text editing shortcuts: Cut, Copy, Paste, Undo, Redo\n\n#### Click-to-focus behavior\n\nClicking anywhere within the input container (including the search icon area) will focus the input field, providing a larger interactive target for better usability.\n\n## API reference\n\n<PropsTable id=\"SearchInput\" />\n\n## Common patterns\n\n### Search with live filtering\n\nDisplay results that update as the user types:\n\n```jsx live-dev\nconst App = () => {\n const [query, setQuery] = useState<string>('');\n\n const products = [\n 'Laptop', 'Phone', 'Tablet', 'Monitor', 'Keyboard',\n 'Mouse', 'Headphones', 'Camera', 'Printer', 'Scanner'\n ];\n\n const filteredProducts = products.filter(product =>\n product.toLowerCase().includes(query.toLowerCase())\n );\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <SearchInput\n value={query}\n onChange={setQuery}\n placeholder=\"Search products...\"\n />\n <Stack direction=\"column\" gap=\"200\">\n {query && filteredProducts.length === 0 && (\n <Text fontSize=\"sm\" color=\"neutral.11\">\n No results found for \"{query}\"\n </Text>\n )}\n {query && filteredProducts.length > 0 && (\n <Text fontSize=\"sm\" fontWeight=\"medium\">\n Found {filteredProducts.length} result{filteredProducts.length !== 1 ? 's' : ''}:\n </Text>\n )}\n {query && filteredProducts.map(product => (\n <Text key={product} fontSize=\"sm\">\n • {product}\n </Text>\n ))}\n </Stack>\n </Stack>\n );\n}\n```\n\n### Search with submit action\n\nHandle search submission with Enter key or button click:\n\n```jsx live-dev\nconst App = () => {\n const [query, setQuery] = useState<string>('');\n const [results, setResults] = useState<string[]>([]);\n const [isSearching, setIsSearching] = useState<boolean>(false);\n\n const performSearch = (searchQuery: string) => {\n if (!searchQuery.trim()) {\n setResults([]);\n return;\n }\n\n setIsSearching(true);\n\n // Simulate API call delay\n setTimeout(() => {\n const mockResults = [\n `Result 1 for \"${searchQuery}\"`,\n `Result 2 for \"${searchQuery}\"`,\n `Result 3 for \"${searchQuery}\"`,\n ];\n setResults(mockResults);\n setIsSearching(false);\n }, 500);\n };\n\n return (\n <Stack direction=\"column\" gap=\"400\">\n <Stack direction=\"row\" gap=\"200\" alignItems=\"center\">\n <SearchInput\n value={query}\n onChange={setQuery}\n onSubmit={performSearch}\n placeholder=\"Type and press Enter...\"\n />\n <Button\n onClick={() => performSearch(query)}\n size=\"md\"\n >\n Search\n </Button>\n </Stack>\n\n {isSearching && (\n <Text fontSize=\"sm\" color=\"neutral.11\">\n Searching...\n </Text>\n )}\n\n {!isSearching && results.length > 0 && (\n <Stack direction=\"column\" gap=\"200\">\n <Text fontSize=\"sm\" fontWeight=\"medium\">\n Search Results:\n </Text>\n {results.map((result, index) => (\n <Text key={index} fontSize=\"sm\">\n • {result}\n </Text>\n ))}\n </Stack>\n )}\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using SearchInput within your application. As the component's internal functionality is already tested by Nimbus, these patterns help you verify your integration and application-specific logic.\n\n### Basic Rendering Tests\n\nVerify the component renders with expected elements\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune, Visibility } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"SearchInput - Basic rendering\", () => {\n it(\"renders search input with search icon\", () => {\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toBeInTheDocument();\n expect(searchBox).toHaveAttribute(\"type\", \"search\");\n expect(searchBox).toHaveAttribute(\"placeholder\", \"Search...\");\n });\n\n it(\"renders with clear button (hidden when empty)\", () => {\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n // Clear button is always in DOM\n const clearButton = screen.getByRole(\"button\", { hidden: true });\n expect(clearButton).toBeInTheDocument();\n\n // But hidden visually when empty\n const computedStyle = window.getComputedStyle(clearButton);\n expect(computedStyle.opacity).toBe(\"0\");\n expect(computedStyle.pointerEvents).toBe(\"none\");\n });\n});\n```\n\n### Interaction Tests\n\nTest user interactions with the search input\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune, Visibility } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"SearchInput - Interactions\", () => {\n it(\"handles typing in search input\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test query\");\n\n expect(searchBox).toHaveValue(\"test query\");\n });\n\n it(\"calls onChange with string value on input\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n\n render(\n <NimbusProvider>\n <SearchInput\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test\");\n\n // onChange receives string, not event\n expect(handleChange).toHaveBeenCalledWith(\"test\");\n });\n\n it(\"shows clear button when input has value\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test\");\n\n const clearButton = screen.getByRole(\"button\");\n\n await waitFor(() => {\n const computedStyle = window.getComputedStyle(clearButton);\n expect(computedStyle.opacity).toBe(\"1\");\n expect(computedStyle.pointerEvents).toBe(\"auto\");\n });\n });\n\n it(\"clears input when clear button is clicked\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test query\");\n expect(searchBox).toHaveValue(\"test query\");\n\n const clearButton = screen.getByRole(\"button\");\n await user.click(clearButton);\n\n expect(searchBox).toHaveValue(\"\");\n });\n\n it(\"calls onClear when clear button is clicked\", async () => {\n const user = userEvent.setup();\n const handleClear = vi.fn();\n\n render(\n <NimbusProvider>\n <SearchInput\n defaultValue=\"test\"\n onClear={handleClear}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const clearButton = screen.getByRole(\"button\");\n await user.click(clearButton);\n\n expect(handleClear).toHaveBeenCalled();\n });\n});\n```\n\n### Keyboard Navigation Tests\n\nTest keyboard interactions and shortcuts\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune, Visibility } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"SearchInput - Keyboard navigation\", () => {\n it(\"focuses on tab key\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).not.toHaveFocus();\n\n await user.tab();\n expect(searchBox).toHaveFocus();\n });\n\n it(\"clears input on Escape key\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test query\");\n expect(searchBox).toHaveValue(\"test query\");\n\n await user.keyboard(\"{Escape}\");\n expect(searchBox).toHaveValue(\"\");\n });\n\n it(\"calls onSubmit on Enter key\", async () => {\n const user = userEvent.setup();\n const handleSubmit = vi.fn();\n\n render(\n <NimbusProvider>\n <SearchInput\n onSubmit={handleSubmit}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test query\");\n await user.keyboard(\"{Enter}\");\n\n expect(handleSubmit).toHaveBeenCalledWith(\"test query\");\n });\n});\n```\n\n### Controlled Mode Tests\n\nTest controlled component behavior\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune, Visibility } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"SearchInput - Controlled mode\", () => {\n it(\"updates controlled value via onChange\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n\n const { rerender } = render(\n <NimbusProvider>\n <SearchInput\n value=\"\"\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n await user.type(searchBox, \"test\");\n\n // onChange called once per character - receives strings\n expect(handleChange).toHaveBeenCalledTimes(4);\n\n // Verify all calls receive strings\n for (const call of handleChange.mock.calls) {\n expect(typeof call[0]).toBe(\"string\");\n }\n\n // Rerender with new value\n rerender(\n <NimbusProvider>\n <SearchInput\n value=\"test\"\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n expect(searchBox).toHaveValue(\"test\");\n });\n\n it(\"can be programmatically cleared\", async () => {\n let value = \"initial value\";\n const handleChange = vi.fn((newValue: string) => {\n value = newValue;\n });\n\n const { rerender } = render(\n <NimbusProvider>\n <SearchInput\n value={value}\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toHaveValue(\"initial value\");\n\n // Programmatically clear\n value = \"\";\n rerender(\n <NimbusProvider>\n <SearchInput\n value={value}\n onChange={handleChange}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n expect(searchBox).toHaveValue(\"\");\n });\n});\n```\n\n### State Tests\n\nTest disabled, invalid, and read-only states\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune, Visibility } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"SearchInput - States\", () => {\n it(\"disables input when isDisabled is true\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput isDisabled placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toBeDisabled();\n\n // Cannot type when disabled\n await user.type(searchBox, \"test\");\n expect(searchBox).toHaveValue(\"\");\n });\n\n it(\"marks input as invalid when isInvalid is true\", () => {\n render(\n <NimbusProvider>\n <SearchInput isInvalid placeholder=\"Search...\" aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n // Check parent container for data-invalid attribute\n const container = searchBox.closest(\"[data-invalid]\");\n expect(container).toHaveAttribute(\"data-invalid\", \"true\");\n });\n\n it(\"prevents editing when isReadOnly is true\", async () => {\n const user = userEvent.setup();\n render(\n <NimbusProvider>\n <SearchInput\n isReadOnly\n defaultValue=\"read-only value\"\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toHaveAttribute(\"readonly\");\n expect(searchBox).toHaveValue(\"read-only value\");\n\n // Cannot type when read-only\n await user.click(searchBox);\n await user.type(searchBox, \"test\");\n expect(searchBox).toHaveValue(\"read-only value\");\n });\n\n it(\"disables clear button when input is disabled\", () => {\n render(\n <NimbusProvider>\n <SearchInput\n isDisabled\n defaultValue=\"test\"\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const clearButton = screen.getByRole(\"button\", { hidden: true });\n expect(clearButton).toBeDisabled();\n });\n\n it(\"disables clear button when input is read-only\", () => {\n render(\n <NimbusProvider>\n <SearchInput\n isReadOnly\n defaultValue=\"test\"\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const clearButton = screen.getByRole(\"button\");\n expect(clearButton).toBeDisabled();\n });\n});\n```\n\n### Event Handler Tests\n\nTest onChange, onSubmit, and onClear event handlers\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune, Visibility } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"SearchInput - Event handlers\", () => {\n it(\"calls all event handlers appropriately\", async () => {\n const user = userEvent.setup();\n const handleChange = vi.fn();\n const handleSubmit = vi.fn();\n const handleClear = vi.fn();\n\n render(\n <NimbusProvider>\n <SearchInput\n onChange={handleChange}\n onSubmit={handleSubmit}\n onClear={handleClear}\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n\n // Type to trigger onChange\n await user.type(searchBox, \"test\");\n expect(handleChange).toHaveBeenCalled();\n expect(handleChange).toHaveBeenLastCalledWith(\"test\");\n\n // Press Enter to trigger onSubmit\n await user.keyboard(\"{Enter}\");\n expect(handleSubmit).toHaveBeenCalledWith(\"test\");\n\n // Click clear button to trigger onClear\n const clearButton = screen.getByRole(\"button\");\n await user.click(clearButton);\n expect(handleClear).toHaveBeenCalled();\n });\n\n it(\"clears value on Escape key when focused\", async () => {\n const user = userEvent.setup();\n\n render(\n <NimbusProvider>\n <SearchInput\n defaultValue=\"test value\"\n placeholder=\"Search...\"\n aria-label=\"Search\"\n />\n </NimbusProvider>\n );\n\n const searchBox = screen.getByRole(\"searchbox\");\n expect(searchBox).toHaveValue(\"test value\");\n\n // Focus the input first\n await user.click(searchBox);\n await user.keyboard(\"{Escape}\");\n\n expect(searchBox).toHaveValue(\"\");\n });\n});\n```\n\n### Leading and Trailing Element Tests\n\nVerify configurable leading content and trailing elements\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { SearchInput, IconButton, NimbusProvider } from \"@commercetools/nimbus\";\nimport { Tune, Visibility } from \"@commercetools/nimbus-icons\";\n\ndescribe(\"SearchInput - Leading and trailing elements\", () => {\n it(\"renders the search icon by default\", () => {\n render(\n <NimbusProvider>\n <SearchInput aria-label=\"Search\" />\n </NimbusProvider>\n );\n\n const field = screen.getByRole(\"searchbox\").closest(\"div\");\n expect(\n field?.querySelector(\".nimbus-search-input__leadingElement\")\n ).toBeInTheDocument();\n });\n\n it(\"replaces the leading icon when leadingElement is provided\", () => {\n render(\n <NimbusProvider>\n <SearchInput\n aria-label=\"Search\"\n leadingElement={<Visibility data-testid=\"custom-leading\" />}\n />\n </NimbusProvider>\n );\n\n expect(screen.getByTestId(\"custom-leading\")).toBeInTheDocument();\n });\n\n it(\"renders no leading element when leadingElement is null\", () => {\n render(\n <NimbusProvider>\n <SearchInput aria-label=\"Search\" leadingElement={null} />\n </NimbusProvider>\n );\n\n const field = screen.getByRole(\"searchbox\").closest(\"div\");\n expect(\n field?.querySelector(\".nimbus-search-input__leadingElement\")\n ).not.toBeInTheDocument();\n });\n\n it(\"renders a trailing element after the input\", () => {\n render(\n <NimbusProvider>\n <SearchInput\n aria-label=\"Search\"\n trailingElement={<Tune data-testid=\"custom-trailing\" />}\n />\n </NimbusProvider>\n );\n\n const trailing = screen.getByTestId(\"custom-trailing\");\n expect(trailing.parentElement).toHaveClass(\n \"nimbus-search-input__trailingElement\"\n );\n });\n\n it(\"runs a trailing button's own handler without clearing the value\", async () => {\n const user = userEvent.setup();\n const onFilter = vi.fn();\n\n render(\n <NimbusProvider>\n <SearchInput\n aria-label=\"Search\"\n defaultValue=\"shoes\"\n trailingElement={\n <IconButton\n size=\"xs\"\n variant=\"ghost\"\n aria-label=\"Filter results\"\n onPress={onFilter}\n >\n <Tune />\n </IconButton>\n }\n />\n </NimbusProvider>\n );\n\n await user.click(screen.getByRole(\"button\", { name: \"Filter results\" }));\n\n expect(onFilter).toHaveBeenCalledTimes(1);\n expect(screen.getByRole(\"searchbox\")).toHaveValue(\"shoes\");\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-searchinput--docs)\n- [React Aria SearchField](https://react-spectrum.adobe.com/react-aria/SearchField.html)\n- [ARIA Search Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/examples/search.html)\n",
223
223
  "toc": [
224
224
  {
225
225
  "value": "Getting started",
@@ -296,6 +296,17 @@
296
296
  ],
297
297
  "parent": "root"
298
298
  },
299
+ {
300
+ "value": "Leading and trailing elements",
301
+ "href": "#leading-and-trailing-elements",
302
+ "depth": 3,
303
+ "numbering": [
304
+ 1,
305
+ 2,
306
+ 4
307
+ ],
308
+ "parent": "root"
309
+ },
299
310
  {
300
311
  "value": "Disabled state",
301
312
  "href": "#disabled-state",
@@ -303,7 +314,7 @@
303
314
  "numbering": [
304
315
  1,
305
316
  2,
306
- 4
317
+ 5
307
318
  ],
308
319
  "parent": "root"
309
320
  },
@@ -314,7 +325,7 @@
314
325
  "numbering": [
315
326
  1,
316
327
  2,
317
- 5
328
+ 6
318
329
  ],
319
330
  "parent": "root"
320
331
  },
@@ -325,7 +336,7 @@
325
336
  "numbering": [
326
337
  1,
327
338
  2,
328
- 6
339
+ 7
329
340
  ],
330
341
  "parent": "root"
331
342
  },
@@ -336,7 +347,7 @@
336
347
  "numbering": [
337
348
  1,
338
349
  2,
339
- 7
350
+ 8
340
351
  ],
341
352
  "parent": "root"
342
353
  },
@@ -347,7 +358,7 @@
347
358
  "numbering": [
348
359
  1,
349
360
  2,
350
- 8
361
+ 9
351
362
  ],
352
363
  "parent": "root"
353
364
  },
@@ -358,7 +369,7 @@
358
369
  "numbering": [
359
370
  1,
360
371
  2,
361
- 9
372
+ 10
362
373
  ],
363
374
  "parent": "root"
364
375
  },
@@ -524,6 +535,17 @@
524
535
  ],
525
536
  "parent": "root"
526
537
  },
538
+ {
539
+ "value": "Leading and Trailing Element Tests",
540
+ "href": "#leading-and-trailing-element-tests",
541
+ "depth": 3,
542
+ "numbering": [
543
+ 1,
544
+ 7,
545
+ 7
546
+ ],
547
+ "parent": "root"
548
+ },
527
549
  {
528
550
  "value": "Resources",
529
551
  "href": "#resources",