@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,1024 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
+ /**
14
+ *
15
+ * Copyright (c) "Neo4j"
16
+ * Neo4j Sweden AB [http://neo4j.com]
17
+ *
18
+ * This file is part of Neo4j.
19
+ *
20
+ * Neo4j is free software: you can redistribute it and/or modify
21
+ * it under the terms of the GNU General Public License as published by
22
+ * the Free Software Foundation, either version 3 of the License, or
23
+ * (at your option) any later version.
24
+ *
25
+ * This program is distributed in the hope that it will be useful,
26
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
+ * GNU General Public License for more details.
29
+ *
30
+ * You should have received a copy of the GNU General Public License
31
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
32
+ */
33
+ import { FloatingFocusManager, FloatingPortal, useMergeRefs, } from '@floating-ui/react';
34
+ import { useVirtualizer } from '@tanstack/react-virtual';
35
+ import classNames from 'classnames';
36
+ import { Fragment, useEffect, useId, useMemo, useRef, useState } from 'react';
37
+ import { useSemicontrolledState } from '../_common/use-semi-controlled-state';
38
+ import { needleWarningMessage } from '../_common/utils';
39
+ import { Checkbox } from '../checkbox';
40
+ import { ConditionalWrap } from '../conditional-wrap';
41
+ import { DismissibleTag } from '../dismissible-tag';
42
+ import { Divider } from '../divider';
43
+ import { CheckIconOutline, ChevronDownIconOutline, ExclamationCircleIconSolid, MagnifyingGlassIconOutline, PlusIconOutline, XMarkIconOutline, } from '../icons/generated/heroIcons';
44
+ import { LoadingSpinner } from '../loading-spinner';
45
+ import { ReadOnlyTag } from '../read-only-tag';
46
+ import { useNeedleTheme } from '../theme';
47
+ import { Typography } from '../typography';
48
+ import { asActionRow, asCreateOption, buildCreateOption, filterComboboxOptions, flattenComboboxOptions, formatCreateOptionLabel, getAction, getFirstOptionIndex, getInitialValue, getLastOptionIndex, getNextOptionIndex, isActionRow, isCreateOption, isOptionGroup, isValidNewComboboxOption, } from './combobox-utils';
49
+ import { formatOverflowCount, getOverflowCounterMeasureTexts, useComboboxMultiValueOverflow, } from './use-combobox-multi-value-overflow';
50
+ import { useComboboxPopover } from './use-combobox-popover';
51
+ const DEFAULT_SEARCH_PLACEHOLDER = 'Search';
52
+ const DEFAULT_PLACEHOLDER = 'Select...';
53
+ const DEFAULT_LOADING_MESSAGE = 'Loading...';
54
+ const NO_ACTIVE_INDEX = -1;
55
+ const OPENING_KEYS = ['ArrowDown', 'ArrowUp', 'Enter', ' '];
56
+ // Above this many rendered rows - options and group headers - the listbox
57
+ // switches to windowed rendering. Below it, the DOM, including the nested
58
+ // group semantics, is exactly what it always was.
59
+ const VIRTUALIZATION_ROW_THRESHOLD = 100;
60
+ // Starting guess only: every virtual row is measured once rendered.
61
+ const VIRTUAL_ROW_ESTIMATED_HEIGHT = 38;
62
+ // With a row limit, at most this many candidate tags per permitted row are
63
+ // rendered and measured; anything past them is overflowed arithmetically. Far
64
+ // more than fit a row even with the narrowest plausible tags.
65
+ const TAG_CANDIDATES_PER_ROW = 50;
66
+ // The counter's title and screen reader text name at most this many overflowed
67
+ // options before summarizing the rest as a count.
68
+ const OVERFLOW_LABEL_LIMIT = 20;
69
+ // The hidden form select mirrors the options so browser autofill can match
70
+ // against them, but past this many the DOM cost outweighs an autofill list
71
+ // nobody scrolls. Above it, only what submission, validation, and reset need
72
+ // is mirrored: the selection and the default value. Matches the limit React
73
+ // Aria's HiddenSelect uses for the same trade-off.
74
+ const FORM_OPTION_MIRROR_LIMIT = 300;
75
+ /**
76
+ * Flattens the grouped option items into display rows for the virtualizer,
77
+ * along with a lookup from an option's flat index - the one behind option ids
78
+ * and the active index - to its display row index. Pinned action rows sit at
79
+ * the end of the flat indices but get no display row, so the lookup simply
80
+ * misses for them.
81
+ */
82
+ const buildComboboxDisplayRows = (optionItems) => {
83
+ const displayRows = [];
84
+ const flatToDisplayIndex = [];
85
+ const pushOption = (row) => {
86
+ flatToDisplayIndex.push(displayRows.length);
87
+ displayRows.push({ kind: 'option', row });
88
+ };
89
+ optionItems.forEach((item) => {
90
+ if (isOptionGroup(item)) {
91
+ displayRows.push({ kind: 'group-header', label: item.label });
92
+ item.options.forEach(pushOption);
93
+ return;
94
+ }
95
+ pushOption(item);
96
+ });
97
+ return { displayRows, flatToDisplayIndex };
98
+ };
99
+ export const Combobox = ({ label, name, formId, autoComplete, triggerRef, hasVisuallyHiddenLabel = false, helpText, errorText, isRequired = false, hasRequiredIndicator = false, options, value, defaultValue, onChange, isMulti = false, isSearchable = true, searchValue, defaultSearchValue, onSearchChange, filterOption, searchPlaceholder = DEFAULT_SEARCH_PLACEHOLDER, isLoading = false, loadingMessage = DEFAULT_LOADING_MESSAGE, isCreatable = false, onCreateOption, createOption = buildCreateOption, formatCreateLabel = formatCreateOptionLabel, isValidNewOption = isValidNewComboboxOption, actions, maxRows = 1, isOpen, onOpenChange, isClearable = false, placeholder = DEFAULT_PLACEHOLDER, isDisabled = false, size = 'medium', isFluid = false, isClean = false, isPortaled, portalTarget, floatingStrategy, renderTrigger, components = {}, className, style, htmlAttributes, ref, }) => {
100
+ var _a;
101
+ const baseId = useId();
102
+ const listboxId = `${baseId}-listbox`;
103
+ const panelId = `${baseId}-panel`;
104
+ const triggerId = `${baseId}-trigger`;
105
+ const labelId = `${baseId}-label`;
106
+ const helpTextId = `${baseId}-help-text`;
107
+ const errorTextId = `${baseId}-error-text`;
108
+ const initialValue = getInitialValue(value, defaultValue, isMulti);
109
+ const [selectedValue, setSelectedValue, resetSelectedValue] = useSemicontrolledState({
110
+ isControlled: value !== undefined,
111
+ onChange: (nextValue) => {
112
+ if (isMulti) {
113
+ onChange === null || onChange === void 0 ? void 0 : onChange(Array.isArray(nextValue) ? nextValue : []);
114
+ }
115
+ else if (!Array.isArray(nextValue)) {
116
+ onChange === null || onChange === void 0 ? void 0 : onChange(nextValue);
117
+ }
118
+ },
119
+ state: initialValue,
120
+ });
121
+ const [currentSearchValue, setCurrentSearchValue, resetSearchValue] = useSemicontrolledState({
122
+ isControlled: searchValue !== undefined,
123
+ onChange: onSearchChange,
124
+ state: (_a = searchValue !== null && searchValue !== void 0 ? searchValue : defaultSearchValue) !== null && _a !== void 0 ? _a : '',
125
+ });
126
+ const [isPanelOpen, setIsPanelOpen, resetPanelOpen] = useSemicontrolledState({
127
+ isControlled: isOpen !== undefined,
128
+ onChange: onOpenChange,
129
+ state: isOpen !== null && isOpen !== void 0 ? isOpen : false,
130
+ });
131
+ const { themeClassName } = useNeedleTheme();
132
+ const internalTriggerRef = useRef(null);
133
+ const searchInputRef = useRef(null);
134
+ const { context, floatingStyles, getFloatingProps, getReferenceProps, isPortaled: isPanelPortaled, refs, } = useComboboxPopover({
135
+ floatingStrategy,
136
+ isDisabled,
137
+ isOpen: isPanelOpen,
138
+ isPortaled,
139
+ onOpenChange: setIsPanelOpen,
140
+ });
141
+ const hasCustomTrigger = renderTrigger !== undefined;
142
+ const builtInTriggerRef = useMergeRefs([internalTriggerRef, triggerRef]);
143
+ const customTriggerRef = useMergeRefs([
144
+ refs.setReference,
145
+ internalTriggerRef,
146
+ triggerRef,
147
+ ]);
148
+ // Creating builds an option out of the search value, so there is nothing to
149
+ // create from without a search input.
150
+ const canCreate = isCreatable && isSearchable;
151
+ useEffect(() => {
152
+ if (isCreatable && !isSearchable) {
153
+ needleWarningMessage('A creatable Combobox also needs isSearchable, since a new option is built from the search value.');
154
+ }
155
+ }, [isCreatable, isSearchable]);
156
+ const selectedOptions = getSelectedOptions(selectedValue);
157
+ const resetValue = getInitialValue(undefined, defaultValue, isMulti);
158
+ const resetSelectedOptions = getSelectedOptions(resetValue);
159
+ const selectedValuesKey = selectedOptions
160
+ .map((option) => option.value)
161
+ .join('\u0000');
162
+ // Selection checks run once per rendered option, so with large lists they
163
+ // must not scan the selection array each time.
164
+ const selectedValuesSet = useMemo(() => new Set(getSelectedOptions(selectedValue).map(({ value }) => value)), [selectedValue]);
165
+ const allOptions = useMemo(() => flattenComboboxOptions(options), [options]);
166
+ const [nativeValidationMessage, setNativeValidationMessage] = useState(undefined);
167
+ const formOptions = useMemo(() => {
168
+ const seenValues = new Set();
169
+ // The default value options are mirrored even above the limit, since the
170
+ // native select can only reset to options it renders.
171
+ const mirroredOptions = allOptions.length > FORM_OPTION_MIRROR_LIMIT
172
+ ? [
173
+ ...getSelectedOptions(selectedValue),
174
+ ...getSelectedOptions(getInitialValue(undefined, defaultValue, isMulti)),
175
+ ]
176
+ : [...allOptions, ...getSelectedOptions(selectedValue)];
177
+ return mirroredOptions.filter((option) => {
178
+ if (seenValues.has(option.value)) {
179
+ return false;
180
+ }
181
+ seenValues.add(option.value);
182
+ return true;
183
+ });
184
+ }, [allOptions, selectedValue, defaultValue, isMulti]);
185
+ useEffect(() => {
186
+ setNativeValidationMessage(undefined);
187
+ }, [selectedValuesKey]);
188
+ const resetFromForm = () => {
189
+ resetSelectedValue(resetValue);
190
+ resetSearchValue(defaultSearchValue !== null && defaultSearchValue !== void 0 ? defaultSearchValue : '');
191
+ resetPanelOpen(false);
192
+ setNativeValidationMessage(undefined);
193
+ };
194
+ const filteredOptions = useMemo(() => isSearchable
195
+ ? filterComboboxOptions(options, currentSearchValue, filterOption)
196
+ : options, [isSearchable, options, currentSearchValue, filterOption]);
197
+ // Created options are never added to `options`, so the selection is offered
198
+ // for deduplication as well.
199
+ const createRow = canCreate &&
200
+ isValidNewOption(currentSearchValue, [...allOptions, ...selectedOptions])
201
+ ? asCreateOption(createOption(currentSearchValue))
202
+ : undefined;
203
+ // The create row is appended after any groups rather than inside one, since it
204
+ // belongs to the list as a whole.
205
+ const optionItems = createRow === undefined ? filteredOptions : [...filteredOptions, createRow];
206
+ // Actions are not data, so they survive filtering and close the list off at
207
+ // the end, after anything that selects.
208
+ const actionRows = (actions !== null && actions !== void 0 ? actions : []).map(asActionRow);
209
+ const flatOptions = [
210
+ ...flattenComboboxOptions(optionItems),
211
+ ...actionRows,
212
+ ];
213
+ const optionsKey = flatOptions.map((option) => option.value).join('\u0000');
214
+ // Ids and hover both need a row's index; one O(n) map build per render
215
+ // replaces an O(n) scan per rendered row.
216
+ const rowIndexes = new Map(flatOptions.map((row, index) => [row, index]));
217
+ const getOptionId = (row) => { var _a; return `${baseId}-option-${(_a = rowIndexes.get(row)) !== null && _a !== void 0 ? _a : NO_ACTIVE_INDEX}`; };
218
+ const groupHeaderCount = optionItems.reduce((count, item) => (isOptionGroup(item) ? count + 1 : count), 0);
219
+ // Only the options listbox is windowed; the pinned action rows below it
220
+ // are always rendered, so they do not count towards the threshold.
221
+ const displayRowCount = flatOptions.length - actionRows.length + groupHeaderCount;
222
+ const isListVirtualized = displayRowCount > VIRTUALIZATION_ROW_THRESHOLD;
223
+ const virtualRows = isListVirtualized
224
+ ? buildComboboxDisplayRows(optionItems)
225
+ : undefined;
226
+ // Registered by the virtualized listbox while it is mounted, since scrolling
227
+ // a windowed list works by index rather than by element.
228
+ const scrollToActiveRowRef = useRef(null);
229
+ // No option is active until the user navigates or hovers, so nothing is
230
+ // highlighted and Enter is inert before then.
231
+ const [activeIndex, setActiveIndex] = useState(NO_ACTIVE_INDEX);
232
+ const [renderedOptionsKey, setRenderedOptionsKey] = useState(optionsKey);
233
+ const wasActivatedByPointerRef = useRef(false);
234
+ // Indices change meaning when the list is filtered, so a stale one would
235
+ // point at the wrong option.
236
+ if (renderedOptionsKey !== optionsKey) {
237
+ setRenderedOptionsKey(optionsKey);
238
+ setActiveIndex(NO_ACTIVE_INDEX);
239
+ }
240
+ const activeOption = flatOptions[activeIndex];
241
+ const activeOptionId = activeOption === undefined ? undefined : getOptionId(activeOption);
242
+ // The listbox scrolls, so keep the virtually focused option visible.
243
+ // Pointer activation is skipped, otherwise hovering a partially visible
244
+ // option would nudge the list out from under the cursor. Pinned action rows
245
+ // sit outside the scrolling listbox and are always visible, so scrolling
246
+ // for them is a harmless no-op in both paths.
247
+ useEffect(() => {
248
+ var _a;
249
+ if (wasActivatedByPointerRef.current) {
250
+ wasActivatedByPointerRef.current = false;
251
+ return;
252
+ }
253
+ if (activeOptionId === undefined) {
254
+ return;
255
+ }
256
+ // A windowed listbox scrolls by index, since the active option's element
257
+ // may not be mounted yet. Scrolling is also what mounts it, keeping
258
+ // aria-activedescendant resolvable.
259
+ if (scrollToActiveRowRef.current !== null) {
260
+ scrollToActiveRowRef.current(activeIndex);
261
+ return;
262
+ }
263
+ (_a = document
264
+ .getElementById(activeOptionId)) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'nearest' });
265
+ }, [activeOptionId, activeIndex]);
266
+ // Reopening the panel starts with nothing highlighted, the same as the first
267
+ // open, so a stale active option can never be one Enter away from selection.
268
+ useEffect(() => {
269
+ if (!isPanelOpen) {
270
+ setActiveIndex(NO_ACTIVE_INDEX);
271
+ }
272
+ }, [isPanelOpen]);
273
+ const isOptionSelected = (option) => selectedValuesSet.has(option.value);
274
+ const selectOption = (option) => {
275
+ if (isMulti) {
276
+ const currentValue = Array.isArray(selectedValue) ? selectedValue : [];
277
+ const isSelected = selectedValuesSet.has(option.value);
278
+ const nextValue = isSelected
279
+ ? currentValue.filter((selected) => selected.value !== option.value)
280
+ : [...currentValue, option];
281
+ setSelectedValue(nextValue);
282
+ // Multi select keeps the panel open so several options can be picked in
283
+ // one pass, matching Select.
284
+ return;
285
+ }
286
+ if (Array.isArray(selectedValue) || (selectedValue === null || selectedValue === void 0 ? void 0 : selectedValue.value) !== option.value) {
287
+ setSelectedValue(option);
288
+ }
289
+ setIsPanelOpen(false);
290
+ };
291
+ const createFromSearch = () => {
292
+ const created = createOption(currentSearchValue);
293
+ selectOption(created);
294
+ onCreateOption === null || onCreateOption === void 0 ? void 0 : onCreateOption(created);
295
+ // Multi select stays open, so the query is reset to leave the full list
296
+ // usable for the next pick.
297
+ setCurrentSearchValue('');
298
+ };
299
+ const handleOptionChange = (row) => {
300
+ if (isDisabled || row.isDisabled) {
301
+ return;
302
+ }
303
+ // An action leaves the selection alone, so it only runs and closes.
304
+ if (isActionRow(row)) {
305
+ getAction(row).onSelect({ searchValue: currentSearchValue });
306
+ setIsPanelOpen(false);
307
+ return;
308
+ }
309
+ if (isCreateOption(row)) {
310
+ createFromSearch();
311
+ return;
312
+ }
313
+ selectOption(row);
314
+ };
315
+ const clearSelection = () => {
316
+ if (isDisabled) {
317
+ return;
318
+ }
319
+ setSelectedValue(isMulti ? [] : null);
320
+ };
321
+ const removeOption = (option) => {
322
+ if (isDisabled) {
323
+ return;
324
+ }
325
+ if (!isMulti) {
326
+ clearSelection();
327
+ return;
328
+ }
329
+ setSelectedValue(selectedOptions.filter((selected) => selected.value !== option.value));
330
+ };
331
+ // Hover and the keyboard share one active option, so exactly one option is
332
+ // ever highlighted. Unlike the keyboard, hover skips disabled options: a
333
+ // pointer user sees the disabled state directly, so there is no
334
+ // discoverability to preserve.
335
+ const handleOptionHover = (row) => {
336
+ var _a;
337
+ if (row.isDisabled === true) {
338
+ return;
339
+ }
340
+ const index = (_a = rowIndexes.get(row)) !== null && _a !== void 0 ? _a : NO_ACTIVE_INDEX;
341
+ if (index === activeIndex) {
342
+ return;
343
+ }
344
+ wasActivatedByPointerRef.current = true;
345
+ setActiveIndex(index);
346
+ };
347
+ // Disabled options are included so they stay discoverable to screen reader
348
+ // users, per the APG guidance on disabled controls in composite widgets.
349
+ // Activating one is a no-op.
350
+ const moveActiveIndex = (direction) => {
351
+ if (activeIndex === NO_ACTIVE_INDEX) {
352
+ setActiveIndex(direction === 1
353
+ ? getFirstOptionIndex(flatOptions)
354
+ : getLastOptionIndex(flatOptions));
355
+ return;
356
+ }
357
+ setActiveIndex(getNextOptionIndex(flatOptions, activeIndex, direction));
358
+ };
359
+ /**
360
+ * Shared by the search input and the select-only trigger, since both drive
361
+ * the same virtual focus. Returns whether the key was consumed.
362
+ */
363
+ const handleNavigationKeyDown = (event) => {
364
+ switch (event.key) {
365
+ case 'ArrowDown':
366
+ event.preventDefault();
367
+ moveActiveIndex(1);
368
+ return true;
369
+ case 'ArrowUp':
370
+ event.preventDefault();
371
+ moveActiveIndex(-1);
372
+ return true;
373
+ case 'Home':
374
+ event.preventDefault();
375
+ setActiveIndex(getFirstOptionIndex(flatOptions));
376
+ return true;
377
+ case 'End':
378
+ event.preventDefault();
379
+ setActiveIndex(getLastOptionIndex(flatOptions));
380
+ return true;
381
+ case 'Enter':
382
+ if (activeOption === undefined) {
383
+ return false;
384
+ }
385
+ event.preventDefault();
386
+ handleOptionChange(activeOption);
387
+ return true;
388
+ default:
389
+ return false;
390
+ }
391
+ };
392
+ const handleSearchKeyDown = (event) => {
393
+ // Home and End belong to the text cursor while typing.
394
+ if (event.key === 'Home' || event.key === 'End') {
395
+ return;
396
+ }
397
+ if (handleNavigationKeyDown(event)) {
398
+ return;
399
+ }
400
+ if (event.key === 'Escape') {
401
+ // Escape belongs to this combobox: close immediately and keep it from
402
+ // dismissing an enclosing dialog, popover, or other composite widget.
403
+ event.preventDefault();
404
+ event.stopPropagation();
405
+ if (currentSearchValue !== '') {
406
+ setCurrentSearchValue('');
407
+ }
408
+ setIsPanelOpen(false);
409
+ }
410
+ };
411
+ const handleTriggerKeyDown = (event) => {
412
+ if (isDisabled) {
413
+ return;
414
+ }
415
+ if (!isPanelOpen) {
416
+ if (!OPENING_KEYS.includes(event.key)) {
417
+ return;
418
+ }
419
+ // Preventing the default keeps the button from synthesizing a click,
420
+ // which would let useClick toggle the panel straight back shut.
421
+ event.preventDefault();
422
+ setIsPanelOpen(true);
423
+ if (!isSearchable && event.key === 'ArrowDown') {
424
+ setActiveIndex(getFirstOptionIndex(flatOptions));
425
+ }
426
+ else if (!isSearchable && event.key === 'ArrowUp') {
427
+ setActiveIndex(getLastOptionIndex(flatOptions));
428
+ }
429
+ return;
430
+ }
431
+ if (event.key === 'Escape') {
432
+ event.preventDefault();
433
+ event.stopPropagation();
434
+ setIsPanelOpen(false);
435
+ return;
436
+ }
437
+ // A searchable panel holds DOM focus in its search input, so the trigger
438
+ // only ever sees keys while the panel is closed.
439
+ if (isSearchable) {
440
+ return;
441
+ }
442
+ if (event.key === ' ' && activeOption !== undefined) {
443
+ event.preventDefault();
444
+ handleOptionChange(activeOption);
445
+ return;
446
+ }
447
+ if (handleNavigationKeyDown(event)) {
448
+ return;
449
+ }
450
+ if (event.key === 'Enter' || event.key === ' ') {
451
+ event.preventDefault();
452
+ setIsPanelOpen(false);
453
+ }
454
+ };
455
+ // Actions are rows of the listbox but not results, so they keep the list
456
+ // rendered without hiding the message that nothing matched.
457
+ const hasRows = flatOptions.length > 0;
458
+ const hasOptionRows = flatOptions.length > actionRows.length;
459
+ // The pinned action rows live outside the scrolling listbox, so the listbox
460
+ // claims them through aria-owns to keep them its logical children.
461
+ const actionOwnsIds = actionRows.length > 0
462
+ ? actionRows.map((row) => getOptionId(row)).join(' ')
463
+ : undefined;
464
+ const isDialogPanel = isSearchable;
465
+ // An error replaces the help text, so only one message is ever rendered and
466
+ // only that one is described.
467
+ const displayedErrorText = errorText !== null && errorText !== void 0 ? errorText : nativeValidationMessage;
468
+ const hasError = Boolean(displayedErrorText);
469
+ const hasHelpText = Boolean(helpText) && !hasError;
470
+ const messageId = hasError ? errorTextId : helpTextId;
471
+ const describedById = hasError || hasHelpText ? messageId : undefined;
472
+ const triggerAria = {
473
+ 'aria-activedescendant': isPanelOpen && !isSearchable ? activeOptionId : undefined,
474
+ 'aria-controls': getTriggerControlsId({
475
+ hasRows,
476
+ isDialogPanel,
477
+ isPanelOpen,
478
+ listboxId,
479
+ panelId,
480
+ }),
481
+ 'aria-describedby': describedById,
482
+ 'aria-disabled': isDisabled ? true : undefined,
483
+ 'aria-expanded': isPanelOpen,
484
+ 'aria-haspopup': isDialogPanel ? 'dialog' : 'listbox',
485
+ 'aria-invalid': hasError ? true : undefined,
486
+ 'aria-required': isRequired ? true : undefined,
487
+ // The label points at the trigger through `htmlFor`, so clicking it moves
488
+ // focus to the trigger.
489
+ id: triggerId,
490
+ onKeyDown: handleTriggerKeyDown,
491
+ role: 'combobox',
492
+ };
493
+ // A custom trigger becomes the anchor itself, so it gets the reference props
494
+ // and the reference ref. The built-in trigger leaves both to its wrapper,
495
+ // which is what the panel should size to, and takes only the ARIA.
496
+ //
497
+ // It is also named by the label text itself rather than by a reference to it,
498
+ // since a custom trigger is free to leave the label unrendered.
499
+ const triggerProps = Object.assign(Object.assign(Object.assign({}, getReferenceProps()), triggerAria), { 'aria-label': label, ref: customTriggerRef });
500
+ const builtInTriggerProps = Object.assign(Object.assign({}, triggerAria), {
501
+ // The label row also holds the required indicator, so only the label text
502
+ // is referenced, keeping "Optional" out of the accessible name.
503
+ 'aria-labelledby': labelId, ref: builtInTriggerRef });
504
+ const field = {
505
+ errorText: displayedErrorText,
506
+ errorTextId,
507
+ hasError,
508
+ hasRequiredIndicator,
509
+ hasVisuallyHiddenLabel,
510
+ helpText,
511
+ helpTextId,
512
+ isRequired,
513
+ label,
514
+ labelId,
515
+ triggerId,
516
+ };
517
+ const triggerState = {
518
+ clear: clearSelection,
519
+ field,
520
+ isDisabled,
521
+ isMulti,
522
+ isOpen: isPanelOpen,
523
+ remove: removeOption,
524
+ selectedOptions,
525
+ };
526
+ // While windowed, the DOM no longer holds every option, so each one carries
527
+ // its position in the full list for assistive technology.
528
+ const getSetPositionProps = (row) => {
529
+ var _a;
530
+ return isListVirtualized
531
+ ? {
532
+ posInSet: ((_a = rowIndexes.get(row)) !== null && _a !== void 0 ? _a : 0) + 1,
533
+ setSize: flatOptions.length,
534
+ }
535
+ : {};
536
+ };
537
+ const renderOption = (option) => {
538
+ var _a;
539
+ const sharedProps = Object.assign({ id: getOptionId(option), isFocused: option === activeOption, onClick: () => handleOptionChange(option), onHover: () => handleOptionHover(option) }, getSetPositionProps(option));
540
+ // The create row is never selected and shows the create text rather than
541
+ // the label of the option it would produce.
542
+ if (isCreateOption(option)) {
543
+ return (_jsx(Option, Object.assign({}, sharedProps, { isCreate: true, isSelected: false, children: _jsx(CreateOptionContent, { children: formatCreateLabel(currentSearchValue) }) }), "ndl-combobox-create"));
544
+ }
545
+ const { OptionLabel } = components;
546
+ const isSelected = isOptionSelected(option);
547
+ return (_jsx(Option, Object.assign({}, sharedProps, { isSelected: isSelected, isDisabled: option.isDisabled, children: _jsx(OptionContent, { label: option.label, customLabel: OptionLabel === undefined ? undefined : (_jsx(OptionLabel, { option: option, isSelected: isSelected, isDisabled: (_a = option.isDisabled) !== null && _a !== void 0 ? _a : false, isFocused: option === activeOption, isMulti: isMulti, size: size })), description: option.description, leadingVisual: option.leadingVisual, isDisabled: option.isDisabled, isSelected: isSelected, isMulti: isMulti }) }), option.value));
548
+ };
549
+ // An action row shares the chrome of an option but never the selection, so it
550
+ // is rendered as a single select row: its own leading visual instead of a
551
+ // checkbox, and no selection indicator.
552
+ const renderAction = (row) => {
553
+ const action = getAction(row);
554
+ return (_jsx(Option, Object.assign({ id: getOptionId(row), isAction: true, isDisabled: action.isDisabled, isFocused: row === activeOption, onClick: () => handleOptionChange(row), onHover: () => handleOptionHover(row) }, getSetPositionProps(row), { children: _jsx(OptionContent, { label: action.label, description: action.description, leadingVisual: action.leadingVisual, isDisabled: action.isDisabled }) }), row.value));
555
+ };
556
+ const panel = (_jsxs(Panel, Object.assign({}, getFloatingProps(), { ref: refs.setFloating, id: panelId, ariaLabel: label, isDialog: isDialogPanel, className: themeClassName, style: floatingStyles, children: [isSearchable && (_jsx(Input, { ref: searchInputRef, ariaLabel: searchPlaceholder, value: currentSearchValue, onValueChange: setCurrentSearchValue, placeholder: searchPlaceholder, listboxId: hasRows ? listboxId : undefined, activeOptionId: hasRows ? activeOptionId : undefined, onKeyDown: handleSearchKeyDown })), !hasOptionRows &&
557
+ (isLoading ? (_jsx(LoadingState, { children: loadingMessage })) : (_jsx(EmptyState, {}))), hasOptionRows &&
558
+ (virtualRows !== undefined ? (_jsx(VirtualizedListBox, { id: listboxId, ariaLabel: label, isMulti: isMulti, ownsIds: actionOwnsIds, rows: virtualRows.displayRows, rowsKey: optionsKey, flatToDisplayIndex: virtualRows.flatToDisplayIndex, scrollToFlatIndexRef: scrollToActiveRowRef, renderRow: renderOption })) : (_jsx(ListBox, { id: listboxId, ariaLabel: label, isMulti: isMulti, ownsIds: actionOwnsIds, children: optionItems.map((item) => isOptionGroup(item) ? (_jsx(Group, { label: item.label, children: item.options.map(renderOption) }, item.label)) : (renderOption(item))) }))), actionRows.length > 0 && (_jsxs("div", Object.assign({}, (hasOptionRows
559
+ ? { role: 'presentation' }
560
+ : {
561
+ 'aria-label': label,
562
+ 'aria-multiselectable': isMulti || undefined,
563
+ id: listboxId,
564
+ role: 'listbox',
565
+ }), { className: "ndl-combobox-actions", children: [_jsx(Divider, { className: "ndl-combobox-action-divider", htmlAttributes: { 'aria-hidden': true } }), actionRows.map((row, index) => (_jsxs(Fragment, { children: [index > 0 && getAction(row).hasDivider === true && (_jsx(Divider, { className: "ndl-combobox-action-divider", htmlAttributes: { 'aria-hidden': true } })), renderAction(row)] }, row.value)))] })))] })));
566
+ return (_jsxs("div", Object.assign({ ref: ref, style: style, className: classNames('ndl-combobox', {
567
+ 'ndl-clean': isClean,
568
+ 'ndl-combobox-custom-trigger': hasCustomTrigger,
569
+ 'ndl-combobox-field': !hasCustomTrigger,
570
+ 'ndl-disabled': isDisabled,
571
+ 'ndl-fluid': isFluid,
572
+ 'ndl-has-error': hasError,
573
+ 'ndl-large': size === 'large',
574
+ 'ndl-medium': size === 'medium',
575
+ 'ndl-small': size === 'small',
576
+ }, className) }, htmlAttributes, { children: [name !== undefined && (_jsx(ComboboxFormControl, { ref: internalTriggerRef, name: name, form: formId, autoComplete: autoComplete, options: formOptions, selectedOptions: selectedOptions, resetSelectedOptions: resetSelectedOptions, isMulti: isMulti, isDisabled: isDisabled, isRequired: isRequired, isSelectionControlled: value !== undefined, onValueChange: setSelectedValue, onInvalid: setNativeValidationMessage, onReset: resetFromForm })), renderTrigger === undefined ? (_jsx(Control, { triggerProps: builtInTriggerProps, state: triggerState, field: field, placeholder: placeholder, isClearable: isClearable, isClean: isClean, size: size, maxRows: isMulti ? maxRows : undefined, components: components, referenceProps: getReferenceProps(), referenceRef: refs.setReference })) : (renderTrigger(triggerProps, triggerState)), context.open && (_jsx(ConditionalWrap, { shouldWrap: isPanelPortaled, wrap: (wrapChildren) => (_jsx(FloatingPortal, { root: portalTarget, children: wrapChildren })), children: _jsx(FloatingFocusManager, { context: context, modal: false,
577
+ // A select-only combobox keeps DOM focus on the trigger and speaks
578
+ // through aria-activedescendant, so nothing inside is focused.
579
+ initialFocus: isSearchable ? searchInputRef : -1, returnFocus: internalTriggerRef, children: panel }) }))] })));
580
+ };
581
+ const getTriggerControlsId = ({ isPanelOpen, isDialogPanel, hasRows, panelId, listboxId, }) => {
582
+ if (!isPanelOpen) {
583
+ return undefined;
584
+ }
585
+ if (isDialogPanel) {
586
+ return panelId;
587
+ }
588
+ // An expanded combobox must always control something, so fall back to the
589
+ // panel when the list is empty and only the status message is rendered.
590
+ return hasRows ? listboxId : panelId;
591
+ };
592
+ const getSelectedOptions = (value) => {
593
+ if (Array.isArray(value)) {
594
+ return value;
595
+ }
596
+ return value === null ? [] : [value];
597
+ };
598
+ /**
599
+ * A native select mirrors the custom control for submission, autofill,
600
+ * constraint validation, and reset. It is hidden from assistive technology
601
+ * because the combobox trigger exposes the same field semantics.
602
+ *
603
+ * Above `FORM_OPTION_MIRROR_LIMIT` it receives only the selected and default
604
+ * options rather than the full list, so autofill has nothing to offer but
605
+ * everything else keeps working without thousands of mirrored options.
606
+ */
607
+ const ComboboxFormControl = ({ ref: triggerRef, name, form, autoComplete, options, selectedOptions, resetSelectedOptions, isMulti, isDisabled, isRequired, isSelectionControlled, onValueChange, onInvalid, onReset, }) => {
608
+ var _a;
609
+ const selectRef = useRef(null);
610
+ const isDispatchingChangeRef = useRef(false);
611
+ const isResettingRef = useRef(false);
612
+ const skipNextChangeRef = useRef(false);
613
+ const selectedValues = selectedOptions.map((option) => option.value);
614
+ const resetSelectedValues = resetSelectedOptions.map((option) => option.value);
615
+ const selectedValuesKey = selectedValues.join('\u0000');
616
+ const previousValuesKeyRef = useRef(selectedValuesKey);
617
+ const valuesOnReset = isSelectionControlled
618
+ ? selectedValues
619
+ : resetSelectedValues;
620
+ const valuesOnResetKey = valuesOnReset.join('\u0000');
621
+ useEffect(() => {
622
+ const select = selectRef.current;
623
+ if (select === null) {
624
+ return;
625
+ }
626
+ const resetValues = new Set(valuesOnReset);
627
+ Array.from(select.options).forEach((option) => {
628
+ option.defaultSelected = resetValues.has(option.value);
629
+ });
630
+ // valuesOnReset is rebuilt every render, so the effect keys on its content.
631
+ // eslint-disable-next-line react-hooks/exhaustive-deps
632
+ }, [valuesOnResetKey]);
633
+ useEffect(() => {
634
+ if (previousValuesKeyRef.current === selectedValuesKey) {
635
+ return;
636
+ }
637
+ previousValuesKeyRef.current = selectedValuesKey;
638
+ if (isResettingRef.current || skipNextChangeRef.current) {
639
+ skipNextChangeRef.current = false;
640
+ return;
641
+ }
642
+ const select = selectRef.current;
643
+ if (select === null) {
644
+ return;
645
+ }
646
+ isDispatchingChangeRef.current = true;
647
+ select.dispatchEvent(new Event('change', { bubbles: true }));
648
+ isDispatchingChangeRef.current = false;
649
+ }, [selectedValuesKey]);
650
+ useEffect(() => {
651
+ const select = selectRef.current;
652
+ const formElement = select === null || select === void 0 ? void 0 : select.form;
653
+ if (select === null ||
654
+ select === undefined ||
655
+ formElement === null ||
656
+ formElement === undefined) {
657
+ return;
658
+ }
659
+ const handleReset = () => {
660
+ isResettingRef.current = true;
661
+ onReset();
662
+ window.setTimeout(() => {
663
+ isResettingRef.current = false;
664
+ });
665
+ };
666
+ formElement.addEventListener('reset', handleReset);
667
+ return () => formElement.removeEventListener('reset', handleReset);
668
+ }, [onReset]);
669
+ const handleChange = (event) => {
670
+ var _a;
671
+ if (isDispatchingChangeRef.current) {
672
+ return;
673
+ }
674
+ const optionsByValue = new Map(options.map((option) => [option.value, option]));
675
+ const nextOptions = Array.from(event.currentTarget.selectedOptions)
676
+ .map((selectedOption) => optionsByValue.get(selectedOption.value))
677
+ .filter((option) => option !== undefined);
678
+ skipNextChangeRef.current =
679
+ nextOptions.map((option) => option.value).join('\u0000') !==
680
+ selectedValuesKey;
681
+ onValueChange(isMulti ? nextOptions : ((_a = nextOptions[0]) !== null && _a !== void 0 ? _a : null));
682
+ };
683
+ return (_jsxs("select", { ref: selectRef, className: "ndl-combobox-form-control", "aria-hidden": "true", tabIndex: -1, name: name, form: form, autoComplete: autoComplete, disabled: isDisabled, required: isRequired, multiple: isMulti, value: isMulti ? selectedValues : ((_a = selectedValues[0]) !== null && _a !== void 0 ? _a : ''), onChange: handleChange, onInvalid: (event) => {
684
+ var _a;
685
+ event.preventDefault();
686
+ onInvalid(event.currentTarget.validationMessage);
687
+ (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
688
+ }, children: [!isMulti && _jsx("option", { value: "" }), options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] }));
689
+ };
690
+ const Control = ({ triggerProps, state, field, placeholder, isClearable, isClean, size, maxRows, components, referenceProps, referenceRef, }) => {
691
+ const { isOpen, isMulti, isDisabled, selectedOptions, clear, remove } = state;
692
+ const { MultiValue, SingleValue } = components;
693
+ const { ref: triggerRef } = triggerProps, triggerAria = __rest(triggerProps, ["ref"]);
694
+ const hasValue = selectedOptions.length > 0;
695
+ const singleValueOption = isMulti ? undefined : selectedOptions[0];
696
+ // Selected values are shown as tags, so the trigger renders nothing and is
697
+ // taken out of the wrapping flow rather than claiming a row of its own.
698
+ const isTriggerCollapsed = isMulti && hasValue;
699
+ const multiValueSize = size === 'large' ? 'small' : 'x-small';
700
+ // `null` opts out of the default row limit, so both it and a missing value
701
+ // resolve to unlimited, which the hook expresses as undefined.
702
+ const rowLimit = isMulti && maxRows !== null && maxRows !== undefined ? maxRows : undefined;
703
+ // Under a row limit only the tags that could possibly be visible are
704
+ // rendered and measured; the rest of a huge selection is overflowed
705
+ // arithmetically without ever touching the DOM. Without a limit every tag
706
+ // renders, so unlimited rows with a huge selection is on the consumer.
707
+ const tagCandidateCap = rowLimit === undefined
708
+ ? selectedOptions.length
709
+ : Math.max(1, Math.floor(rowLimit)) * TAG_CANDIDATES_PER_ROW;
710
+ const renderedOptions = selectedOptions.length > tagCandidateCap
711
+ ? selectedOptions.slice(0, tagCandidateCap)
712
+ : selectedOptions;
713
+ const { containerRef: valueContainerRef, visibleCount } = useComboboxMultiValueOverflow({
714
+ maxRows: rowLimit,
715
+ // Disabling swaps the dismissible tags for read-only ones, which have no
716
+ // remove button and are narrower. The total count is included since it
717
+ // sets the measure tag texts.
718
+ measureKey: [
719
+ size,
720
+ isDisabled,
721
+ selectedOptions.length,
722
+ ...renderedOptions.map((option) => `${option.value}:${option.label}`),
723
+ ].join('\u0000'),
724
+ tagCount: renderedOptions.length,
725
+ });
726
+ const visibleTagCount = visibleCount !== null && visibleCount !== void 0 ? visibleCount : renderedOptions.length;
727
+ const overflowCount = selectedOptions.length - visibleTagCount;
728
+ const overflowedOptions = selectedOptions.slice(visibleTagCount, visibleTagCount + OVERFLOW_LABEL_LIMIT);
729
+ const unnamedOverflowCount = overflowCount - overflowedOptions.length;
730
+ const overflowedLabels = overflowedOptions.map((option) => option.label).join(', ') +
731
+ (unnamedOverflowCount > 0 ? `, and ${unnamedOverflowCount} more` : '');
732
+ // "+999" is wider than "+1K", so each text the counter could show at its
733
+ // widest is measured; the hook reserves room for the widest of them.
734
+ const counterMeasureTexts = getOverflowCounterMeasureTexts(selectedOptions.length);
735
+ const internalTriggerRef = useRef(null);
736
+ const mergedTriggerRef = useMergeRefs([
737
+ triggerRef,
738
+ internalTriggerRef,
739
+ ]);
740
+ const handleAuxiliaryActionKeyDown = (event) => {
741
+ var _a, _b;
742
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
743
+ event.preventDefault();
744
+ event.stopPropagation();
745
+ (_a = internalTriggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
746
+ (_b = triggerAria.onKeyDown) === null || _b === void 0 ? void 0 : _b.call(triggerAria, event);
747
+ }
748
+ };
749
+ const handleTagRemove = (event, option) => {
750
+ var _a;
751
+ // Capture the following removal targets before React removes the current
752
+ // tag. With `maxRows`, the removal can promote a collapsed tag into view,
753
+ // so which of them is focusable is only known after the update.
754
+ const tag = event.currentTarget.closest('[data-ndl-combobox-multi-value]');
755
+ const followingRemoveButtons = [];
756
+ let sibling = (_a = tag === null || tag === void 0 ? void 0 : tag.nextElementSibling) !== null && _a !== void 0 ? _a : null;
757
+ while (sibling !== null) {
758
+ const removeButton = sibling.querySelector('[data-ndl-dismissible-tag-remove]');
759
+ if (removeButton !== null) {
760
+ followingRemoveButtons.push(removeButton);
761
+ }
762
+ sibling = sibling.nextElementSibling;
763
+ }
764
+ // Otherwise the click bubbles to the control and opens the panel.
765
+ event.stopPropagation();
766
+ remove(option);
767
+ requestAnimationFrame(() => {
768
+ var _a;
769
+ // A tag collapsed into the overflow counter is invisible and cannot take
770
+ // focus, so it is passed over.
771
+ const nextRemoveButton = followingRemoveButtons.find((removeButton) => removeButton.isConnected &&
772
+ removeButton.closest('.ndl-overflowed') === null);
773
+ if (nextRemoveButton !== undefined) {
774
+ nextRemoveButton.focus();
775
+ return;
776
+ }
777
+ (_a = internalTriggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
778
+ });
779
+ };
780
+ const handleClear = (event) => {
781
+ // Otherwise the click bubbles to the control and opens the panel.
782
+ event.stopPropagation();
783
+ clear();
784
+ // Clearing removes the focused clear button from the DOM. Restore focus to
785
+ // the combobox after React has committed that update.
786
+ requestAnimationFrame(() => {
787
+ var _a;
788
+ (_a = internalTriggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
789
+ });
790
+ };
791
+ // Clicking the label moves focus to the trigger without opening the panel,
792
+ // as with a native select. Preventing the default cancels the click the
793
+ // label would otherwise forward to the trigger button, which would toggle
794
+ // the panel.
795
+ const handleLabelClick = (event) => {
796
+ var _a;
797
+ event.preventDefault();
798
+ (_a = internalTriggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
799
+ };
800
+ return (
801
+ // A fragment, so the label, the control, and the message are rows of the
802
+ // combobox itself. A custom trigger replaces all three.
803
+ _jsxs(_Fragment, { children: [_jsx(FieldLabel, { field: field, size: size, onLabelClick: handleLabelClick }), _jsxs("div", Object.assign({ ref: referenceRef, className: classNames('ndl-combobox-control', {
804
+ 'ndl-clean': isClean,
805
+ 'ndl-disabled': isDisabled,
806
+ 'ndl-has-error': field.hasError,
807
+ 'ndl-is-multi': isMulti,
808
+ 'ndl-large': size === 'large',
809
+ 'ndl-medium': size === 'medium',
810
+ 'ndl-small': size === 'small',
811
+ }) }, referenceProps, { children: [_jsxs("div", { ref: valueContainerRef, className: classNames('ndl-combobox-value-container', {
812
+ // While a counter may be needed, every tag reserves room for it,
813
+ // so an extreme-width tag shrinks instead of collapsing entirely.
814
+ 'ndl-counter-reserved': rowLimit !== undefined && selectedOptions.length > 1,
815
+ }), children: [_jsxs("button", Object.assign({ type: "button", className: classNames('ndl-combobox-trigger', {
816
+ 'ndl-collapsed': isTriggerCollapsed,
817
+ 'ndl-disabled': isDisabled,
818
+ }) }, triggerAria, { ref: mergedTriggerRef, children: [singleValueOption !== undefined &&
819
+ (SingleValue === undefined ? (_jsx("span", { className: "ndl-combobox-single-value", children: singleValueOption.label })) : (_jsx(SingleValue, { option: singleValueOption, isDisabled: isDisabled, size: size }))), !hasValue && (_jsx("span", { className: "ndl-combobox-placeholder", children: placeholder }))] })), isMulti &&
820
+ renderedOptions.map((option, index) => {
821
+ // Tags past the overflow cutoff stay rendered so they keep their
822
+ // measurements, but the stylesheet takes them out of the layout,
823
+ // out of the accessibility tree, and out of focus reach.
824
+ const multiValueClasses = classNames('ndl-combobox-multi-value', {
825
+ 'ndl-overflowed': index >= visibleTagCount,
826
+ });
827
+ // A custom value owns its own presentation, so it is only given
828
+ // the wrapper the value container lays out, and no remove button.
829
+ if (MultiValue !== undefined) {
830
+ return (_jsx("span", { className: multiValueClasses, children: _jsx(MultiValue, { option: option, index: index, isDisabled: isDisabled, size: size, remove: () => remove(option) }) }, option.value));
831
+ }
832
+ return isDisabled ? (_jsx(ReadOnlyTag, { className: multiValueClasses, size: multiValueSize, children: option.label }, option.value)) : (_jsx(DismissibleTag, { className: multiValueClasses, size: multiValueSize, htmlAttributes: {
833
+ 'data-ndl-combobox-multi-value': '',
834
+ onKeyDown: handleAuxiliaryActionKeyDown,
835
+ }, onRemove: (event) => handleTagRemove(event, option), children: option.label }, option.value));
836
+ }), overflowCount > 0 && (_jsxs(ReadOnlyTag, { className: "ndl-combobox-multi-value-counter", size: multiValueSize, htmlAttributes: {
837
+ title: overflowedLabels,
838
+ }, children: [_jsxs("span", { "aria-hidden": "true", children: ["+", formatOverflowCount(overflowCount)] }), _jsxs("span", { className: "ndl-combobox-visually-hidden", children: [overflowCount, " more selected: ", overflowedLabels] })] })), rowLimit !== undefined &&
839
+ hasValue &&
840
+ // Invisible and out of the layout, purely to measure the widest
841
+ // counter the current selection could need, so the fit computation
842
+ // can reserve room for it in a single deterministic pass.
843
+ counterMeasureTexts.map((text) => (_jsxs(ReadOnlyTag, { className: "ndl-combobox-multi-value-measure", size: multiValueSize, htmlAttributes: { 'aria-hidden': true }, children: ["+", text] }, text)))] }), _jsxs("div", { className: "ndl-combobox-indicators-container", children: [isClearable && hasValue && !isDisabled && (
844
+ // The divider only separates the clear button from the chevron, so
845
+ // it is tied to the button rather than always rendered, as in
846
+ // Select.
847
+ _jsxs("div", { className: "ndl-combobox-clear", children: [_jsx("button", { type: "button", className: "ndl-combobox-clear-button", "aria-label": "Clear selection", onKeyDown: handleAuxiliaryActionKeyDown, onClick: handleClear, children: _jsx(XMarkIconOutline, {}) }), _jsx("div", { className: "ndl-combobox-divider" })] })), _jsx("div", { className: classNames('ndl-combobox-dropdown-indicator', {
848
+ 'ndl-open': isOpen,
849
+ }), children: _jsx(ChevronDownIconOutline, { "aria-hidden": "true", className: "ndl-combobox-icon" }) })] })] })), _jsx(FieldMessage, { field: field, size: size })] }));
850
+ };
851
+ /**
852
+ * The trigger is a button with `role="combobox"`, which takes its accessible
853
+ * name from the author rather than from its content, so the label is referenced
854
+ * through `aria-labelledby` instead of wrapping the control in a `<label>`.
855
+ * The label is still a `<label>` associated through `htmlFor`, and clicking it
856
+ * moves focus to the trigger, as with a native select. It deliberately does
857
+ * not open the panel, which is also what a native select label leaves alone.
858
+ */
859
+ const FieldLabel = ({ field, size, onLabelClick }) => {
860
+ const { label, labelId, triggerId, hasVisuallyHiddenLabel, hasRequiredIndicator, isRequired, } = field;
861
+ const variant = size === 'large' ? 'body-large' : 'body-medium';
862
+ return (_jsxs("div", { className: classNames('ndl-combobox-label-row', {
863
+ 'ndl-visually-hidden': hasVisuallyHiddenLabel,
864
+ }), children: [_jsx(Typography, { as: "label", variant: variant, className: "ndl-combobox-label", htmlAttributes: {
865
+ htmlFor: triggerId,
866
+ id: labelId,
867
+ onClick: onLabelClick,
868
+ }, children: label }), hasRequiredIndicator && (_jsx(Typography, { variant: variant, className: "ndl-combobox-label-indicator", children: isRequired ? 'Required' : 'Optional' }))] }));
869
+ };
870
+ /**
871
+ * Rendered whether or not there is a message, so the live region is in the
872
+ * document before the text arrives and a validation error is announced.
873
+ */
874
+ const FieldMessage = ({ field, size }) => {
875
+ const { errorText, hasError, helpText, helpTextId, errorTextId } = field;
876
+ const variant = size === 'large' ? 'body-medium' : 'body-small';
877
+ return (_jsxs("div", { className: "ndl-combobox-message", "aria-live": "polite", children: [hasError && (_jsxs(_Fragment, { children: [_jsx("div", { className: "ndl-error-icon", children: _jsx(ExclamationCircleIconSolid, {}) }), _jsx(Typography, { variant: variant, className: "ndl-error-text", htmlAttributes: { id: errorTextId }, children: errorText })] })), !hasError && Boolean(helpText) && (_jsx(Typography, { variant: variant, htmlAttributes: { id: helpTextId }, children: helpText }))] }));
878
+ };
879
+ const Panel = (_a) => {
880
+ var { children, ref, id, ariaLabel, isDialog, className, style } = _a, restProps = __rest(_a, ["children", "ref", "id", "ariaLabel", "isDialog", "className", "style"]);
881
+ return (_jsx("div", Object.assign({ ref: ref, id: id, role: isDialog === true ? 'dialog' : undefined, "aria-label": isDialog === true ? ariaLabel : undefined, className: classNames('ndl-combobox-panel', className), style: style }, restProps, { children: children })));
882
+ };
883
+ const Input = ({ ref, ariaLabel, value, onValueChange, placeholder, listboxId, activeOptionId, onKeyDown, }) => {
884
+ return (_jsxs("label", { className: "ndl-combobox-input-wrapper", children: [_jsx("span", { className: "ndl-combobox-input-label", children: ariaLabel }), _jsx(MagnifyingGlassIconOutline, { className: "ndl-combobox-input-leading" }), _jsx("input", { ref: ref, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-expanded": listboxId !== undefined, "aria-controls": listboxId, "aria-activedescendant": activeOptionId, className: "ndl-combobox-input", placeholder: placeholder, value: value, onChange: (event) => onValueChange(event.target.value), onKeyDown: onKeyDown })] }));
885
+ };
886
+ const ListBox = ({ ariaLabel, children, isMulti, id, ownsIds, }) => {
887
+ return (_jsx("div", { id: id, role: "listbox", "aria-label": ariaLabel, "aria-owns": ownsIds, "aria-multiselectable": isMulti || undefined, className: "ndl-combobox-listbox", children: children }));
888
+ };
889
+ /**
890
+ * The windowed listbox used above the row threshold: only the visible slice of
891
+ * rows is in the DOM, absolutely positioned inside one full-height spacer.
892
+ * Group nesting cannot survive windowing, so headers render as presentational
893
+ * rows of their own and options carry `aria-setsize`/`aria-posinset` instead.
894
+ * Pinned action rows render outside, below the listbox, so they are never
895
+ * windowed.
896
+ */
897
+ const VirtualizedListBox = ({ id, ariaLabel, isMulti, ownsIds, rows, rowsKey, flatToDisplayIndex, scrollToFlatIndexRef, renderRow, }) => {
898
+ const scrollRef = useRef(null);
899
+ const virtualizer = useVirtualizer({
900
+ count: rows.length,
901
+ estimateSize: () => VIRTUAL_ROW_ESTIMATED_HEIGHT,
902
+ getScrollElement: () => scrollRef.current,
903
+ overscan: 10,
904
+ });
905
+ const pendingScrollIndexRef = useRef(null);
906
+ useEffect(() => {
907
+ scrollToFlatIndexRef.current = (flatIndex) => {
908
+ const displayIndex = flatToDisplayIndex[flatIndex];
909
+ if (displayIndex !== undefined) {
910
+ pendingScrollIndexRef.current = displayIndex;
911
+ virtualizer.scrollToIndex(displayIndex);
912
+ }
913
+ };
914
+ return () => {
915
+ scrollToFlatIndexRef.current = null;
916
+ };
917
+ });
918
+ // A long jump scrolls to an offset built from estimated sizes, and the
919
+ // estimates only get corrected once the rows near the target render, so one
920
+ // scroll can land short. Each render after a scroll re-issues it until the
921
+ // target row is actually mounted, which is also what lets
922
+ // aria-activedescendant resolve.
923
+ useEffect(() => {
924
+ const target = pendingScrollIndexRef.current;
925
+ if (target === null) {
926
+ return;
927
+ }
928
+ const isRendered = virtualizer
929
+ .getVirtualItems()
930
+ .some((item) => item.index === target);
931
+ if (isRendered) {
932
+ pendingScrollIndexRef.current = null;
933
+ return;
934
+ }
935
+ virtualizer.scrollToIndex(target);
936
+ });
937
+ // A filtered list is a different list, so it starts at the top.
938
+ useEffect(() => {
939
+ virtualizer.scrollToOffset(0);
940
+ // The virtualizer is stable and scrolling must not rerun on its rerenders.
941
+ // eslint-disable-next-line react-hooks/exhaustive-deps
942
+ }, [rowsKey]);
943
+ return (_jsx("div", { ref: scrollRef, id: id, role: "listbox", "aria-label": ariaLabel, "aria-owns": ownsIds, "aria-multiselectable": isMulti === true || undefined, className: "ndl-combobox-listbox", children: _jsx("div", { role: "presentation", className: "ndl-combobox-virtual-space", style: { height: virtualizer.getTotalSize() }, children: virtualizer.getVirtualItems().map((item) => {
944
+ const row = rows[item.index];
945
+ return (_jsx("div", { ref: virtualizer.measureElement, "data-index": item.index, role: "presentation", className: "ndl-combobox-virtual-row", style: { transform: `translateY(${item.start}px)` }, children: row.kind === 'group-header' ? (_jsx(GroupHeader, { isPresentational: true, label: row.label })) : (renderRow(row.row)) }, item.key));
946
+ }) }) }));
947
+ };
948
+ const Option = ({ children, id, isSelected, isDisabled, isFocused, isCreate, isAction, onClick, onHover, posInSet, setSize, }) => {
949
+ const classes = classNames('ndl-combobox-option', {
950
+ 'ndl-combobox-option-action': isAction,
951
+ 'ndl-combobox-option-create': isCreate,
952
+ 'ndl-disabled': isDisabled,
953
+ 'ndl-focused': isFocused,
954
+ });
955
+ return (
956
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus -- options are never DOM focused; keyboard is handled on the search input or the listbox and exposed through aria-activedescendant
957
+ _jsx("div", { id: id, role: "option", "aria-selected": isSelected, "aria-disabled": isDisabled, "aria-posinset": posInSet, "aria-setsize": setSize, className: classes, onClick: onClick,
958
+ // Options are not focusable, so a plain mousedown would blur whatever
959
+ // holds DOM focus - the search input or the trigger - and drop the panel.
960
+ onMouseDown: (event) => event.preventDefault(),
961
+ // mouseover fires when the element under a stationary cursor changes
962
+ // because the list scrolled, which would let a rested mouse steal virtual
963
+ // focus back during keyboard navigation. mousemove needs real movement.
964
+ onMouseMove: onHover, children: children }));
965
+ };
966
+ const OptionContent = ({ label, customLabel, description, leadingVisual, isDisabled, isSelected, isMulti, }) => {
967
+ return (_jsxs("div", { className: "ndl-combobox-option-content", children: [isMulti ? (_jsx(MultiSelectIndicator, { isDisabled: isDisabled, isSelected: isSelected })) : (leadingVisual && (_jsx(LeadingVisual, { isDisabled: isDisabled, children: leadingVisual }))), _jsxs("div", { className: "ndl-combobox-option-content-main", children: [customLabel !== null && customLabel !== void 0 ? customLabel : _jsx(Label, { isDisabled: isDisabled, children: label }), description && (_jsx(Description, { isDisabled: isDisabled, children: description }))] }), !isMulti && isSelected && _jsx(SelectionIndicator, { isDisabled: isDisabled })] }));
968
+ };
969
+ const CreateOptionContent = ({ children }) => {
970
+ return (_jsxs("div", { className: "ndl-combobox-option-content", children: [_jsx(LeadingVisual, { children: _jsx(PlusIconOutline, {}) }), _jsx("div", { className: "ndl-combobox-option-content-main", children: _jsx(Label, { children: children }) })] }));
971
+ };
972
+ const LeadingVisual = ({ children, isDisabled }) => {
973
+ const classes = classNames('ndl-combobox-option-content-leading-visual', {
974
+ 'ndl-disabled': isDisabled,
975
+ });
976
+ return _jsx("div", { className: classes, children: children });
977
+ };
978
+ const Label = ({ children, isDisabled }) => {
979
+ const classes = classNames('ndl-combobox-option-content-label', {
980
+ 'ndl-disabled': isDisabled,
981
+ });
982
+ return (_jsx(Typography, { variant: "body-medium", className: classes, children: children }));
983
+ };
984
+ const Description = ({ children, isDisabled }) => {
985
+ const classes = classNames('ndl-combobox-option-content-description', {
986
+ 'ndl-disabled': isDisabled,
987
+ });
988
+ return (_jsx(Typography, { variant: "body-small", className: classes, children: children }));
989
+ };
990
+ const SelectionIndicator = ({ isDisabled, }) => {
991
+ const classes = classNames('ndl-combobox-option-content-selection-indicator', {
992
+ 'ndl-disabled': isDisabled,
993
+ });
994
+ return _jsx(CheckIconOutline, { className: classes });
995
+ };
996
+ /**
997
+ * The option itself owns selection through `aria-selected` and its click
998
+ * handler, so the checkbox is decorative. `inert` keeps it out of the
999
+ * accessibility tree and out of hit testing - without it the wrapping label
1000
+ * forwards a second click to the input, which bubbles back to the option and
1001
+ * undoes the toggle.
1002
+ */
1003
+ const MultiSelectIndicator = ({ isDisabled, isSelected, }) => {
1004
+ return (_jsx("span", { className: "ndl-combobox-option-content-multi-select-indicator", inert: true, children: _jsx(Checkbox, { ariaLabel: "Select", isChecked: isSelected, isDisabled: isDisabled }) }));
1005
+ };
1006
+ /**
1007
+ * The visible group header text, shared by both listbox paths: nested inside a
1008
+ * `role="group"` wrapper below the row threshold, or as a presentational row of
1009
+ * its own while windowed.
1010
+ */
1011
+ const GroupHeader = ({ label, id, isPresentational, }) => {
1012
+ return (_jsx("div", { id: id, role: isPresentational === true ? 'presentation' : undefined, className: "ndl-combobox-group-header", children: label }));
1013
+ };
1014
+ const Group = ({ children, label }) => {
1015
+ const headerId = useId();
1016
+ return (_jsxs("div", { role: "group", "aria-labelledby": headerId, className: "ndl-combobox-group", children: [_jsx(GroupHeader, { id: headerId, label: label }), children] }));
1017
+ };
1018
+ const EmptyState = ({ children }) => {
1019
+ return (_jsx(Typography, { variant: "body-medium", className: "ndl-combobox-empty-state", htmlAttributes: { role: 'status' }, children: children !== null && children !== void 0 ? children : 'No results found' }));
1020
+ };
1021
+ const LoadingState = ({ children }) => {
1022
+ return (_jsxs("div", { role: "status", className: "ndl-combobox-empty-state ndl-combobox-loading-state", children: [_jsx(LoadingSpinner, { size: "small", htmlAttributes: { 'aria-hidden': true } }), _jsx(Typography, { variant: "body-medium", children: children !== null && children !== void 0 ? children : DEFAULT_LOADING_MESSAGE })] }));
1023
+ };
1024
+ //# sourceMappingURL=Combobox.js.map