@neo4j-ndl/react 4.18.13 → 4.20.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 (246) hide show
  1. package/lib/LICENSES.txt +54 -0
  2. package/lib/NOTICE.txt +8 -0
  3. package/lib/cjs/_common/use-semi-controlled-state.js +6 -1
  4. package/lib/cjs/_common/use-semi-controlled-state.js.map +1 -1
  5. package/lib/cjs/combobox/Combobox.js +1031 -0
  6. package/lib/cjs/combobox/Combobox.js.map +1 -0
  7. package/lib/cjs/combobox/combobox-types.js +23 -0
  8. package/lib/cjs/combobox/combobox-types.js.map +1 -0
  9. package/lib/cjs/combobox/combobox-utils.js +129 -0
  10. package/lib/cjs/combobox/combobox-utils.js.map +1 -0
  11. package/lib/cjs/combobox/index.js +26 -0
  12. package/lib/cjs/combobox/index.js.map +1 -0
  13. package/lib/cjs/combobox/stories/combobox-action-on-select.story.js +82 -0
  14. package/lib/cjs/combobox/stories/combobox-action-on-select.story.js.map +1 -0
  15. package/lib/cjs/combobox/stories/combobox-actions.story.js +78 -0
  16. package/lib/cjs/combobox/stories/combobox-actions.story.js.map +1 -0
  17. package/lib/cjs/combobox/stories/combobox-async.story.js +75 -0
  18. package/lib/cjs/combobox/stories/combobox-async.story.js.map +1 -0
  19. package/lib/cjs/combobox/stories/combobox-clean.story.js +38 -0
  20. package/lib/cjs/combobox/stories/combobox-clean.story.js.map +1 -0
  21. package/lib/cjs/combobox/stories/combobox-creatable.story.js +41 -0
  22. package/lib/cjs/combobox/stories/combobox-creatable.story.js.map +1 -0
  23. package/lib/cjs/combobox/stories/combobox-custom-multi.story.js +45 -0
  24. package/lib/cjs/combobox/stories/combobox-custom-multi.story.js.map +1 -0
  25. package/lib/cjs/combobox/stories/combobox-custom-single.story.js +44 -0
  26. package/lib/cjs/combobox/stories/combobox-custom-single.story.js.map +1 -0
  27. package/lib/cjs/combobox/stories/combobox-custom-trigger.story.js +60 -0
  28. package/lib/cjs/combobox/stories/combobox-custom-trigger.story.js.map +1 -0
  29. package/lib/cjs/combobox/stories/combobox-default.story.js +42 -0
  30. package/lib/cjs/combobox/stories/combobox-default.story.js.map +1 -0
  31. package/lib/cjs/combobox/stories/combobox-descriptions.story.js +45 -0
  32. package/lib/cjs/combobox/stories/combobox-descriptions.story.js.map +1 -0
  33. package/lib/cjs/combobox/stories/combobox-disabled.story.js +34 -0
  34. package/lib/cjs/combobox/stories/combobox-disabled.story.js.map +1 -0
  35. package/lib/cjs/combobox/stories/combobox-fluid.story.js +34 -0
  36. package/lib/cjs/combobox/stories/combobox-fluid.story.js.map +1 -0
  37. package/lib/cjs/combobox/stories/combobox-groups.story.js +49 -0
  38. package/lib/cjs/combobox/stories/combobox-groups.story.js.map +1 -0
  39. package/lib/cjs/combobox/stories/combobox-hidden-label.story.js +33 -0
  40. package/lib/cjs/combobox/stories/combobox-hidden-label.story.js.map +1 -0
  41. package/lib/cjs/combobox/stories/combobox-icons.story.js +46 -0
  42. package/lib/cjs/combobox/stories/combobox-icons.story.js.map +1 -0
  43. package/lib/cjs/combobox/stories/combobox-in-dialog.story.js +40 -0
  44. package/lib/cjs/combobox/stories/combobox-in-dialog.story.js.map +1 -0
  45. package/lib/cjs/combobox/stories/combobox-in-popover-dialog.story.js +40 -0
  46. package/lib/cjs/combobox/stories/combobox-in-popover-dialog.story.js.map +1 -0
  47. package/lib/cjs/combobox/stories/combobox-in-popover.story.js +36 -0
  48. package/lib/cjs/combobox/stories/combobox-in-popover.story.js.map +1 -0
  49. package/lib/cjs/combobox/stories/combobox-large-data-set.story.js +36 -0
  50. package/lib/cjs/combobox/stories/combobox-large-data-set.story.js.map +1 -0
  51. package/lib/cjs/combobox/stories/combobox-max-rows.story.js +42 -0
  52. package/lib/cjs/combobox/stories/combobox-max-rows.story.js.map +1 -0
  53. package/lib/cjs/combobox/stories/combobox-multi.story.js +45 -0
  54. package/lib/cjs/combobox/stories/combobox-multi.story.js.map +1 -0
  55. package/lib/cjs/combobox/stories/combobox-native-form.story.js +49 -0
  56. package/lib/cjs/combobox/stories/combobox-native-form.story.js.map +1 -0
  57. package/lib/cjs/combobox/stories/combobox-select-only.story.js +38 -0
  58. package/lib/cjs/combobox/stories/combobox-select-only.story.js.map +1 -0
  59. package/lib/cjs/combobox/stories/combobox-sizes.story.js +33 -0
  60. package/lib/cjs/combobox/stories/combobox-sizes.story.js.map +1 -0
  61. package/lib/cjs/combobox/stories/combobox-unlimited-rows.story.js +42 -0
  62. package/lib/cjs/combobox/stories/combobox-unlimited-rows.story.js.map +1 -0
  63. package/lib/cjs/combobox/stories/combobox-validation.story.js +33 -0
  64. package/lib/cjs/combobox/stories/combobox-validation.story.js.map +1 -0
  65. package/lib/cjs/combobox/stories/index.js +133 -0
  66. package/lib/cjs/combobox/stories/index.js.map +1 -0
  67. package/lib/cjs/combobox/use-combobox-multi-value-overflow.js +173 -0
  68. package/lib/cjs/combobox/use-combobox-multi-value-overflow.js.map +1 -0
  69. package/lib/cjs/combobox/use-combobox-popover.js +80 -0
  70. package/lib/cjs/combobox/use-combobox-popover.js.map +1 -0
  71. package/lib/cjs/date-picker/DatePicker.js.map +1 -1
  72. package/lib/cjs/dismissible-tag/DismissibleTag.js +1 -1
  73. package/lib/cjs/dismissible-tag/DismissibleTag.js.map +1 -1
  74. package/lib/cjs/icons/generated/custom/PencilSparkle.js +30 -0
  75. package/lib/cjs/icons/generated/custom/PencilSparkle.js.map +1 -0
  76. package/lib/cjs/icons/generated/custom/index.js +4 -2
  77. package/lib/cjs/icons/generated/custom/index.js.map +1 -1
  78. package/lib/cjs/index.js +1 -0
  79. package/lib/cjs/index.js.map +1 -1
  80. package/lib/cjs/select/Select.js +3 -0
  81. package/lib/cjs/select/Select.js.map +1 -1
  82. package/lib/cjs/text-area/TextArea.js.map +1 -1
  83. package/lib/cjs/text-input/TextInput.js.map +1 -1
  84. package/lib/esm/_common/use-semi-controlled-state.js +6 -1
  85. package/lib/esm/_common/use-semi-controlled-state.js.map +1 -1
  86. package/lib/esm/combobox/Combobox.js +1024 -0
  87. package/lib/esm/combobox/Combobox.js.map +1 -0
  88. package/lib/esm/combobox/combobox-types.js +22 -0
  89. package/lib/esm/combobox/combobox-types.js.map +1 -0
  90. package/lib/esm/combobox/combobox-utils.js +112 -0
  91. package/lib/esm/combobox/combobox-utils.js.map +1 -0
  92. package/lib/esm/combobox/index.js +22 -0
  93. package/lib/esm/combobox/index.js.map +1 -0
  94. package/lib/esm/combobox/stories/combobox-action-on-select.story.js +80 -0
  95. package/lib/esm/combobox/stories/combobox-action-on-select.story.js.map +1 -0
  96. package/lib/esm/combobox/stories/combobox-actions.story.js +76 -0
  97. package/lib/esm/combobox/stories/combobox-actions.story.js.map +1 -0
  98. package/lib/esm/combobox/stories/combobox-async.story.js +73 -0
  99. package/lib/esm/combobox/stories/combobox-async.story.js.map +1 -0
  100. package/lib/esm/combobox/stories/combobox-clean.story.js +36 -0
  101. package/lib/esm/combobox/stories/combobox-clean.story.js.map +1 -0
  102. package/lib/esm/combobox/stories/combobox-creatable.story.js +39 -0
  103. package/lib/esm/combobox/stories/combobox-creatable.story.js.map +1 -0
  104. package/lib/esm/combobox/stories/combobox-custom-multi.story.js +43 -0
  105. package/lib/esm/combobox/stories/combobox-custom-multi.story.js.map +1 -0
  106. package/lib/esm/combobox/stories/combobox-custom-single.story.js +42 -0
  107. package/lib/esm/combobox/stories/combobox-custom-single.story.js.map +1 -0
  108. package/lib/esm/combobox/stories/combobox-custom-trigger.story.js +58 -0
  109. package/lib/esm/combobox/stories/combobox-custom-trigger.story.js.map +1 -0
  110. package/lib/esm/combobox/stories/combobox-default.story.js +40 -0
  111. package/lib/esm/combobox/stories/combobox-default.story.js.map +1 -0
  112. package/lib/esm/combobox/stories/combobox-descriptions.story.js +43 -0
  113. package/lib/esm/combobox/stories/combobox-descriptions.story.js.map +1 -0
  114. package/lib/esm/combobox/stories/combobox-disabled.story.js +32 -0
  115. package/lib/esm/combobox/stories/combobox-disabled.story.js.map +1 -0
  116. package/lib/esm/combobox/stories/combobox-fluid.story.js +32 -0
  117. package/lib/esm/combobox/stories/combobox-fluid.story.js.map +1 -0
  118. package/lib/esm/combobox/stories/combobox-groups.story.js +47 -0
  119. package/lib/esm/combobox/stories/combobox-groups.story.js.map +1 -0
  120. package/lib/esm/combobox/stories/combobox-hidden-label.story.js +31 -0
  121. package/lib/esm/combobox/stories/combobox-hidden-label.story.js.map +1 -0
  122. package/lib/esm/combobox/stories/combobox-icons.story.js +44 -0
  123. package/lib/esm/combobox/stories/combobox-icons.story.js.map +1 -0
  124. package/lib/esm/combobox/stories/combobox-in-dialog.story.js +38 -0
  125. package/lib/esm/combobox/stories/combobox-in-dialog.story.js.map +1 -0
  126. package/lib/esm/combobox/stories/combobox-in-popover-dialog.story.js +38 -0
  127. package/lib/esm/combobox/stories/combobox-in-popover-dialog.story.js.map +1 -0
  128. package/lib/esm/combobox/stories/combobox-in-popover.story.js +34 -0
  129. package/lib/esm/combobox/stories/combobox-in-popover.story.js.map +1 -0
  130. package/lib/esm/combobox/stories/combobox-large-data-set.story.js +34 -0
  131. package/lib/esm/combobox/stories/combobox-large-data-set.story.js.map +1 -0
  132. package/lib/esm/combobox/stories/combobox-max-rows.story.js +40 -0
  133. package/lib/esm/combobox/stories/combobox-max-rows.story.js.map +1 -0
  134. package/lib/esm/combobox/stories/combobox-multi.story.js +43 -0
  135. package/lib/esm/combobox/stories/combobox-multi.story.js.map +1 -0
  136. package/lib/esm/combobox/stories/combobox-native-form.story.js +47 -0
  137. package/lib/esm/combobox/stories/combobox-native-form.story.js.map +1 -0
  138. package/lib/esm/combobox/stories/combobox-select-only.story.js +36 -0
  139. package/lib/esm/combobox/stories/combobox-select-only.story.js.map +1 -0
  140. package/lib/esm/combobox/stories/combobox-sizes.story.js +31 -0
  141. package/lib/esm/combobox/stories/combobox-sizes.story.js.map +1 -0
  142. package/lib/esm/combobox/stories/combobox-unlimited-rows.story.js +40 -0
  143. package/lib/esm/combobox/stories/combobox-unlimited-rows.story.js.map +1 -0
  144. package/lib/esm/combobox/stories/combobox-validation.story.js +31 -0
  145. package/lib/esm/combobox/stories/combobox-validation.story.js.map +1 -0
  146. package/lib/esm/combobox/stories/index.js +100 -0
  147. package/lib/esm/combobox/stories/index.js.map +1 -0
  148. package/lib/esm/combobox/use-combobox-multi-value-overflow.js +166 -0
  149. package/lib/esm/combobox/use-combobox-multi-value-overflow.js.map +1 -0
  150. package/lib/esm/combobox/use-combobox-popover.js +77 -0
  151. package/lib/esm/combobox/use-combobox-popover.js.map +1 -0
  152. package/lib/esm/date-picker/DatePicker.js.map +1 -1
  153. package/lib/esm/dismissible-tag/DismissibleTag.js +1 -1
  154. package/lib/esm/dismissible-tag/DismissibleTag.js.map +1 -1
  155. package/lib/esm/icons/generated/custom/PencilSparkle.js +28 -0
  156. package/lib/esm/icons/generated/custom/PencilSparkle.js.map +1 -0
  157. package/lib/esm/icons/generated/custom/index.js +1 -0
  158. package/lib/esm/icons/generated/custom/index.js.map +1 -1
  159. package/lib/esm/index.js +1 -0
  160. package/lib/esm/index.js.map +1 -1
  161. package/lib/esm/select/Select.js +3 -0
  162. package/lib/esm/select/Select.js.map +1 -1
  163. package/lib/esm/text-area/TextArea.js.map +1 -1
  164. package/lib/esm/text-input/TextInput.js.map +1 -1
  165. package/lib/types/_common/use-semi-controlled-state.d.ts +5 -1
  166. package/lib/types/_common/use-semi-controlled-state.d.ts.map +1 -1
  167. package/lib/types/combobox/Combobox.d.ts +24 -0
  168. package/lib/types/combobox/Combobox.d.ts.map +1 -0
  169. package/lib/types/combobox/combobox-types.d.ts +419 -0
  170. package/lib/types/combobox/combobox-types.d.ts.map +1 -0
  171. package/lib/types/combobox/combobox-utils.d.ts +81 -0
  172. package/lib/types/combobox/combobox-utils.d.ts.map +1 -0
  173. package/lib/types/combobox/index.d.ts +23 -0
  174. package/lib/types/combobox/index.d.ts.map +1 -0
  175. package/lib/types/combobox/stories/combobox-action-on-select.story.d.ts +24 -0
  176. package/lib/types/combobox/stories/combobox-action-on-select.story.d.ts.map +1 -0
  177. package/lib/types/combobox/stories/combobox-actions.story.d.ts +24 -0
  178. package/lib/types/combobox/stories/combobox-actions.story.d.ts.map +1 -0
  179. package/lib/types/combobox/stories/combobox-async.story.d.ts +24 -0
  180. package/lib/types/combobox/stories/combobox-async.story.d.ts.map +1 -0
  181. package/lib/types/combobox/stories/combobox-clean.story.d.ts +24 -0
  182. package/lib/types/combobox/stories/combobox-clean.story.d.ts.map +1 -0
  183. package/lib/types/combobox/stories/combobox-creatable.story.d.ts +24 -0
  184. package/lib/types/combobox/stories/combobox-creatable.story.d.ts.map +1 -0
  185. package/lib/types/combobox/stories/combobox-custom-multi.story.d.ts +24 -0
  186. package/lib/types/combobox/stories/combobox-custom-multi.story.d.ts.map +1 -0
  187. package/lib/types/combobox/stories/combobox-custom-single.story.d.ts +24 -0
  188. package/lib/types/combobox/stories/combobox-custom-single.story.d.ts.map +1 -0
  189. package/lib/types/combobox/stories/combobox-custom-trigger.story.d.ts +24 -0
  190. package/lib/types/combobox/stories/combobox-custom-trigger.story.d.ts.map +1 -0
  191. package/lib/types/combobox/stories/combobox-default.story.d.ts +24 -0
  192. package/lib/types/combobox/stories/combobox-default.story.d.ts.map +1 -0
  193. package/lib/types/combobox/stories/combobox-descriptions.story.d.ts +24 -0
  194. package/lib/types/combobox/stories/combobox-descriptions.story.d.ts.map +1 -0
  195. package/lib/types/combobox/stories/combobox-disabled.story.d.ts +24 -0
  196. package/lib/types/combobox/stories/combobox-disabled.story.d.ts.map +1 -0
  197. package/lib/types/combobox/stories/combobox-fluid.story.d.ts +24 -0
  198. package/lib/types/combobox/stories/combobox-fluid.story.d.ts.map +1 -0
  199. package/lib/types/combobox/stories/combobox-groups.story.d.ts +24 -0
  200. package/lib/types/combobox/stories/combobox-groups.story.d.ts.map +1 -0
  201. package/lib/types/combobox/stories/combobox-hidden-label.story.d.ts +24 -0
  202. package/lib/types/combobox/stories/combobox-hidden-label.story.d.ts.map +1 -0
  203. package/lib/types/combobox/stories/combobox-icons.story.d.ts +24 -0
  204. package/lib/types/combobox/stories/combobox-icons.story.d.ts.map +1 -0
  205. package/lib/types/combobox/stories/combobox-in-dialog.story.d.ts +24 -0
  206. package/lib/types/combobox/stories/combobox-in-dialog.story.d.ts.map +1 -0
  207. package/lib/types/combobox/stories/combobox-in-popover-dialog.story.d.ts +24 -0
  208. package/lib/types/combobox/stories/combobox-in-popover-dialog.story.d.ts.map +1 -0
  209. package/lib/types/combobox/stories/combobox-in-popover.story.d.ts +24 -0
  210. package/lib/types/combobox/stories/combobox-in-popover.story.d.ts.map +1 -0
  211. package/lib/types/combobox/stories/combobox-large-data-set.story.d.ts +24 -0
  212. package/lib/types/combobox/stories/combobox-large-data-set.story.d.ts.map +1 -0
  213. package/lib/types/combobox/stories/combobox-max-rows.story.d.ts +24 -0
  214. package/lib/types/combobox/stories/combobox-max-rows.story.d.ts.map +1 -0
  215. package/lib/types/combobox/stories/combobox-multi.story.d.ts +24 -0
  216. package/lib/types/combobox/stories/combobox-multi.story.d.ts.map +1 -0
  217. package/lib/types/combobox/stories/combobox-native-form.story.d.ts +24 -0
  218. package/lib/types/combobox/stories/combobox-native-form.story.d.ts.map +1 -0
  219. package/lib/types/combobox/stories/combobox-select-only.story.d.ts +24 -0
  220. package/lib/types/combobox/stories/combobox-select-only.story.d.ts.map +1 -0
  221. package/lib/types/combobox/stories/combobox-sizes.story.d.ts +24 -0
  222. package/lib/types/combobox/stories/combobox-sizes.story.d.ts.map +1 -0
  223. package/lib/types/combobox/stories/combobox-unlimited-rows.story.d.ts +24 -0
  224. package/lib/types/combobox/stories/combobox-unlimited-rows.story.d.ts.map +1 -0
  225. package/lib/types/combobox/stories/combobox-validation.story.d.ts +24 -0
  226. package/lib/types/combobox/stories/combobox-validation.story.d.ts.map +1 -0
  227. package/lib/types/combobox/stories/index.d.ts +73 -0
  228. package/lib/types/combobox/stories/index.d.ts.map +1 -0
  229. package/lib/types/combobox/use-combobox-multi-value-overflow.d.ts +76 -0
  230. package/lib/types/combobox/use-combobox-multi-value-overflow.d.ts.map +1 -0
  231. package/lib/types/combobox/use-combobox-popover.d.ts +66 -0
  232. package/lib/types/combobox/use-combobox-popover.d.ts.map +1 -0
  233. package/lib/types/date-picker/DatePicker.d.ts.map +1 -1
  234. package/lib/types/dismissible-tag/DismissibleTag.d.ts.map +1 -1
  235. package/lib/types/icons/generated/custom/PencilSparkle.d.ts +29 -0
  236. package/lib/types/icons/generated/custom/PencilSparkle.d.ts.map +1 -0
  237. package/lib/types/icons/generated/custom/index.d.ts +1 -0
  238. package/lib/types/icons/generated/custom/index.d.ts.map +1 -1
  239. package/lib/types/index.d.ts +1 -0
  240. package/lib/types/index.d.ts.map +1 -1
  241. package/lib/types/select/Select.d.ts +3 -0
  242. package/lib/types/select/Select.d.ts.map +1 -1
  243. package/lib/types/text-input/TextInput.d.ts.map +1 -1
  244. package/package.json +10 -4
  245. package/skills/ndl-react/SKILL.md +20 -0
  246. package/skills/ndl-react/components/combobox.md +1487 -0
@@ -0,0 +1,1487 @@
1
+ # Combobox
2
+
3
+ Import: `import { Combobox } from '@neo4j-ndl/react'`
4
+
5
+ ## Props
6
+
7
+ ### Combobox
8
+
9
+ | Prop | Type | Required | Default | Description |
10
+ |------|------|----------|---------|-------------|
11
+ | `actions` | `ComboboxAction[]` | | | Rows that act instead of selecting, pinned below the scrolling options after a divider so they stay visible however long the list is. They are always shown, never filtered by the search value, and never part of the selection. |
12
+ | `autoComplete` | `string` | | | Hint for browser autofill on the native form control. |
13
+ | `components` | `ComboboxComponents<TOption>` | | `{}` | Replaces how selected values and option labels are presented, keeping the behavior and the ARIA of the combobox. |
14
+ | `createOption` | `((searchValue: string) => TOption)` | | `<TOption extends ComboboxOption>( searchValue: string, ): TOption => ({ label: searchValue, value: searchValue, }) as TOption` | Builds the option to create from the search value. Defaults to using the search value as both label and value, which is only enough when the options carry no fields of their own beyond `ComboboxOption`. |
15
+ | `defaultSearchValue` | `string` | | | The initial search value in uncontrolled mode. |
16
+ | `defaultValue` | `ComboboxOption \| TOption[] \| null` | | | The initially selected option in uncontrolled mode. The initially selected options in uncontrolled mode. |
17
+ | `errorText` | `ReactNode` | | | Error message below the control. Puts the control in its error state. |
18
+ | `filterOption` | `ComboboxFilterOption<TOption>` | | | How options are matched against the search value. Defaults to a case-insensitive match on the option label. Pass `false` to skip internal filtering when the options are already filtered, such as when searching server side. |
19
+ | `floatingStrategy` | `'absolute' \| 'fixed'` | | | Positioning strategy used by Floating UI. |
20
+ | `formatCreateLabel` | `((searchValue: string) => ReactNode)` | | `` (searchValue: string) => `Create "${searchValue}"` `` | Text of the create row. Defaults to `Create "<search value>"`. |
21
+ | `formId` | `string` | | | Id of the form the combobox belongs to when it is outside that form. |
22
+ | `hasRequiredIndicator` | `boolean` | | `false` | Whether Required or Optional is shown at the end of the label row. |
23
+ | `hasVisuallyHiddenLabel` | `boolean` | | `false` | Whether the label is clipped out of the layout, leaving it to screen readers only. |
24
+ | `helpText` | `ReactNode` | | | Assistive text below the control. Replaced by `errorText` while there is an error. |
25
+ | `isClean` | `boolean` | | `false` | Whether the control hugs its content without a background or resting outline. |
26
+ | `isClearable` | `boolean` | | `false` | Whether a button for clearing the selection is rendered in the trigger. |
27
+ | `isCreatable` | `boolean` | | `false` | Whether the search value can be turned into a new option, shown as an extra row at the end of the list. Requires `isSearchable`. |
28
+ | `isDisabled` | `boolean` | | `false` | Whether the combobox is disabled. |
29
+ | `isFluid` | `boolean` | | `false` | Whether the combobox fills the available width. |
30
+ | `isLoading` | `boolean` | | `false` | Whether the panel is waiting for options. Replaces the empty message while there are no options. |
31
+ | `isMulti` | `boolean` | | `false` | Whether multiple options can be selected. |
32
+ | `isOpen` | `boolean` | | | Whether the panel is controlled open. |
33
+ | `isPortaled` | `boolean` | | | Whether the panel is wrapped in a floating portal. Defaults to true outside a dialog. |
34
+ | `isRequired` | `boolean` | | `false` | Whether a selection is required. Sets `aria-required` and, when `name` is provided, enables native form validation. |
35
+ | `isSearchable` | `boolean` | | `true` | Whether a search input is rendered inside the panel. On by default; turn it off for a short list (roughly 10 options or fewer) where filtering adds nothing. |
36
+ | `isValidNewOption` | `((searchValue: string, options: TOption[]) => boolean)` | | `( searchValue: string, options: ComboboxOption[], ) => searchValue.trim() !== '' && !options.some((option) => matchesExactly(option, searchValue))` | Whether the search value can become a new option. Defaults to requiring a non-blank value that does not exactly match an existing option or a currently selected one. |
37
+ | `label` | `string` | ✅ | | Accessible label for the combobox. Rendered above the control. |
38
+ | `loadingMessage` | `ReactNode` | | `Loading...` | Text of the loading status. Defaults to `Loading...`. |
39
+ | `maxRows` | `number \| null` | | `1` | Maximum number of rows the selected tags may wrap into, a single row by default. Tags that do not fit are collapsed into a read-only counter tag, such as `+4`, at the end of the last row. Pass `null` to let the control grow with the selection. |
40
+ | `name` | `string` | | | Name used to submit the selected option value through a native form. Above 300 options, only the selection is mirrored to the form, so browser autofill no longer offers the option list. |
41
+ | `onChange` | `((value: TOption \| null) => void) \| ((value: TOption[]) => void)` | | | Called when the selected option changes. `null` when the selection is cleared. Called when the selected options change. |
42
+ | `onCreateOption` | `((option: TOption) => void)` | | | Called after a created option has been selected, so it can be persisted into `options`. Created options are not added to `options` by the combobox. |
43
+ | `onOpenChange` | `((isOpen: boolean) => void)` | | | Called when the panel opens or closes. |
44
+ | `onSearchChange` | `((searchValue: string) => void)` | | | Called when the search value changes. |
45
+ | `options` | `TOption[] \| ComboboxOptionGroup<TOption>[]` | ✅ | | The options to display in the combobox. Either an array of options or an array of option groups. |
46
+ | `placeholder` | `string` | | `Select...` | Text shown in the trigger while nothing is selected. |
47
+ | `portalTarget` | `HTMLElement \| null` | | | Portal target element. Defaults to `document.body`. |
48
+ | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
49
+ | `renderTrigger` | `((triggerProps: ComboboxTriggerProps, state: ComboboxTriggerState<TOption>) => ReactNode)` | | | Replaces the built-in trigger. Spread `triggerProps` onto the focusable element that should open the panel; the panel is anchored to it. The root element then lays out as `display: contents`, so the trigger is placed by whatever contains the combobox, and `isFluid`, `isClean`, `size`, `className` and `style` no longer shape it. |
50
+ | `searchPlaceholder` | `string` | | `Search` | Placeholder for the search input, which also names it. |
51
+ | `searchValue` | `string` | | | The search value in controlled mode. |
52
+ | `size` | `'large' \| 'medium' \| 'small'` | | `medium` | Size of the combobox control. |
53
+ | `triggerRef` | `Ref<HTMLElement>` | | | Ref to the focusable combobox trigger. |
54
+ | `value` | `ComboboxOption \| TOption[] \| null` | | | The selected option in controlled mode. The selected options in controlled mode. |
55
+
56
+ ## Accessibility
57
+
58
+ ## Two compositions
59
+
60
+ `isSearchable` decides which WAI-ARIA combobox pattern applies. The two must not be mixed.
61
+
62
+ **Searchable** (the default) — the trigger opens a dialog that contains its own combobox. DOM focus moves to the search input, so the trigger must not carry `aria-activedescendant`.
63
+
64
+ | Element | Roles and attributes |
65
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
66
+ | Trigger | `role="combobox"`, `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls` pointing at the panel |
67
+ | Panel | `role="dialog"`, `aria-label` from `label` |
68
+ | Search input | `role="combobox"`, `aria-haspopup="listbox"`, `aria-autocomplete="list"`, `aria-expanded`, `aria-controls`, `aria-activedescendant` |
69
+ | Listbox | `role="listbox"`, `aria-label` from `label` |
70
+
71
+ **Not searchable** (`isSearchable={false}`) — the select-only combobox. The trigger is the only `role="combobox"`, DOM focus stays on it while the panel is open, and it owns `aria-activedescendant`.
72
+
73
+ | Element | Roles and attributes |
74
+ | ------- | ------------------------------------------------------------------------------------------------------- |
75
+ | Trigger | `role="combobox"`, `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`, `aria-activedescendant` |
76
+ | Panel | Plain container, no role |
77
+ | Listbox | `role="listbox"`, `aria-label` from `label` |
78
+
79
+ Nesting a second `role="combobox"` inside the popup is deliberate. The trigger is the form control and owns the field label, the current value, and validation ARIA such as `aria-invalid` and `aria-describedby`; the search input is only a filter control and never carries any of those.
80
+
81
+ ## Naming
82
+
83
+ `label` is required and is the only accessible name in the API. It names the trigger, the listbox, and the dialog panel, so there is nothing to keep in sync. The separate `name` prop identifies a value during native form submission and does not participate in accessible naming.
84
+
85
+ Clicking the rendered label moves focus to the trigger without opening the panel, matching a native select label.
86
+
87
+ `hasVisuallyHiddenLabel` clips the label row out of the layout instead of removing it, so the name survives. Use it when something else already names the control, such as a toolbar heading or a preceding sentence, and never as a way to skip the label.
88
+
89
+ The search input inside the panel is named by `searchPlaceholder`. A placeholder alone is not a valid accessible name, so the same text is also rendered as a visually hidden label of the input.
90
+
91
+ ## Validation and messages
92
+
93
+ The trigger is the form control, so it carries all of the validation ARIA.
94
+
95
+ | Prop | Effect on the trigger |
96
+ | ------------ | ----------------------------------------------------------------------- |
97
+ | `helpText` | `aria-describedby` pointing at the help text |
98
+ | `errorText` | `aria-invalid="true"` and `aria-describedby` pointing at the error text |
99
+ | `isRequired` | `aria-required="true"` |
100
+
101
+ - An error replaces the help text, so only one message is ever rendered and only that one is described. Do not put the error in `helpText` as well.
102
+ - The message container is a polite live region that is always in the document, so a validation error is announced when it appears.
103
+ - `hasRequiredIndicator` adds Required or Optional at the end of the label row without changing the accessible name; requiredness is exposed through `aria-required` alone.
104
+ - Without `name`, `isRequired` only marks the trigger with `aria-required`; validation belongs to the form around it.
105
+ - With `name`, `isRequired` also enables native constraint validation. If submission is attempted without a selection, the hidden native control prevents submission, moves focus to the visible trigger, and puts the browser validation message in the existing error live region. An explicit `errorText` takes precedence over that message.
106
+
107
+ ## Native forms
108
+
109
+ Setting `name` opts the combobox into native form behavior through a visually hidden `<select>`. The select is `aria-hidden` because the visible trigger already exposes the label, value, requiredness, and validation state.
110
+
111
+ - Single selection submits the selected option's `value`, or an empty string when nothing is selected.
112
+ - Multi selection uses a native `multiple` select, so `FormData.getAll(name)` returns one entry per selected option.
113
+ - Selected creatable values are included even before the consumer adds them to `options`.
114
+ - `form` associates the value with a form elsewhere in the document, and `autoComplete` is forwarded for browser autofill.
115
+ - A disabled combobox is omitted from submission, matching native controls.
116
+ - Native or autofill changes update the combobox, while combobox changes dispatch a bubbling native `change` event for form listeners.
117
+ - `form.reset()` restores `defaultValue` and `defaultSearchValue` and closes uncontrolled state. Controlled values remain owned by their props.
118
+
119
+ ## Roles and attributes on the list
120
+
121
+ - Options have `role="option"` with `aria-selected` and, when applicable, `aria-disabled`. Action rows are the one exception and carry no `aria-selected`, since nothing about them is selectable.
122
+ - Multi-select listboxes set `aria-multiselectable="true"`.
123
+ - Multi-select checkboxes are visual indicators only and are kept out of the accessibility tree, the tab order, and hit testing. Selection is exposed through each option's `aria-selected` state.
124
+ - Option groups use `role="group"` and are named through `aria-labelledby` by their visible group header.
125
+ - The empty state has `role="status"` so filtering down to no matches is announced. Loading uses the same status slot when `isLoading` is set and there are no options yet.
126
+
127
+ ### Large lists
128
+
129
+ Above roughly a hundred rows the listbox windows its rendering: only the visible slice of rows is in the DOM. Below that threshold nothing changes.
130
+
131
+ - Every option carries `aria-setsize` and `aria-posinset` while windowed, so screen readers announce the position in the full list even though the DOM only holds a slice.
132
+ - Group nesting cannot survive windowing, so above the threshold group headers render as presentational rows instead of `role="group"` containers. The visual grouping is unchanged; the group name is not part of the options' ARIA while windowed.
133
+ - Keyboard navigation is unchanged. Moving the active option scrolls it into the window first, so `aria-activedescendant` always resolves to a rendered element.
134
+
135
+ ## Structure of the control
136
+
137
+ The focusable `role="combobox"` button is the control's first Tab stop. Subsequent Tab presses move through each multi-value remove button in value order and then to the clear button. Clicking anywhere else in the control opens the panel.
138
+
139
+ A disabled combobox keeps its place in the tab order and is marked with `aria-disabled` rather than the native `disabled` attribute, so it stays discoverable to screen reader users. Selected multi-values remain visible as read-only tags, with no remove or clear buttons, and all selection mutation actions are ignored.
140
+
141
+ ### Limiting the rows of a multi selection
142
+
143
+ The selected tags are kept to a single row by default. Tags that do not fit are collapsed into one read-only counter tag at the end of the last row, shown as `+N`. `maxRows` raises the cap, and `maxRows={null}` removes it so the control grows with the selection.
144
+
145
+ - Collapsed tags are taken out of the layout, the accessibility tree, and the tab order, so Tab moves through the visible remove buttons only.
146
+ - The counter is not interactive. It is announced as `N more selected` followed by the collapsed labels, and carries the same list as a `title` tooltip for pointer users. With very many collapsed values, both name at most the first twenty labels and summarize the rest as `and N more`.
147
+ - From one thousand collapsed values the visible counter rounds to whole thousands - `+2K` for 1500 - so it keeps a bounded width. The announced count stays exact.
148
+ - Under a row limit, a selection far larger than the limit could ever show only renders the tags that could possibly be visible; everything past them is part of the counter. With `maxRows={null}` every tag renders, so unbounded rows with thousands of selected values is on the consumer.
149
+ - A collapsed value cannot be removed from the control directly; deselect it through the listbox, where every selected option stays reachable, or with the clear button.
150
+ - Removing a visible tag can bring a collapsed one back into view; focus then moves to the next visible remove button rather than being dropped on a collapsed one.
151
+
152
+ ## Keyboard interaction
153
+
154
+ ### Trigger, panel closed
155
+
156
+ | Key | Behavior |
157
+ | ----------------- | ------------------------------------------------------------------------- |
158
+ | `ArrowDown` | Opens the panel. When not searchable, also makes the first option active. |
159
+ | `ArrowUp` | Opens the panel. When not searchable, also makes the last option active. |
160
+ | `Enter` / `Space` | Opens the panel without making an option active. |
161
+
162
+ Printable-character typeahead on a closed select-only combobox is not implemented.
163
+
164
+ ### Multi-value remove buttons
165
+
166
+ | Key | Behavior |
167
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ |
168
+ | `Enter` / `Space` | Removes the value, then focuses the following remove button or returns focus to the combobox when there is no following value. |
169
+ | `ArrowDown` | Focuses the combobox and opens the panel, activating the first option when the panel does not contain a search. |
170
+ | `ArrowUp` | Focuses the combobox and opens the panel, activating the last option when the panel does not contain a search. |
171
+
172
+ ### Clear button
173
+
174
+ | Key | Behavior |
175
+ | ----------------- | --------------------------------------------------------------------------------------------------------------- |
176
+ | `Enter` / `Space` | Clears the selection. |
177
+ | `ArrowDown` | Focuses the combobox and opens the panel, activating the first option when the panel does not contain a search. |
178
+ | `ArrowUp` | Focuses the combobox and opens the panel, activating the last option when the panel does not contain a search. |
179
+
180
+ ### Trigger, panel open and not searchable
181
+
182
+ Focus stays on the trigger, so it drives the list.
183
+
184
+ | Key | Behavior |
185
+ | -------------- | ------------------------------------------------------------------------------------------------------------------- |
186
+ | `ArrowDown` | Moves virtual focus to the next option, clamping at the end of the list. With no active option, moves to the first. |
187
+ | `ArrowUp` | Moves virtual focus to the previous option. With no active option, moves to the last. |
188
+ | `Home` / `End` | Moves virtual focus to the first / last option. |
189
+ | `Enter` | Selects the active option; a disabled active option is left alone. With no active option, closes the panel. |
190
+ | `Space` | Matches `Enter`. |
191
+ | `Escape` | Closes the panel without propagating to an enclosing component. |
192
+
193
+ ### Search input
194
+
195
+ Focus is inside the panel, so the trigger sees nothing.
196
+
197
+ | Key | Behavior |
198
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
199
+ | `ArrowDown` | Moves virtual focus to the next option, clamping at the end of the list. With no active option, moves to the first. |
200
+ | `ArrowUp` | Moves virtual focus to the previous option. With no active option, moves to the last. |
201
+ | `Enter` | Selects the active option, creates from the search value when the create row is active, or runs an active action. Multi-select toggles it. Does nothing while no option is active or the active option is disabled. |
202
+ | `Escape` | Clears the search value and closes the panel without propagating to an enclosing component. |
203
+ | `Home` / `End` | Left to native text-cursor behavior. |
204
+
205
+ Disabled options are included in keyboard navigation but cannot be selected, following the [APG guidance on the focusability of disabled controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls), which keeps them discoverable to screen reader users. Each one carries `aria-disabled="true"` so its state is announced when it takes virtual focus.
206
+
207
+ No option is active until the user navigates or hovers, and filtering the list clears the active option, so typing a query never puts a selection one keystroke away.
208
+
209
+ ## Creating options
210
+
211
+ `isCreatable` adds one more `role="option"` at the end of the list, so creating uses the same virtual focus, hover, and activation path as every other option. It carries `aria-selected="false"`, and its accessible name is the create text, which by default reads `Create "<search value>"`.
212
+
213
+ - Creating needs text to build the option from, so `isCreatable` requires `isSearchable` and warns when it is missing.
214
+ - The create row is only reached by an explicit `ArrowDown`, `ArrowUp`, or hover, and never sits under `Enter` by default, so typing alone never puts a create action one keystroke away.
215
+ - When the query matches nothing, the create row is the only entry in the listbox and replaces the `role="status"` empty state.
216
+ - Activating it selects the created option, so it is announced through the trigger like any other value, and the search value resets so the list is usable again.
217
+
218
+ ## Actions
219
+
220
+ `actions` adds rows at the end of the list, after a divider, that act instead of selecting. The use case is leaving the selection behind: a row like Create project that navigates into a create flow.
221
+
222
+ An action is a `role="option"` of the listbox, which gives it virtual focus, hover, and `Enter` like any option. It carries no `aria-selected`, so a screen reader never announces it as an unselected choice.
223
+
224
+ Visually, actions are pinned below the scrolling options so they stay in view however long the list is. In the DOM they sit after the scrollable listbox, which claims them through `aria-owns`; in the accessibility tree they remain the last options of the listbox, so keyboard navigation and `aria-activedescendant` treat them exactly like rows at the end of the list. When no option matches, there is no scrolling listbox and the pinned container itself becomes the listbox the trigger controls.
225
+
226
+ - Activating an action runs its `onSelect` and closes the panel, which returns focus to the trigger. The selection is left untouched and `onChange` does not fire.
227
+ - Actions are not results, so they are never filtered by the search value. When nothing matches, the `role="status"` empty state is announced above the list rather than replacing it, and the actions stay reachable.
228
+ - Give an action a label that reads as an action, since the label is its accessible name and nothing in the ARIA says it is not one of the choices.
229
+ - A disabled action stays reachable by keyboard navigation but ignores activation and clicks, the same as a disabled option.
230
+ - Dividers around actions — the one separating them from the options and any `hasDivider` one between two actions — are `aria-hidden` decoration, so a screen reader hears only the rows.
231
+
232
+ ## Focus movement
233
+
234
+ - Opening a searchable combobox moves focus to the search input. Opening a select-only one leaves focus on the trigger.
235
+ - Closing the panel returns focus to the trigger, whichever way it closed: selection, `Escape`, or an outside click.
236
+ - Single select closes on selection. Multi-select stays open so several options can be picked in one pass, matching `Select`.
237
+
238
+ ## Pointer and keyboard virtual focus
239
+
240
+ Hovering an option makes it the active option, so hover, `aria-activedescendant`, and what `Enter` selects always agree; hovering a disabled option leaves the active option unchanged. Only keyboard and filter changes scroll the active option into view, since scrolling on hover would move the list out from under the cursor.
241
+
242
+ ## Custom triggers
243
+
244
+ `renderTrigger` receives the full set of trigger props, including `role="combobox"`, the ARIA above, a keyboard handler, and the ref the panel anchors to. Spread all of them onto a single focusable element. Spreading them onto a non-focusable element breaks keyboard access, since nothing else in the composition is tabbable.
245
+
246
+ A custom trigger replaces the label and the messages as well as the control, which is what makes an alternative field layout possible. Two things follow from that:
247
+
248
+ - It is named with `aria-label` rather than `aria-labelledby`, since it is free to leave the label unrendered.
249
+ - It still carries the `aria-describedby` of the message that would have been rendered. `state.field` hands over the label, the help text, the error text, and the ids they need, so render the message that `field.hasError` selects with `field.errorTextId` or `field.helpTextId`. Setting `helpText` or `errorText` and rendering neither leaves the trigger described by an element that does not exist.
250
+
251
+ ## Custom value and option rendering
252
+
253
+ `components` replaces how an option is presented, never how it behaves. Selection, filtering, virtual focus, and the roles and states of the surrounding elements stay with the combobox, so an override can be as plain as a span.
254
+
255
+ | Override | Replaces | Rendered |
256
+ | ------------- | -------------------------------------------------- | ----------------------------------- |
257
+ | `MultiValue` | The tag of a selected value, remove button and all | In the control, next to the trigger |
258
+ | `SingleValue` | The value text of a single select | Inside the trigger |
259
+ | `OptionLabel` | The label of an option | Inside the option |
260
+
261
+ - A `MultiValue` without a removal affordance of its own leaves the list and the clear button as the ways to deselect, so the multi-value remove button keyboard table above no longer applies. Do not also drop `isClearable` unless reopening the panel is an acceptable way back.
262
+ - An override that does render a control has to stop the click from reaching the control, which would otherwise open the panel, and has to place focus itself once the value it lived in is gone. `remove` deselects the option and knows nothing about focus.
263
+ - `SingleValue` is rendered inside the trigger, which is what makes its content read as the current value. It must stay non-interactive, and inline: a block element inside a button is invalid.
264
+ - `OptionLabel` replaces the label of the option rather than the option itself. `role="option"`, `aria-selected`, `aria-disabled`, the multi-select checkbox, and the description are all kept, so option state never rests on the custom presentation alone. Color in particular carries nothing on its own.
265
+ - Every override is handed `isDisabled`, so a disabled combobox is presented as disabled rather than looking active.
266
+
267
+ ## Implementation guidelines
268
+
269
+ - Provide a concise `label`. It names the trigger, the listbox, and the dialog panel.
270
+ - Reach for `hasVisuallyHiddenLabel` rather than dropping the label when the design has no room for it.
271
+ - Do not rely on leading visuals or selection indicators to communicate option state.
272
+ - Keep the label of an option readable as text when overriding `OptionLabel`, since it is the accessible name of the option.
273
+
274
+ ## Examples
275
+
276
+ ### Default
277
+
278
+ ```tsx
279
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
280
+
281
+ import { Combobox } from '@neo4j-ndl/react';
282
+
283
+ const Component = () => {
284
+ let options = [
285
+ {
286
+ label: 'Option 1 ',
287
+ value: 'option-1',
288
+ },
289
+ { label: 'Option 2', value: 'option-2' },
290
+ {
291
+ label: 'Option 3 has a really, really, really, really, long label',
292
+ value: 'option-3',
293
+ },
294
+ { isDisabled: true, label: 'Option 4', value: 'option-4' },
295
+ ];
296
+
297
+ return (
298
+ <Combobox label="Products" options={options} defaultValue={options[0]} />
299
+ );
300
+ };
301
+
302
+ export default Component;
303
+ ```
304
+
305
+ ### Action On Select
306
+
307
+ ```tsx
308
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
309
+
310
+ import { Combobox, IconButton, TextArea } from '@neo4j-ndl/react';
311
+ import { CommandLineIconOutline } from '@neo4j-ndl/react/icons';
312
+ import { useRef, useState } from 'react';
313
+
314
+ const commands = [
315
+ {
316
+ description: 'Return a small sample of nodes',
317
+ label: 'Match nodes',
318
+ value: 'MATCH (n) RETURN n LIMIT 25',
319
+ },
320
+ {
321
+ description: 'Show the labels and relationships in the database',
322
+ label: 'Visualize schema',
323
+ value: 'CALL db.schema.visualization()',
324
+ },
325
+ {
326
+ description: 'List the indexes available to the current user',
327
+ label: 'Show indexes',
328
+ value: 'SHOW INDEXES',
329
+ },
330
+ ];
331
+
332
+ const Component = () => {
333
+ const [terminalValue, setTerminalValue] = useState('');
334
+ const [searchValue, setSearchValue] = useState('');
335
+ const textAreaRef = useRef<HTMLTextAreaElement>(null);
336
+ return (
337
+ <div className="n-flex n-gap-token-2 n-items-center">
338
+ <TextArea
339
+ ref={textAreaRef}
340
+ label="Command input"
341
+ value={terminalValue}
342
+ htmlAttributes={{
343
+ onChange: (event) => setTerminalValue(event.target.value),
344
+ rows: 5,
345
+ }}
346
+ helpText="Use the button below to search for a command."
347
+ />
348
+ <Combobox
349
+ label="Insert command"
350
+ options={commands}
351
+ searchPlaceholder="Search commands"
352
+ searchValue={searchValue}
353
+ onSearchChange={setSearchValue}
354
+ // The picker activates an option but never keeps it as its value.
355
+ // Keeping the controlled value empty avoids a persistent selected state.
356
+ value={null}
357
+ onChange={(command) => {
358
+ if (command === null) {
359
+ return;
360
+ }
361
+
362
+ setTerminalValue((currentValue) =>
363
+ currentValue === ''
364
+ ? command.value
365
+ : `${currentValue}\n${command.value}`,
366
+ );
367
+ setSearchValue('');
368
+ textAreaRef.current?.focus();
369
+ }}
370
+ renderTrigger={({ ref, ...triggerProps }) => (
371
+ <IconButton
372
+ ref={ref as React.Ref<HTMLButtonElement>}
373
+ description="Insert command"
374
+ htmlAttributes={triggerProps}
375
+ >
376
+ <CommandLineIconOutline />
377
+ </IconButton>
378
+ )}
379
+ />
380
+ </div>
381
+ );
382
+ };
383
+
384
+ export default Component;
385
+ ```
386
+
387
+ ### Actions
388
+
389
+ ```tsx
390
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
391
+
392
+ import { Combobox } from '@neo4j-ndl/react';
393
+ import { PlusIconOutline } from '@neo4j-ndl/react/icons';
394
+
395
+ const options = [
396
+ { label: 'Movie recommendations', value: 'movie-recommendations' },
397
+ { label: 'Fraud detection', value: 'fraud-detection' },
398
+ { label: 'Supply chain', value: 'supply-chain' },
399
+ { label: 'Customer 360', value: 'customer-360' },
400
+ { label: 'Knowledge graph', value: 'knowledge-graph' },
401
+ { label: 'Network topology', value: 'network-topology' },
402
+ { label: 'Identity resolution', value: 'identity-resolution' },
403
+ { label: 'Master data management', value: 'master-data-management' },
404
+ { label: 'Social network analysis', value: 'social-network-analysis' },
405
+ { label: 'Route optimization', value: 'route-optimization' },
406
+ { label: 'Recommendation engine', value: 'recommendation-engine' },
407
+ { label: 'Access control', value: 'access-control' },
408
+ { label: 'Data lineage', value: 'data-lineage' },
409
+ { label: 'Entity resolution', value: 'entity-resolution' },
410
+ { label: 'Bill of materials', value: 'bill-of-materials' },
411
+ { label: 'Impact analysis', value: 'impact-analysis' },
412
+ { label: 'Anti money laundering', value: 'anti-money-laundering' },
413
+ { label: 'Dependency mapping', value: 'dependency-mapping' },
414
+ { label: 'Patient journey', value: 'patient-journey' },
415
+ { label: 'Content personalization', value: 'content-personalization' },
416
+ { label: 'Risk assessment', value: 'risk-assessment' },
417
+ { label: 'Churn prediction', value: 'churn-prediction' },
418
+ { label: 'Inventory tracking', value: 'inventory-tracking' },
419
+ { label: 'Semantic search', value: 'semantic-search' },
420
+ { label: 'Digital twin', value: 'digital-twin' },
421
+ ];
422
+
423
+ const Component = () => {
424
+ return (
425
+ <div className="n-flex n-flex-col n-gap-token-4">
426
+ <Combobox
427
+ label="Project"
428
+ placeholder="Select project"
429
+ searchPlaceholder="Search projects"
430
+ options={options}
431
+ // An action never becomes part of the value. It runs, the panel closes,
432
+ // and the selection is left as it was, which is what makes it usable for
433
+ // leaving the combobox entirely.
434
+ actions={[
435
+ {
436
+ label: 'All projects',
437
+ onSelect: () => console.info('all projects'),
438
+ },
439
+ {
440
+ // A divider above the action separates it from the actions before
441
+ // it. The first action never needs one: it already sits below the
442
+ // divider that separates actions from options.
443
+ hasDivider: true,
444
+ label: 'Create project',
445
+ leadingVisual: <PlusIconOutline />,
446
+ // The search value comes along, so an unmatched query can prefill
447
+ // the flow the action leads to.
448
+ onSelect: ({ searchValue }) =>
449
+ console.info('create project with search value', searchValue),
450
+ },
451
+ ]}
452
+ helpText="actions stay pinned below the scrolling options, and are exempt from searching"
453
+ />
454
+ </div>
455
+ );
456
+ };
457
+
458
+ export default Component;
459
+ ```
460
+
461
+ ### Async
462
+
463
+ ```tsx
464
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
465
+
466
+ import { Combobox, Typography } from '@neo4j-ndl/react';
467
+ import { useEffect, useState } from 'react';
468
+
469
+ const catalog = [
470
+ { label: 'Neo4j', value: 'neo4j' },
471
+ { label: 'Aura', value: 'aura' },
472
+ { label: 'Bloom', value: 'bloom' },
473
+ { label: 'Browser', value: 'browser' },
474
+ { label: 'Cypher', value: 'cypher' },
475
+ { label: 'Data Importer', value: 'data-importer' },
476
+ { label: 'GraphQL', value: 'graphql' },
477
+ ];
478
+
479
+ const SEARCH_DELAY_MS = 600;
480
+
481
+ const Component = () => {
482
+ const [searchValue, setSearchValue] = useState('');
483
+ const [options, setOptions] = useState<typeof catalog>([]);
484
+ const [isLoading, setIsLoading] = useState(false);
485
+ const [hasOpened, setHasOpened] = useState(false);
486
+
487
+ useEffect(() => {
488
+ if (!hasOpened) {
489
+ return;
490
+ }
491
+
492
+ let isCancelled = false;
493
+ const query = searchValue.trim().toLowerCase();
494
+
495
+ setIsLoading(true);
496
+ setOptions([]);
497
+
498
+ const timeoutId = window.setTimeout(() => {
499
+ if (isCancelled) {
500
+ return;
501
+ }
502
+
503
+ setOptions(
504
+ query === ''
505
+ ? catalog
506
+ : catalog.filter((option) =>
507
+ option.label.toLowerCase().includes(query),
508
+ ),
509
+ );
510
+ setIsLoading(false);
511
+ }, SEARCH_DELAY_MS);
512
+
513
+ return () => {
514
+ isCancelled = true;
515
+ window.clearTimeout(timeoutId);
516
+ };
517
+ }, [hasOpened, searchValue]);
518
+
519
+ return (
520
+ <div className="n-flex n-flex-col n-gap-token-4">
521
+ <Combobox
522
+ label="Products"
523
+ // The consumer supplies already-filtered options, so internal
524
+ // filtering must be disabled.
525
+ filterOption={false}
526
+ searchValue={searchValue}
527
+ onSearchChange={setSearchValue}
528
+ onOpenChange={(isOpen) => {
529
+ if (isOpen) {
530
+ setIsLoading(true);
531
+ setHasOpened(true);
532
+ }
533
+ }}
534
+ options={options}
535
+ isLoading={isLoading}
536
+ searchPlaceholder="Search products"
537
+ />
538
+ <Typography variant="body-small" className="n-text-neutral-text-weak">
539
+ The consumer clears, fetches, and filters options after a short delay.
540
+ The loading state is shown while each request is in flight.
541
+ </Typography>
542
+ </div>
543
+ );
544
+ };
545
+
546
+ export default Component;
547
+ ```
548
+
549
+ ### Clean
550
+
551
+ ```tsx
552
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
553
+
554
+ import { Combobox } from '@neo4j-ndl/react';
555
+
556
+ const Component = () => {
557
+ const options = [
558
+ { label: 'Neo4j', value: 'neo4j' },
559
+ { label: 'Aura', value: 'aura' },
560
+ { label: 'Bloom', value: 'bloom' },
561
+ { isDisabled: true, label: 'Unavailable', value: 'unavailable' },
562
+ ];
563
+
564
+ return (
565
+ <Combobox
566
+ label="Product"
567
+ // A clean combobox sits inline, next to whatever already names it.
568
+ hasVisuallyHiddenLabel
569
+ isClean
570
+ options={options}
571
+ defaultValue={options[0]}
572
+ />
573
+ );
574
+ };
575
+
576
+ export default Component;
577
+ ```
578
+
579
+ ### Creatable
580
+
581
+ ```tsx
582
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
583
+
584
+ import { Combobox } from '@neo4j-ndl/react';
585
+ import { useState } from 'react';
586
+
587
+ type Option = {
588
+ label: string;
589
+ value: string;
590
+ };
591
+
592
+ const INITIAL_OPTIONS: Option[] = [
593
+ { label: 'Recommendations', value: 'recommendations' },
594
+ { label: 'Fraud detection', value: 'fraud-detection' },
595
+ { label: 'Knowledge graph', value: 'knowledge-graph' },
596
+ ];
597
+
598
+ const Component = () => {
599
+ const [options, setOptions] = useState(INITIAL_OPTIONS);
600
+ const [value, setValue] = useState<Option[]>([]);
601
+
602
+ return (
603
+ <Combobox
604
+ label="Use cases"
605
+ placeholder="Select use cases"
606
+ searchPlaceholder="Search or add a use case"
607
+ isMulti
608
+ isCreatable
609
+ isClearable
610
+ options={options}
611
+ value={value}
612
+ onChange={(nextValue) => setValue(nextValue)}
613
+ // Created options are not added to `options` by the Combobox, so they are
614
+ // appended here to keep them available for later selections.
615
+ onCreateOption={(option) => setOptions((current) => [...current, option])}
616
+ />
617
+ );
618
+ };
619
+
620
+ export default Component;
621
+ ```
622
+
623
+ ### Custom Multi
624
+
625
+ ```tsx
626
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
627
+
628
+ import { tokens } from '@neo4j-ndl/base';
629
+ import { Combobox, GraphLabel } from '@neo4j-ndl/react';
630
+ import { useState } from 'react';
631
+
632
+ // The option type is free to carry fields of its own. Passing these options to
633
+ // the combobox is what types `option` inside the overrides below.
634
+ type NodeLabelOption = {
635
+ label: string;
636
+ value: string;
637
+ color: string;
638
+ };
639
+
640
+ const options: NodeLabelOption[] = [
641
+ { color: tokens.graph['1'], label: 'Person', value: 'person' },
642
+ { color: tokens.graph['2'], label: 'Movie', value: 'movie' },
643
+ { color: tokens.graph['3'], label: 'Director', value: 'director' },
644
+ { color: tokens.graph['4'], label: 'Actor', value: 'actor' },
645
+ { color: tokens.graph['7'], label: 'Genre', value: 'genre' },
646
+ { color: tokens.graph['12'], label: 'Studio', value: 'studio' },
647
+ ];
648
+
649
+ // One presentation for the selected values and for the options, so both read
650
+ // the same. `as="span"` keeps it non-interactive, which leaves the list and the
651
+ // clear button as the ways to deselect.
652
+ const NodeLabel = ({
653
+ option,
654
+ isDisabled,
655
+ }: {
656
+ option: NodeLabelOption;
657
+ isDisabled: boolean;
658
+ }) => (
659
+ <GraphLabel
660
+ as="span"
661
+ type="node"
662
+ size="small"
663
+ color={option.color}
664
+ isDisabled={isDisabled}
665
+ >
666
+ {option.label}
667
+ </GraphLabel>
668
+ );
669
+
670
+ const Component = () => {
671
+ const [value, setValue] = useState<NodeLabelOption[]>(options.slice(0, 2));
672
+
673
+ return (
674
+ <Combobox
675
+ label="Node labels"
676
+ helpText="Deselect a label in the list to remove it."
677
+ isMulti
678
+ isClearable
679
+ options={options}
680
+ value={value}
681
+ onChange={setValue}
682
+ components={{ MultiValue: NodeLabel, OptionLabel: NodeLabel }}
683
+ />
684
+ );
685
+ };
686
+
687
+ export default Component;
688
+ ```
689
+
690
+ ### Custom Single
691
+
692
+ ```tsx
693
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
694
+
695
+ import { tokens } from '@neo4j-ndl/base';
696
+ import { Combobox, GraphLabel } from '@neo4j-ndl/react';
697
+ import { useState } from 'react';
698
+
699
+ // The option type is free to carry fields of its own. Passing these options to
700
+ // the combobox is what types `option` inside the overrides below.
701
+ type NodeLabelOption = {
702
+ label: string;
703
+ value: string;
704
+ color: string;
705
+ };
706
+
707
+ const options: NodeLabelOption[] = [
708
+ { color: tokens.graph['1'], label: 'Person', value: 'person' },
709
+ { color: tokens.graph['2'], label: 'Movie', value: 'movie' },
710
+ { color: tokens.graph['3'], label: 'Director', value: 'director' },
711
+ { color: tokens.graph['4'], label: 'Actor', value: 'actor' },
712
+ { color: tokens.graph['7'], label: 'Genre', value: 'genre' },
713
+ { color: tokens.graph['12'], label: 'Studio', value: 'studio' },
714
+ ];
715
+
716
+ // One presentation for the selected value and for the options, so both read
717
+ // the same. `as="span"` keeps the label non-interactive.
718
+ const NodeLabel = ({
719
+ option,
720
+ isDisabled,
721
+ }: {
722
+ option: NodeLabelOption;
723
+ isDisabled: boolean;
724
+ }) => (
725
+ <GraphLabel
726
+ as="span"
727
+ type="node"
728
+ size="small"
729
+ color={option.color}
730
+ isDisabled={isDisabled}
731
+ >
732
+ {option.label}
733
+ </GraphLabel>
734
+ );
735
+
736
+ const Component = () => {
737
+ const [value, setValue] = useState<NodeLabelOption | null>(options[0]);
738
+
739
+ return (
740
+ <Combobox
741
+ label="Node label"
742
+ isClearable
743
+ options={options}
744
+ value={value}
745
+ onChange={setValue}
746
+ components={{ OptionLabel: NodeLabel, SingleValue: NodeLabel }}
747
+ />
748
+ );
749
+ };
750
+
751
+ export default Component;
752
+ ```
753
+
754
+ ### Custom Trigger
755
+
756
+ ```tsx
757
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
758
+
759
+ import { Combobox, OutlinedButton } from '@neo4j-ndl/react';
760
+ import { AdjustmentsHorizontalIconOutline } from '@neo4j-ndl/react/icons';
761
+
762
+ const Component = () => {
763
+ const options = [
764
+ { label: 'Neo4j', value: 'neo4j' },
765
+ { label: 'Aura', value: 'aura' },
766
+ { label: 'Bloom', value: 'bloom' },
767
+ ];
768
+
769
+ return (
770
+ <Combobox
771
+ // The custom trigger renders no label of its own, so the label reaches it
772
+ // as its accessible name only.
773
+ label="Product"
774
+ options={options}
775
+ searchPlaceholder="Search products"
776
+ // The panel anchors to whichever element the props land on, and that
777
+ // element becomes the combobox, so it has to be focusable.
778
+ renderTrigger={({ ref, ...triggerProps }, state) => (
779
+ <OutlinedButton
780
+ ref={ref as React.Ref<HTMLButtonElement>}
781
+ htmlAttributes={triggerProps}
782
+ leadingVisual={<AdjustmentsHorizontalIconOutline />}
783
+ >
784
+ {state.selectedOptions[0]?.label ?? 'Pick a product'}
785
+ </OutlinedButton>
786
+ )}
787
+ />
788
+ );
789
+ };
790
+
791
+ export default Component;
792
+ ```
793
+
794
+ ### Descriptions
795
+
796
+ ```tsx
797
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
798
+
799
+ import { Combobox } from '@neo4j-ndl/react';
800
+
801
+ const options = [
802
+ {
803
+ description: 'A fully managed graph database service',
804
+ label: 'AuraDB',
805
+ value: 'auradb',
806
+ },
807
+ {
808
+ description: 'Graph analytics and data science tools',
809
+ label: 'Graph Data Science',
810
+ value: 'graph-data-science',
811
+ },
812
+ {
813
+ description: 'Explore and visualize connected data',
814
+ label: 'Neo4j Bloom',
815
+ value: 'neo4j-bloom',
816
+ },
817
+ ];
818
+
819
+ const Component = () => (
820
+ <Combobox label="Products" options={options} placeholder="Select product" />
821
+ );
822
+
823
+ export default Component;
824
+ ```
825
+
826
+ ### Disabled
827
+
828
+ ```tsx
829
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
830
+
831
+ import { Combobox } from '@neo4j-ndl/react';
832
+
833
+ const options = [
834
+ { label: 'Neo4j', value: 'neo4j' },
835
+ { label: 'Aura', value: 'aura' },
836
+ { label: 'Bloom', value: 'bloom' },
837
+ { label: 'Data Importer', value: 'data-importer' },
838
+ ];
839
+
840
+ const Component = () => (
841
+ <div className="n-flex n-flex-col n-items-start n-gap-token-16">
842
+ <Combobox
843
+ label="Disabled single select"
844
+ defaultValue={options[0]}
845
+ isDisabled
846
+ options={options}
847
+ />
848
+ <Combobox
849
+ label="Disabled multi select"
850
+ defaultValue={options.slice(0, 3)}
851
+ isDisabled
852
+ isMulti
853
+ options={options}
854
+ />
855
+ </div>
856
+ );
857
+
858
+ export default Component;
859
+ ```
860
+
861
+ ### Fluid
862
+
863
+ ```tsx
864
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
865
+
866
+ import { Combobox } from '@neo4j-ndl/react';
867
+
868
+ const options = [
869
+ { label: 'Neo4j', value: 'neo4j' },
870
+ { label: 'Aura', value: 'aura' },
871
+ { label: 'Bloom', value: 'bloom' },
872
+ { label: 'Data Importer', value: 'data-importer' },
873
+ ];
874
+
875
+ const Component = () => (
876
+ <div className="n-w-full">
877
+ <Combobox
878
+ label="Product"
879
+ helpText="This combobox fills the width of its parent."
880
+ isClearable
881
+ isFluid
882
+ options={options}
883
+ />
884
+ </div>
885
+ );
886
+
887
+ export default Component;
888
+ ```
889
+
890
+ ### Groups
891
+
892
+ ```tsx
893
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
894
+
895
+ import { Combobox } from '@neo4j-ndl/react';
896
+
897
+ const Component = () => {
898
+ let options = [
899
+ {
900
+ label: 'Group 1',
901
+ options: [
902
+ { label: 'Option 1', value: 'option-1' },
903
+ { label: 'Option 2', value: 'option-2' },
904
+ { label: 'Option 3', value: 'option-3' },
905
+ ],
906
+ },
907
+ {
908
+ label: 'Group 2',
909
+ options: [
910
+ { label: 'Option 4', value: 'option-4' },
911
+ { isDisabled: true, label: 'Option 5', value: 'option-5' },
912
+ { label: 'Option 6', value: 'option-6' },
913
+ { label: 'Option 7', value: 'option-7' },
914
+ ],
915
+ },
916
+ ];
917
+
918
+ return <Combobox label="Products by category" options={options} />;
919
+ };
920
+
921
+ export default Component;
922
+ ```
923
+
924
+ ### Hidden Label
925
+
926
+ ```tsx
927
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
928
+
929
+ import { Combobox, Typography } from '@neo4j-ndl/react';
930
+
931
+ const options = [
932
+ { label: 'Neo4j', value: 'neo4j' },
933
+ { label: 'Aura', value: 'aura' },
934
+ { label: 'Bloom', value: 'bloom' },
935
+ ];
936
+
937
+ const Component = () => (
938
+ <div className="n-flex n-flex-col n-gap-token-4">
939
+ <Typography variant="body-medium">Filter results by product</Typography>
940
+ <Combobox
941
+ label="Product"
942
+ hasVisuallyHiddenLabel
943
+ options={options}
944
+ placeholder="All products"
945
+ />
946
+ </div>
947
+ );
948
+
949
+ export default Component;
950
+ ```
951
+
952
+ ### Icons
953
+
954
+ ```tsx
955
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
956
+
957
+ import { Combobox } from '@neo4j-ndl/react';
958
+ import {
959
+ BeakerIconOutline,
960
+ BoltIconOutline,
961
+ CloudIconOutline,
962
+ } from '@neo4j-ndl/react/icons';
963
+
964
+ const options = [
965
+ {
966
+ label: 'AuraDB',
967
+ leadingVisual: <CloudIconOutline />,
968
+ value: 'auradb',
969
+ },
970
+ {
971
+ label: 'Graph Data Science',
972
+ leadingVisual: <BeakerIconOutline />,
973
+ value: 'graph-data-science',
974
+ },
975
+ {
976
+ label: 'Neo4j Bloom',
977
+ leadingVisual: <BoltIconOutline />,
978
+ value: 'neo4j-bloom',
979
+ },
980
+ ];
981
+
982
+ const Component = () => (
983
+ <Combobox label="Products" options={options} placeholder="Select product" />
984
+ );
985
+
986
+ export default Component;
987
+ ```
988
+
989
+ ### In Dialog
990
+
991
+ ```tsx
992
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
993
+
994
+ import { Combobox, Dialog, FilledButton } from '@neo4j-ndl/react';
995
+ import { useState } from 'react';
996
+
997
+ const options = [
998
+ { label: 'Neo4j', value: 'neo4j' },
999
+ { label: 'Aura', value: 'aura' },
1000
+ { label: 'Bloom', value: 'bloom' },
1001
+ { label: 'Data Importer', value: 'data-importer' },
1002
+ { label: 'GraphQL Library', value: 'graphql-library' },
1003
+ { label: 'Cypher', value: 'cypher' },
1004
+ ];
1005
+
1006
+ const Component = () => {
1007
+ const [isOpen, setIsOpen] = useState(false);
1008
+
1009
+ return (
1010
+ <>
1011
+ <FilledButton onClick={() => setIsOpen(true)}>Open dialog</FilledButton>
1012
+ <Dialog isOpen={isOpen} onClose={() => setIsOpen(false)} size="medium">
1013
+ <Dialog.Header>Combobox in a dialog</Dialog.Header>
1014
+ <Dialog.Description>
1015
+ The panel automatically uses fixed positioning and stays inside the
1016
+ dialog rather than being portaled.
1017
+ </Dialog.Description>
1018
+ <Dialog.Content>
1019
+ <Combobox
1020
+ label="Product"
1021
+ helpText="Open the list to inspect clipping and stacking."
1022
+ isFluid
1023
+ options={options}
1024
+ searchPlaceholder="Search products"
1025
+ />
1026
+ </Dialog.Content>
1027
+ </Dialog>
1028
+ </>
1029
+ );
1030
+ };
1031
+
1032
+ export default Component;
1033
+ ```
1034
+
1035
+ ### In Popover
1036
+
1037
+ ```tsx
1038
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1039
+
1040
+ import { Combobox, FilledButton, Popover, Typography } from '@neo4j-ndl/react';
1041
+
1042
+ const options = [
1043
+ { label: 'Neo4j', value: 'neo4j' },
1044
+ { label: 'Aura', value: 'aura' },
1045
+ { label: 'Bloom', value: 'bloom' },
1046
+ { label: 'Data Importer', value: 'data-importer' },
1047
+ { label: 'GraphQL Library', value: 'graphql-library' },
1048
+ { label: 'Cypher', value: 'cypher' },
1049
+ ];
1050
+
1051
+ const Component = () => (
1052
+ <Popover isPortaled>
1053
+ <Popover.Trigger hasButtonWrapper>
1054
+ <FilledButton>Open popover</FilledButton>
1055
+ </Popover.Trigger>
1056
+ <Popover.Content className="n-max-h-[240px] n-w-[400px] n-overflow-y-auto n-p-token-16">
1057
+ <div className="n-flex n-flex-col n-gap-token-16">
1058
+ <Typography variant="body-medium">
1059
+ Scroll this popover while the combobox panel is open to verify that it
1060
+ remains anchored and is not clipped by the overflow container.
1061
+ </Typography>
1062
+ <div className="n-h-[120px] n-shrink-0 n-rounded-sm n-border n-border-neutral-border-strong n-bg-neutral-bg-weak" />
1063
+ <Combobox
1064
+ label="Product"
1065
+ helpText="The combobox panel is independently portaled above this popover."
1066
+ isFluid
1067
+ options={options}
1068
+ searchPlaceholder="Search products"
1069
+ />
1070
+ <div className="n-h-[120px] n-shrink-0 n-rounded-sm n-border n-border-neutral-border-strong n-bg-neutral-bg-weak" />
1071
+ </div>
1072
+ </Popover.Content>
1073
+ </Popover>
1074
+ );
1075
+
1076
+ export default Component;
1077
+ ```
1078
+
1079
+ ### In Popover Dialog
1080
+
1081
+ ```tsx
1082
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1083
+
1084
+ import { Combobox, Dialog, FilledButton, Popover } from '@neo4j-ndl/react';
1085
+ import { useState } from 'react';
1086
+
1087
+ const options = [
1088
+ { label: 'Neo4j', value: 'neo4j' },
1089
+ { label: 'Aura', value: 'aura' },
1090
+ { label: 'Bloom', value: 'bloom' },
1091
+ { label: 'Data Importer', value: 'data-importer' },
1092
+ { label: 'GraphQL Library', value: 'graphql-library' },
1093
+ { label: 'Cypher', value: 'cypher' },
1094
+ ];
1095
+
1096
+ const Component = () => {
1097
+ const [isOpen, setIsOpen] = useState(false);
1098
+
1099
+ return (
1100
+ <>
1101
+ <FilledButton onClick={() => setIsOpen(true)}>Open dialog</FilledButton>
1102
+ <Dialog isOpen={isOpen} onClose={() => setIsOpen(false)} size="medium">
1103
+ <Dialog.Header>Combobox in a popover in a dialog</Dialog.Header>
1104
+ <Dialog.Description>
1105
+ This exercises nested portals, fixed positioning, focus handling, and
1106
+ stacking across all three components.
1107
+ </Dialog.Description>
1108
+ <Dialog.Content>
1109
+ <Popover isPortaled hasAnchorPortal strategy="fixed">
1110
+ <Popover.Trigger hasButtonWrapper>
1111
+ <FilledButton>Open popover</FilledButton>
1112
+ </Popover.Trigger>
1113
+ <Popover.Content className="n-w-[400px] n-p-token-16">
1114
+ <Combobox
1115
+ label="Product"
1116
+ errorText="This error styling should inherit the dialog theme."
1117
+ isFluid
1118
+ options={options}
1119
+ searchPlaceholder="Search products"
1120
+ />
1121
+ </Popover.Content>
1122
+ </Popover>
1123
+ </Dialog.Content>
1124
+ </Dialog>
1125
+ </>
1126
+ );
1127
+ };
1128
+
1129
+ export default Component;
1130
+ ```
1131
+
1132
+ ### Large Data Set
1133
+
1134
+ ```tsx
1135
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1136
+
1137
+ import { Combobox } from '@neo4j-ndl/react';
1138
+ import { useState } from 'react';
1139
+
1140
+ type Option = {
1141
+ label: string;
1142
+ value: string;
1143
+ };
1144
+
1145
+ const options: Option[] = Array.from({ length: 10000 }, (_, index) => ({
1146
+ label: `Node ${index + 1}`,
1147
+ value: `node-${index + 1}`,
1148
+ }));
1149
+
1150
+ const Component = () => {
1151
+ const [value, setValue] = useState(options.slice(0, 1500));
1152
+
1153
+ return (
1154
+ <Combobox
1155
+ label="Nodes"
1156
+ isClearable
1157
+ isMulti
1158
+ options={options}
1159
+ value={value}
1160
+ onChange={(nextValue) => setValue(nextValue)}
1161
+ helpText="10,000 options with 1,500 selected: the list renders only its visible rows and the counter rounds to thousands."
1162
+ />
1163
+ );
1164
+ };
1165
+
1166
+ export default Component;
1167
+ ```
1168
+
1169
+ ### Max Rows
1170
+
1171
+ ```tsx
1172
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1173
+
1174
+ import { Combobox } from '@neo4j-ndl/react';
1175
+ import { useState } from 'react';
1176
+
1177
+ type Option = {
1178
+ label: string;
1179
+ value: string;
1180
+ };
1181
+
1182
+ const Component = () => {
1183
+ const options: Option[] = [
1184
+ { label: 'Neo4j', value: 'neo4j' },
1185
+ { label: 'Aura', value: 'aura' },
1186
+ { label: 'Bloom', value: 'bloom' },
1187
+ { label: 'Data Importer', value: 'data-importer' },
1188
+ { label: 'GraphQL Library', value: 'graphql-library' },
1189
+ { label: 'Cypher', value: 'cypher' },
1190
+ { label: 'Operations Manager', value: 'operations-manager' },
1191
+ { label: 'Graph Data Science', value: 'graph-data-science' },
1192
+ ];
1193
+ const [value, setValue] = useState(options);
1194
+
1195
+ return (
1196
+ <Combobox
1197
+ label="Products"
1198
+ isClearable
1199
+ isMulti
1200
+ maxRows={2}
1201
+ options={options}
1202
+ value={value}
1203
+ onChange={(nextValue) => setValue(nextValue)}
1204
+ helpText="Set maxRows to 2 to limit the number of rows to 2."
1205
+ />
1206
+ );
1207
+ };
1208
+
1209
+ export default Component;
1210
+ ```
1211
+
1212
+ ### Multi
1213
+
1214
+ ```tsx
1215
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1216
+
1217
+ import { Combobox } from '@neo4j-ndl/react';
1218
+ import { type ReactNode, useState } from 'react';
1219
+
1220
+ type Option = {
1221
+ label: string;
1222
+ value: string;
1223
+ leadingVisual?: ReactNode;
1224
+ description?: string;
1225
+ isDisabled?: boolean;
1226
+ };
1227
+
1228
+ const Component = () => {
1229
+ const options: Option[] = [
1230
+ { label: 'Neo4j', value: 'neo4j' },
1231
+ { label: 'Aura', value: 'aura' },
1232
+ { label: 'Bloom', value: 'bloom' },
1233
+ { label: 'Data Importer', value: 'data-importer' },
1234
+ {
1235
+ label: 'GraphQL Library has a really really long label',
1236
+ value: 'graphql-library',
1237
+ },
1238
+ { label: 'Cypher', value: 'cypher' },
1239
+ { isDisabled: true, label: 'Unavailable', value: 'unavailable' },
1240
+ { label: 'Operations Manager', value: 'operations-manager' },
1241
+ ];
1242
+ const [value, setValue] = useState(options.slice(0, 5));
1243
+
1244
+ return (
1245
+ <Combobox
1246
+ label="Products"
1247
+ isClearable
1248
+ isMulti
1249
+ options={options}
1250
+ value={value}
1251
+ onChange={(nextValue) => setValue(nextValue)}
1252
+ />
1253
+ );
1254
+ };
1255
+
1256
+ export default Component;
1257
+ ```
1258
+
1259
+ ### Native Form
1260
+
1261
+ ```tsx
1262
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1263
+
1264
+ import {
1265
+ Combobox,
1266
+ FilledButton,
1267
+ OutlinedButton,
1268
+ TextButton,
1269
+ Typography,
1270
+ } from '@neo4j-ndl/react';
1271
+ import { type FormEvent, useRef, useState } from 'react';
1272
+
1273
+ const products = [
1274
+ { label: 'Neo4j', value: 'neo4j' },
1275
+ { label: 'Aura', value: 'aura' },
1276
+ { label: 'Bloom', value: 'bloom' },
1277
+ ];
1278
+
1279
+ const environments = [
1280
+ { label: 'Development', value: 'development' },
1281
+ { label: 'Staging', value: 'staging' },
1282
+ { label: 'Production', value: 'production' },
1283
+ ];
1284
+
1285
+ const Component = () => {
1286
+ const triggerRef = useRef<HTMLElement | null>(null);
1287
+ const [submittedValues, setSubmittedValues] = useState<string[]>([]);
1288
+
1289
+ const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
1290
+ event.preventDefault();
1291
+ setSubmittedValues(
1292
+ Array.from(new FormData(event.currentTarget).entries()).map(
1293
+ ([name, value]) => `${name}: ${String(value)}`,
1294
+ ),
1295
+ );
1296
+ };
1297
+
1298
+ return (
1299
+ <form
1300
+ className="n-flex n-max-w-xl n-flex-col n-gap-token-6"
1301
+ onSubmit={handleSubmit}
1302
+ >
1303
+ <Combobox
1304
+ label="Product"
1305
+ name="product"
1306
+ options={products}
1307
+ defaultValue={products[0]}
1308
+ isRequired
1309
+ hasRequiredIndicator
1310
+ triggerRef={triggerRef}
1311
+ />
1312
+ <Combobox
1313
+ label="Environments"
1314
+ name="environment"
1315
+ options={environments}
1316
+ defaultValue={[environments[0], environments[1]]}
1317
+ isMulti
1318
+ />
1319
+ <div className="n-flex n-flex-wrap n-gap-token-2">
1320
+ <FilledButton type="submit">Submit</FilledButton>
1321
+ <OutlinedButton type="reset">Reset</OutlinedButton>
1322
+ <TextButton type="button" onClick={() => triggerRef.current?.focus()}>
1323
+ Focus product
1324
+ </TextButton>
1325
+ </div>
1326
+ <div aria-live="polite">
1327
+ <Typography variant="body-medium">Submitted form data</Typography>
1328
+ <pre className="n-mt-token-2">
1329
+ {submittedValues.length > 0
1330
+ ? submittedValues.join('\n')
1331
+ : 'Submit the form to inspect its values.'}
1332
+ </pre>
1333
+ </div>
1334
+ </form>
1335
+ );
1336
+ };
1337
+
1338
+ export default Component;
1339
+ ```
1340
+
1341
+ ### Select Only
1342
+
1343
+ ```tsx
1344
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1345
+
1346
+ import { Combobox } from '@neo4j-ndl/react';
1347
+
1348
+ const Component = () => {
1349
+ const options = [
1350
+ { label: 'Neo4j', value: 'neo4j' },
1351
+ { label: 'Aura', value: 'aura' },
1352
+ { label: 'Bloom', value: 'bloom' },
1353
+ { isDisabled: true, label: 'Cypher', value: 'cypher' },
1354
+ ];
1355
+
1356
+ // A list this short has nothing to filter, so the search input is turned
1357
+ // off and the combobox follows the select-only pattern.
1358
+ return <Combobox label="Products" isSearchable={false} options={options} />;
1359
+ };
1360
+
1361
+ export default Component;
1362
+ ```
1363
+
1364
+ ### Sizes
1365
+
1366
+ ```tsx
1367
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1368
+
1369
+ import { Combobox } from '@neo4j-ndl/react';
1370
+
1371
+ const options = [
1372
+ { label: 'Neo4j', value: 'neo4j' },
1373
+ { label: 'Aura', value: 'aura' },
1374
+ { label: 'Bloom', value: 'bloom' },
1375
+ ];
1376
+
1377
+ const Component = () => (
1378
+ <div className="n-flex n-flex-col n-gap-token-16">
1379
+ {(['small', 'medium', 'large'] as const).map((size) => (
1380
+ <div className="n-flex n-flex-col n-gap-token-4" key={size}>
1381
+ <p className="n-capitalize">{size}</p>
1382
+ <div className="n-flex n-flex-wrap n-items-start n-gap-token-16">
1383
+ <Combobox
1384
+ label={`${size} single-select`}
1385
+ defaultValue={options[0]}
1386
+ options={options}
1387
+ size={size}
1388
+ />
1389
+ <Combobox
1390
+ label={`${size} multi-select`}
1391
+ defaultValue={options.slice(0, 2)}
1392
+ isMulti
1393
+ options={options}
1394
+ size={size}
1395
+ />
1396
+ </div>
1397
+ </div>
1398
+ ))}
1399
+ </div>
1400
+ );
1401
+
1402
+ export default Component;
1403
+ ```
1404
+
1405
+ ### Unlimited Rows
1406
+
1407
+ ```tsx
1408
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1409
+
1410
+ import { Combobox } from '@neo4j-ndl/react';
1411
+ import { useState } from 'react';
1412
+
1413
+ type Option = {
1414
+ label: string;
1415
+ value: string;
1416
+ };
1417
+
1418
+ const Component = () => {
1419
+ const options: Option[] = [
1420
+ { label: 'Neo4j', value: 'neo4j' },
1421
+ { label: 'Aura', value: 'aura' },
1422
+ { label: 'Bloom', value: 'bloom' },
1423
+ { label: 'Data Importer', value: 'data-importer' },
1424
+ { label: 'GraphQL Library', value: 'graphql-library' },
1425
+ { label: 'Cypher', value: 'cypher' },
1426
+ { label: 'Operations Manager', value: 'operations-manager' },
1427
+ { label: 'Graph Data Science', value: 'graph-data-science' },
1428
+ ];
1429
+ const [value, setValue] = useState(options);
1430
+
1431
+ return (
1432
+ <Combobox
1433
+ label="Products"
1434
+ isClearable
1435
+ isMulti
1436
+ maxRows={null}
1437
+ options={options}
1438
+ value={value}
1439
+ onChange={(nextValue) => setValue(nextValue)}
1440
+ helpText="Set maxRows to null to let the control grow with the selection."
1441
+ />
1442
+ );
1443
+ };
1444
+
1445
+ export default Component;
1446
+ ```
1447
+
1448
+ ### Validation
1449
+
1450
+ ```tsx
1451
+ import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
1452
+
1453
+ import { Combobox } from '@neo4j-ndl/react';
1454
+
1455
+ const options = [
1456
+ { label: 'Neo4j', value: 'neo4j' },
1457
+ { label: 'Aura', value: 'aura' },
1458
+ { label: 'Bloom', value: 'bloom' },
1459
+ ];
1460
+
1461
+ const Component = () => (
1462
+ <div className="n-flex n-flex-col n-gap-token-16">
1463
+ <Combobox
1464
+ label="Product"
1465
+ helpText="Pick the product the ticket is about."
1466
+ isRequired
1467
+ hasRequiredIndicator
1468
+ options={options}
1469
+ />
1470
+ <Combobox
1471
+ label="Product"
1472
+ errorText="Select a product to continue."
1473
+ isRequired
1474
+ hasRequiredIndicator
1475
+ options={options}
1476
+ />
1477
+ <Combobox
1478
+ label="Environment"
1479
+ helpText="Leave empty to include every environment."
1480
+ hasRequiredIndicator
1481
+ options={options}
1482
+ />
1483
+ </div>
1484
+ );
1485
+
1486
+ export default Component;
1487
+ ```