@dashforge/tw 1.1.0 → 1.1.1

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 (390) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/index.esm.js +1 -1
  3. package/dist/src/index.d.ts +1 -1
  4. package/package.json +8 -3
  5. package/.swcrc +0 -29
  6. package/A11Y.md +0 -130
  7. package/CONSUMER-VALIDATION.md +0 -130
  8. package/COVERAGE.md +0 -63
  9. package/PARITY.md +0 -481
  10. package/PERFORMANCE.md +0 -227
  11. package/REFINEMENT-NOTES.md +0 -50
  12. package/THEME-AUDIT.md +0 -174
  13. package/dashforge-tw-1.0.0.tgz +0 -0
  14. package/eslint.config.mjs +0 -62
  15. package/project.json +0 -15
  16. package/rollup.config.cjs +0 -16
  17. package/src/components/Accordion/Accordion.test.tsx +0 -95
  18. package/src/components/Accordion/Accordion.tsx +0 -97
  19. package/src/components/Accordion/accordion.types.ts +0 -66
  20. package/src/components/Accordion/accordion.variants.ts +0 -32
  21. package/src/components/Alert/Alert.test.tsx +0 -302
  22. package/src/components/Alert/Alert.tsx +0 -186
  23. package/src/components/Alert/alert.types.ts +0 -144
  24. package/src/components/Alert/alert.variants.ts +0 -71
  25. package/src/components/AppShell/AppShell.test.tsx +0 -223
  26. package/src/components/AppShell/AppShell.tsx +0 -239
  27. package/src/components/AppShell/appShell.types.ts +0 -49
  28. package/src/components/AppShell/appShell.variants.ts +0 -54
  29. package/src/components/AspectRatio/AspectRatio.test.tsx +0 -173
  30. package/src/components/AspectRatio/AspectRatio.tsx +0 -73
  31. package/src/components/AspectRatio/aspectRatio.types.ts +0 -54
  32. package/src/components/Autocomplete/Autocomplete.perf.test.tsx +0 -225
  33. package/src/components/Autocomplete/Autocomplete.test.tsx +0 -556
  34. package/src/components/Autocomplete/Autocomplete.tsx +0 -1069
  35. package/src/components/Autocomplete/autocomplete.types.ts +0 -198
  36. package/src/components/Autocomplete/autocomplete.variants.ts +0 -141
  37. package/src/components/Avatar/Avatar.test.tsx +0 -287
  38. package/src/components/Avatar/Avatar.tsx +0 -304
  39. package/src/components/Avatar/avatar.types.ts +0 -205
  40. package/src/components/Avatar/avatar.variants.ts +0 -194
  41. package/src/components/Badge/Badge.test.tsx +0 -385
  42. package/src/components/Badge/Badge.tsx +0 -174
  43. package/src/components/Badge/badge.types.ts +0 -154
  44. package/src/components/Badge/badge.variants.ts +0 -161
  45. package/src/components/Box/Box.test.tsx +0 -391
  46. package/src/components/Box/Box.tsx +0 -112
  47. package/src/components/Box/box.types.ts +0 -116
  48. package/src/components/Box/box.variants.ts +0 -230
  49. package/src/components/Breadcrumbs/Breadcrumbs.perf.test.tsx +0 -128
  50. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +0 -231
  51. package/src/components/Breadcrumbs/Breadcrumbs.tsx +0 -203
  52. package/src/components/Breadcrumbs/breadcrumbs.types.ts +0 -87
  53. package/src/components/Breadcrumbs/breadcrumbs.variants.ts +0 -54
  54. package/src/components/Button/Button.test.tsx +0 -204
  55. package/src/components/Button/Button.tsx +0 -151
  56. package/src/components/Button/button.types.ts +0 -110
  57. package/src/components/Button/button.variants.ts +0 -159
  58. package/src/components/Calendar/Calendar.test.tsx +0 -75
  59. package/src/components/Calendar/Calendar.tsx +0 -264
  60. package/src/components/Calendar/calendar.types.ts +0 -65
  61. package/src/components/Calendar/calendar.variants.ts +0 -64
  62. package/src/components/Card/Card.test.tsx +0 -291
  63. package/src/components/Card/Card.tsx +0 -241
  64. package/src/components/Card/card.types.ts +0 -132
  65. package/src/components/Checkbox/Checkbox.test.tsx +0 -146
  66. package/src/components/Checkbox/Checkbox.tsx +0 -286
  67. package/src/components/Checkbox/checkbox.types.ts +0 -88
  68. package/src/components/Checkbox/checkbox.variants.ts +0 -68
  69. package/src/components/Chip/Chip.test.tsx +0 -316
  70. package/src/components/Chip/Chip.tsx +0 -224
  71. package/src/components/Chip/chip.types.ts +0 -128
  72. package/src/components/Chip/chip.variants.ts +0 -173
  73. package/src/components/ConfirmDialog/ConfirmDialog.perf.test.tsx +0 -172
  74. package/src/components/ConfirmDialog/ConfirmDialog.test.tsx +0 -311
  75. package/src/components/ConfirmDialog/ConfirmDialog.tsx +0 -252
  76. package/src/components/ConfirmDialog/confirmDialog.types.ts +0 -70
  77. package/src/components/ConfirmDialog/confirmDialog.variants.ts +0 -76
  78. package/src/components/Container/Container.test.tsx +0 -209
  79. package/src/components/Container/Container.tsx +0 -74
  80. package/src/components/Container/container.types.ts +0 -45
  81. package/src/components/Container/container.variants.ts +0 -81
  82. package/src/components/DataGrid/DataGrid.perf.test.tsx +0 -114
  83. package/src/components/DataGrid/DataGrid.test.tsx +0 -1153
  84. package/src/components/DataGrid/DataGrid.tsx +0 -1282
  85. package/src/components/DataGrid/dataGrid.types.ts +0 -238
  86. package/src/components/DataGrid/dataGrid.variants.ts +0 -152
  87. package/src/components/DataGrid/filters/ColumnFilters.test.tsx +0 -330
  88. package/src/components/DataGrid/filters/ColumnFilters.tsx +0 -456
  89. package/src/components/DataGrid/reorder/useColumnReorder.test.ts +0 -130
  90. package/src/components/DataGrid/reorder/useColumnReorder.ts +0 -144
  91. package/src/components/DataGrid/resize/useColumnResize.test.ts +0 -144
  92. package/src/components/DataGrid/resize/useColumnResize.ts +0 -135
  93. package/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx +0 -186
  94. package/src/components/DatePicker/DatePicker.test.tsx +0 -76
  95. package/src/components/DatePicker/DatePicker.tsx +0 -305
  96. package/src/components/DatePicker/datePicker.types.ts +0 -77
  97. package/src/components/DatePicker/datePicker.variants.ts +0 -50
  98. package/src/components/DateRangePicker/DateRangePicker.test.tsx +0 -92
  99. package/src/components/DateRangePicker/DateRangePicker.tsx +0 -510
  100. package/src/components/DateRangePicker/dateRangePicker.types.ts +0 -74
  101. package/src/components/DateRangePicker/dateRangePicker.variants.ts +0 -101
  102. package/src/components/DateTimePicker/DateTimePicker.test.tsx +0 -78
  103. package/src/components/DateTimePicker/DateTimePicker.tsx +0 -347
  104. package/src/components/DateTimePicker/dateTimePicker.types.ts +0 -79
  105. package/src/components/DateTimePicker/dateTimePicker.variants.ts +0 -55
  106. package/src/components/Dialog/Dialog.test.tsx +0 -96
  107. package/src/components/Dialog/Dialog.tsx +0 -102
  108. package/src/components/Dialog/dialog.types.ts +0 -62
  109. package/src/components/Dialog/dialog.variants.ts +0 -66
  110. package/src/components/Divider/Divider.test.tsx +0 -243
  111. package/src/components/Divider/Divider.tsx +0 -140
  112. package/src/components/Divider/divider.types.ts +0 -52
  113. package/src/components/Divider/divider.variants.ts +0 -116
  114. package/src/components/Grid/Grid.test.tsx +0 -321
  115. package/src/components/Grid/Grid.tsx +0 -148
  116. package/src/components/Grid/grid.types.ts +0 -112
  117. package/src/components/Grid/grid.variants.ts +0 -133
  118. package/src/components/IconButton/IconButton.test.tsx +0 -367
  119. package/src/components/IconButton/IconButton.tsx +0 -159
  120. package/src/components/IconButton/iconButton.types.ts +0 -106
  121. package/src/components/IconButton/iconButton.variants.ts +0 -31
  122. package/src/components/LeftNav/LeftNav.perf.test.tsx +0 -172
  123. package/src/components/LeftNav/LeftNav.test.tsx +0 -302
  124. package/src/components/LeftNav/LeftNav.tsx +0 -330
  125. package/src/components/LeftNav/leftNav.types.ts +0 -120
  126. package/src/components/LeftNav/leftNav.variants.ts +0 -107
  127. package/src/components/Menu/Menu.test.tsx +0 -408
  128. package/src/components/Menu/Menu.tsx +0 -335
  129. package/src/components/Menu/menu.types.ts +0 -221
  130. package/src/components/Menu/menu.variants.ts +0 -132
  131. package/src/components/NumberField/NumberField.test.tsx +0 -83
  132. package/src/components/NumberField/NumberField.tsx +0 -321
  133. package/src/components/NumberField/numberField.types.ts +0 -72
  134. package/src/components/NumberField/numberField.variants.ts +0 -94
  135. package/src/components/OTPField/OTPField.test.tsx +0 -121
  136. package/src/components/OTPField/OTPField.tsx +0 -250
  137. package/src/components/OTPField/otpField.types.ts +0 -58
  138. package/src/components/OTPField/otpField.variants.ts +0 -72
  139. package/src/components/Pagination/Pagination.test.tsx +0 -314
  140. package/src/components/Pagination/Pagination.tsx +0 -277
  141. package/src/components/Pagination/pagination.helpers.ts +0 -102
  142. package/src/components/Pagination/pagination.types.ts +0 -117
  143. package/src/components/Pagination/pagination.variants.ts +0 -117
  144. package/src/components/Popover/Popover.test.tsx +0 -72
  145. package/src/components/Popover/Popover.tsx +0 -55
  146. package/src/components/Popover/popover.types.ts +0 -42
  147. package/src/components/Popover/popover.variants.ts +0 -24
  148. package/src/components/RadioGroup/RadioGroup.test.tsx +0 -140
  149. package/src/components/RadioGroup/RadioGroup.tsx +0 -283
  150. package/src/components/RadioGroup/radioGroup.types.ts +0 -85
  151. package/src/components/RadioGroup/radioGroup.variants.ts +0 -87
  152. package/src/components/Skeleton/Skeleton.test.tsx +0 -127
  153. package/src/components/Skeleton/Skeleton.tsx +0 -49
  154. package/src/components/Skeleton/skeleton.types.ts +0 -59
  155. package/src/components/Skeleton/skeleton.variants.ts +0 -87
  156. package/src/components/Snackbar/Snackbar.perf.test.tsx +0 -144
  157. package/src/components/Snackbar/Snackbar.test.tsx +0 -327
  158. package/src/components/Snackbar/Snackbar.tsx +0 -316
  159. package/src/components/Snackbar/snackbar.types.ts +0 -127
  160. package/src/components/Snackbar/snackbar.variants.ts +0 -81
  161. package/src/components/Spinner/Spinner.test.tsx +0 -199
  162. package/src/components/Spinner/Spinner.tsx +0 -158
  163. package/src/components/Spinner/spinner.types.ts +0 -113
  164. package/src/components/Spinner/spinner.variants.ts +0 -83
  165. package/src/components/Stack/Stack.test.tsx +0 -309
  166. package/src/components/Stack/Stack.tsx +0 -115
  167. package/src/components/Stack/stack.types.ts +0 -68
  168. package/src/components/Stack/stack.variants.ts +0 -80
  169. package/src/components/Switch/Switch.test.tsx +0 -131
  170. package/src/components/Switch/Switch.tsx +0 -158
  171. package/src/components/Switch/switch.types.ts +0 -36
  172. package/src/components/Switch/switch.variants.ts +0 -70
  173. package/src/components/Table/Table.perf.test.tsx +0 -81
  174. package/src/components/Table/Table.test.tsx +0 -674
  175. package/src/components/Table/Table.tsx +0 -863
  176. package/src/components/Table/cells/RenderButton.tsx +0 -23
  177. package/src/components/Table/cells/RenderChip.tsx +0 -37
  178. package/src/components/Table/cells/RenderText.tsx +0 -37
  179. package/src/components/Table/cells/RenderTwoLine.tsx +0 -34
  180. package/src/components/Table/cells/RowActionsMenu.tsx +0 -147
  181. package/src/components/Table/cells/cells.test.tsx +0 -247
  182. package/src/components/Table/cells/index.ts +0 -14
  183. package/src/components/Table/table.types.ts +0 -438
  184. package/src/components/Table/table.variants.ts +0 -207
  185. package/src/components/Tabs/Tabs.test.tsx +0 -145
  186. package/src/components/Tabs/Tabs.tsx +0 -77
  187. package/src/components/Tabs/tabs.types.ts +0 -55
  188. package/src/components/Tabs/tabs.variants.ts +0 -69
  189. package/src/components/Tabs/useTabs.ts +0 -167
  190. package/src/components/TextField/TextField.test.tsx +0 -198
  191. package/src/components/TextField/TextField.tsx +0 -245
  192. package/src/components/TextField/textField.types.ts +0 -90
  193. package/src/components/TextField/textField.variants.ts +0 -105
  194. package/src/components/Textarea/Textarea.test.tsx +0 -75
  195. package/src/components/Textarea/Textarea.tsx +0 -190
  196. package/src/components/Textarea/textarea.types.ts +0 -60
  197. package/src/components/Textarea/textarea.variants.ts +0 -85
  198. package/src/components/TimePicker/TimePicker.test.tsx +0 -65
  199. package/src/components/TimePicker/TimePicker.tsx +0 -274
  200. package/src/components/TimePicker/timePicker.types.ts +0 -69
  201. package/src/components/TimePicker/timePicker.variants.ts +0 -53
  202. package/src/components/Tooltip/Tooltip.test.tsx +0 -73
  203. package/src/components/Tooltip/Tooltip.tsx +0 -70
  204. package/src/components/Tooltip/tooltip.types.ts +0 -44
  205. package/src/components/Tooltip/tooltip.variants.ts +0 -28
  206. package/src/components/TopBar/TopBar.test.tsx +0 -129
  207. package/src/components/TopBar/TopBar.tsx +0 -66
  208. package/src/components/TopBar/topBar.types.ts +0 -42
  209. package/src/components/TopBar/topBar.variants.ts +0 -38
  210. package/src/components/Typography/Typography.test.tsx +0 -247
  211. package/src/components/Typography/Typography.tsx +0 -112
  212. package/src/components/Typography/typography.types.ts +0 -65
  213. package/src/components/Typography/typography.variants.ts +0 -130
  214. package/src/components/VisuallyHidden/VisuallyHidden.test.tsx +0 -86
  215. package/src/components/VisuallyHidden/VisuallyHidden.tsx +0 -59
  216. package/src/components/VisuallyHidden/visuallyHidden.types.ts +0 -45
  217. package/src/components/_shared/data/getNestedValue.test.ts +0 -50
  218. package/src/components/_shared/data/getNestedValue.ts +0 -32
  219. package/src/components/_shared/data/useColumnAutoDetect.test.ts +0 -123
  220. package/src/components/_shared/data/useColumnAutoDetect.ts +0 -100
  221. package/src/components/_shared/data/useControllableState.ts +0 -48
  222. package/src/components/_shared/data/useDebouncedValue.ts +0 -24
  223. package/src/components/_shared/data/useTableFilter.test.ts +0 -166
  224. package/src/components/_shared/data/useTableFilter.ts +0 -118
  225. package/src/components/_shared/data/useTableSearch.test.ts +0 -115
  226. package/src/components/_shared/data/useTableSearch.ts +0 -66
  227. package/src/components/_shared/data/useTableSelection.test.ts +0 -100
  228. package/src/components/_shared/data/useTableSelection.ts +0 -100
  229. package/src/components/_shared/data/useTableSort.test.ts +0 -150
  230. package/src/components/_shared/data/useTableSort.ts +0 -141
  231. package/src/components/_shared/data/useVirtualizer.test.ts +0 -158
  232. package/src/components/_shared/data/useVirtualizer.ts +0 -199
  233. package/src/components/_shared/resolveValidationState.ts +0 -60
  234. package/src/components/_shared/severity/index.ts +0 -33
  235. package/src/components/_shared/severity/severity.types.ts +0 -50
  236. package/src/components/_shared/severity/severityIcons.tsx +0 -104
  237. package/src/components/_shared/severity/severityVariants.test.ts +0 -136
  238. package/src/components/_shared/severity/severityVariants.ts +0 -115
  239. package/src/components/_shared/themeIdentity.test.ts +0 -113
  240. package/src/hooks/useAccessState.spec.tsx +0 -161
  241. package/src/hooks/useAccessState.ts +0 -81
  242. package/src/index.ts +0 -550
  243. package/src/utils/cn.spec.ts +0 -16
  244. package/src/utils/cn.ts +0 -29
  245. package/test-output/vitest/coverage/base.css +0 -224
  246. package/test-output/vitest/coverage/block-navigation.js +0 -87
  247. package/test-output/vitest/coverage/clover.xml +0 -2418
  248. package/test-output/vitest/coverage/coverage-final.json +0 -89
  249. package/test-output/vitest/coverage/favicon.png +0 -0
  250. package/test-output/vitest/coverage/index.html +0 -746
  251. package/test-output/vitest/coverage/prettify.css +0 -1
  252. package/test-output/vitest/coverage/prettify.js +0 -2
  253. package/test-output/vitest/coverage/sort-arrow-sprite.png +0 -0
  254. package/test-output/vitest/coverage/sorter.js +0 -210
  255. package/test-output/vitest/coverage/src/components/Accordion/Accordion.tsx.html +0 -376
  256. package/test-output/vitest/coverage/src/components/Accordion/accordion.variants.ts.html +0 -181
  257. package/test-output/vitest/coverage/src/components/Accordion/index.html +0 -131
  258. package/test-output/vitest/coverage/src/components/AppShell/AppShell.tsx.html +0 -802
  259. package/test-output/vitest/coverage/src/components/AppShell/appShell.variants.ts.html +0 -247
  260. package/test-output/vitest/coverage/src/components/AppShell/index.html +0 -131
  261. package/test-output/vitest/coverage/src/components/AspectRatio/AspectRatio.tsx.html +0 -304
  262. package/test-output/vitest/coverage/src/components/AspectRatio/index.html +0 -116
  263. package/test-output/vitest/coverage/src/components/Autocomplete/Autocomplete.tsx.html +0 -3292
  264. package/test-output/vitest/coverage/src/components/Autocomplete/autocomplete.variants.ts.html +0 -508
  265. package/test-output/vitest/coverage/src/components/Autocomplete/index.html +0 -131
  266. package/test-output/vitest/coverage/src/components/Box/Box.tsx.html +0 -334
  267. package/test-output/vitest/coverage/src/components/Box/box.variants.ts.html +0 -775
  268. package/test-output/vitest/coverage/src/components/Box/index.html +0 -131
  269. package/test-output/vitest/coverage/src/components/Breadcrumbs/Breadcrumbs.tsx.html +0 -694
  270. package/test-output/vitest/coverage/src/components/Breadcrumbs/breadcrumbs.variants.ts.html +0 -247
  271. package/test-output/vitest/coverage/src/components/Breadcrumbs/index.html +0 -131
  272. package/test-output/vitest/coverage/src/components/Button/Button.tsx.html +0 -535
  273. package/test-output/vitest/coverage/src/components/Button/button.variants.ts.html +0 -562
  274. package/test-output/vitest/coverage/src/components/Button/index.html +0 -131
  275. package/test-output/vitest/coverage/src/components/Checkbox/Checkbox.tsx.html +0 -943
  276. package/test-output/vitest/coverage/src/components/Checkbox/checkbox.variants.ts.html +0 -289
  277. package/test-output/vitest/coverage/src/components/Checkbox/index.html +0 -131
  278. package/test-output/vitest/coverage/src/components/ConfirmDialog/ConfirmDialog.tsx.html +0 -841
  279. package/test-output/vitest/coverage/src/components/ConfirmDialog/confirmDialog.variants.ts.html +0 -313
  280. package/test-output/vitest/coverage/src/components/ConfirmDialog/index.html +0 -131
  281. package/test-output/vitest/coverage/src/components/Container/Container.tsx.html +0 -307
  282. package/test-output/vitest/coverage/src/components/Container/container.variants.ts.html +0 -328
  283. package/test-output/vitest/coverage/src/components/Container/index.html +0 -131
  284. package/test-output/vitest/coverage/src/components/DataGrid/DataGrid.tsx.html +0 -3937
  285. package/test-output/vitest/coverage/src/components/DataGrid/dataGrid.variants.ts.html +0 -541
  286. package/test-output/vitest/coverage/src/components/DataGrid/filters/ColumnFilters.tsx.html +0 -1453
  287. package/test-output/vitest/coverage/src/components/DataGrid/filters/index.html +0 -116
  288. package/test-output/vitest/coverage/src/components/DataGrid/index.html +0 -131
  289. package/test-output/vitest/coverage/src/components/DataGrid/reorder/index.html +0 -116
  290. package/test-output/vitest/coverage/src/components/DataGrid/reorder/useColumnReorder.ts.html +0 -517
  291. package/test-output/vitest/coverage/src/components/DataGrid/resize/index.html +0 -116
  292. package/test-output/vitest/coverage/src/components/DataGrid/resize/useColumnResize.ts.html +0 -490
  293. package/test-output/vitest/coverage/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx.html +0 -592
  294. package/test-output/vitest/coverage/src/components/DataGrid/visibility/index.html +0 -116
  295. package/test-output/vitest/coverage/src/components/DateTimePicker/DateTimePicker.tsx.html +0 -1042
  296. package/test-output/vitest/coverage/src/components/DateTimePicker/dateTimePicker.variants.ts.html +0 -337
  297. package/test-output/vitest/coverage/src/components/DateTimePicker/index.html +0 -131
  298. package/test-output/vitest/coverage/src/components/Dialog/Dialog.tsx.html +0 -391
  299. package/test-output/vitest/coverage/src/components/Dialog/dialog.variants.ts.html +0 -283
  300. package/test-output/vitest/coverage/src/components/Dialog/index.html +0 -131
  301. package/test-output/vitest/coverage/src/components/Divider/Divider.tsx.html +0 -505
  302. package/test-output/vitest/coverage/src/components/Divider/divider.variants.ts.html +0 -433
  303. package/test-output/vitest/coverage/src/components/Divider/index.html +0 -131
  304. package/test-output/vitest/coverage/src/components/Grid/Grid.tsx.html +0 -529
  305. package/test-output/vitest/coverage/src/components/Grid/grid.variants.ts.html +0 -484
  306. package/test-output/vitest/coverage/src/components/Grid/index.html +0 -131
  307. package/test-output/vitest/coverage/src/components/LeftNav/LeftNav.tsx.html +0 -1075
  308. package/test-output/vitest/coverage/src/components/LeftNav/index.html +0 -131
  309. package/test-output/vitest/coverage/src/components/LeftNav/leftNav.variants.ts.html +0 -406
  310. package/test-output/vitest/coverage/src/components/NumberField/NumberField.tsx.html +0 -1048
  311. package/test-output/vitest/coverage/src/components/NumberField/index.html +0 -131
  312. package/test-output/vitest/coverage/src/components/NumberField/numberField.variants.ts.html +0 -367
  313. package/test-output/vitest/coverage/src/components/OTPField/OTPField.tsx.html +0 -835
  314. package/test-output/vitest/coverage/src/components/OTPField/index.html +0 -131
  315. package/test-output/vitest/coverage/src/components/OTPField/otpField.variants.ts.html +0 -301
  316. package/test-output/vitest/coverage/src/components/Pagination/Pagination.tsx.html +0 -916
  317. package/test-output/vitest/coverage/src/components/Pagination/index.html +0 -146
  318. package/test-output/vitest/coverage/src/components/Pagination/pagination.helpers.ts.html +0 -391
  319. package/test-output/vitest/coverage/src/components/Pagination/pagination.variants.ts.html +0 -436
  320. package/test-output/vitest/coverage/src/components/Popover/Popover.tsx.html +0 -250
  321. package/test-output/vitest/coverage/src/components/Popover/index.html +0 -131
  322. package/test-output/vitest/coverage/src/components/Popover/popover.variants.ts.html +0 -157
  323. package/test-output/vitest/coverage/src/components/RadioGroup/RadioGroup.tsx.html +0 -934
  324. package/test-output/vitest/coverage/src/components/RadioGroup/index.html +0 -131
  325. package/test-output/vitest/coverage/src/components/RadioGroup/radioGroup.variants.ts.html +0 -346
  326. package/test-output/vitest/coverage/src/components/Skeleton/Skeleton.tsx.html +0 -232
  327. package/test-output/vitest/coverage/src/components/Skeleton/index.html +0 -131
  328. package/test-output/vitest/coverage/src/components/Skeleton/skeleton.variants.ts.html +0 -346
  329. package/test-output/vitest/coverage/src/components/Snackbar/Snackbar.tsx.html +0 -979
  330. package/test-output/vitest/coverage/src/components/Snackbar/index.html +0 -131
  331. package/test-output/vitest/coverage/src/components/Snackbar/snackbar.variants.ts.html +0 -307
  332. package/test-output/vitest/coverage/src/components/Stack/Stack.tsx.html +0 -430
  333. package/test-output/vitest/coverage/src/components/Stack/index.html +0 -131
  334. package/test-output/vitest/coverage/src/components/Stack/stack.variants.ts.html +0 -325
  335. package/test-output/vitest/coverage/src/components/Switch/Switch.tsx.html +0 -559
  336. package/test-output/vitest/coverage/src/components/Switch/index.html +0 -131
  337. package/test-output/vitest/coverage/src/components/Switch/switch.variants.ts.html +0 -295
  338. package/test-output/vitest/coverage/src/components/Table/Table.tsx.html +0 -2677
  339. package/test-output/vitest/coverage/src/components/Table/cells/RenderButton.tsx.html +0 -154
  340. package/test-output/vitest/coverage/src/components/Table/cells/RenderChip.tsx.html +0 -385
  341. package/test-output/vitest/coverage/src/components/Table/cells/RenderText.tsx.html +0 -196
  342. package/test-output/vitest/coverage/src/components/Table/cells/RenderTwoLine.tsx.html +0 -187
  343. package/test-output/vitest/coverage/src/components/Table/cells/RowActionsMenu.tsx.html +0 -526
  344. package/test-output/vitest/coverage/src/components/Table/cells/index.html +0 -191
  345. package/test-output/vitest/coverage/src/components/Table/cells/index.ts.html +0 -127
  346. package/test-output/vitest/coverage/src/components/Table/index.html +0 -131
  347. package/test-output/vitest/coverage/src/components/Table/table.variants.ts.html +0 -706
  348. package/test-output/vitest/coverage/src/components/Tabs/Tabs.tsx.html +0 -283
  349. package/test-output/vitest/coverage/src/components/Tabs/index.html +0 -131
  350. package/test-output/vitest/coverage/src/components/Tabs/tabs.variants.ts.html +0 -292
  351. package/test-output/vitest/coverage/src/components/TextField/TextField.tsx.html +0 -820
  352. package/test-output/vitest/coverage/src/components/TextField/index.html +0 -131
  353. package/test-output/vitest/coverage/src/components/TextField/textField.variants.ts.html +0 -400
  354. package/test-output/vitest/coverage/src/components/Textarea/Textarea.tsx.html +0 -655
  355. package/test-output/vitest/coverage/src/components/Textarea/index.html +0 -131
  356. package/test-output/vitest/coverage/src/components/Textarea/textarea.variants.ts.html +0 -340
  357. package/test-output/vitest/coverage/src/components/Tooltip/Tooltip.tsx.html +0 -295
  358. package/test-output/vitest/coverage/src/components/Tooltip/index.html +0 -131
  359. package/test-output/vitest/coverage/src/components/Tooltip/tooltip.variants.ts.html +0 -169
  360. package/test-output/vitest/coverage/src/components/TopBar/TopBar.tsx.html +0 -283
  361. package/test-output/vitest/coverage/src/components/TopBar/index.html +0 -131
  362. package/test-output/vitest/coverage/src/components/TopBar/topBar.variants.ts.html +0 -199
  363. package/test-output/vitest/coverage/src/components/Typography/Typography.tsx.html +0 -421
  364. package/test-output/vitest/coverage/src/components/Typography/index.html +0 -131
  365. package/test-output/vitest/coverage/src/components/Typography/typography.variants.ts.html +0 -436
  366. package/test-output/vitest/coverage/src/components/VisuallyHidden/VisuallyHidden.tsx.html +0 -262
  367. package/test-output/vitest/coverage/src/components/VisuallyHidden/index.html +0 -116
  368. package/test-output/vitest/coverage/src/components/_shared/data/getNestedValue.ts.html +0 -181
  369. package/test-output/vitest/coverage/src/components/_shared/data/index.html +0 -236
  370. package/test-output/vitest/coverage/src/components/_shared/data/useColumnAutoDetect.ts.html +0 -385
  371. package/test-output/vitest/coverage/src/components/_shared/data/useControllableState.ts.html +0 -229
  372. package/test-output/vitest/coverage/src/components/_shared/data/useDebouncedValue.ts.html +0 -157
  373. package/test-output/vitest/coverage/src/components/_shared/data/useTableFilter.ts.html +0 -439
  374. package/test-output/vitest/coverage/src/components/_shared/data/useTableSearch.ts.html +0 -283
  375. package/test-output/vitest/coverage/src/components/_shared/data/useTableSelection.ts.html +0 -385
  376. package/test-output/vitest/coverage/src/components/_shared/data/useTableSort.ts.html +0 -508
  377. package/test-output/vitest/coverage/src/components/_shared/data/useVirtualizer.ts.html +0 -682
  378. package/test-output/vitest/coverage/src/components/_shared/index.html +0 -116
  379. package/test-output/vitest/coverage/src/components/_shared/resolveValidationState.ts.html +0 -265
  380. package/test-output/vitest/coverage/src/hooks/index.html +0 -116
  381. package/test-output/vitest/coverage/src/hooks/useAccessState.ts.html +0 -328
  382. package/test-output/vitest/coverage/src/index.html +0 -116
  383. package/test-output/vitest/coverage/src/index.ts.html +0 -1153
  384. package/test-output/vitest/coverage/src/utils/cn.ts.html +0 -172
  385. package/test-output/vitest/coverage/src/utils/index.html +0 -116
  386. package/tsconfig.json +0 -13
  387. package/tsconfig.lib.json +0 -58
  388. package/tsconfig.spec.json +0 -34
  389. package/vite.config.ts +0 -33
  390. package/vitest.config.mts +0 -29
@@ -1,1069 +0,0 @@
1
- import {
2
- useCallback,
3
- useContext,
4
- useEffect,
5
- useId,
6
- useMemo,
7
- useRef,
8
- useState,
9
- type KeyboardEvent,
10
- type MouseEvent,
11
- type ReactNode,
12
- } from 'react';
13
- import { DashFormContext, useEngineVisibility } from '@dashforge/ui-core';
14
- import type { DashFormBridge, FieldRegistration } from '@dashforge/ui-core';
15
- import { useDashFieldMeta } from '@dashforge/forms';
16
- import { cn } from '../../utils/cn.js';
17
- import { useAccessState } from '../../hooks/useAccessState.js';
18
- import { resolveValidationState } from '../_shared/resolveValidationState.js';
19
- import { autocompleteVariants } from './autocomplete.variants.js';
20
- import type {
21
- AutocompleteOption,
22
- AutocompleteProps,
23
- AutocompleteValue,
24
- } from './autocomplete.types.js';
25
-
26
- /**
27
- * Inline SVG icons used by the Autocomplete chrome (chip remove, clear,
28
- * dropdown caret). Stroke uses `currentColor` so the parent's `text-*`
29
- * propagates — same pattern as Checkbox's CheckIcon.
30
- *
31
- * Why inline SVG (not lucide / heroicons / unicode glyphs):
32
- * - Zero icon-library dependency: keeps `@dashforge/tw` self-contained.
33
- * - Crisp at every size (the unicode `×` / `▾` glyphs we shipped pre-
34
- * 0.2.2 rendered as chunky font characters that looked unpolished
35
- * next to the rest of the design system).
36
- * - SVG scales with parent font-size cleanly via `width="1em" height="1em"`.
37
- *
38
- * @internal
39
- */
40
- function CloseIcon({ className }: { className?: string }) {
41
- return (
42
- <svg
43
- aria-hidden="true"
44
- viewBox="0 0 16 16"
45
- width="1em"
46
- height="1em"
47
- fill="none"
48
- className={className}
49
- >
50
- <path
51
- d="M4 4l8 8M12 4l-8 8"
52
- stroke="currentColor"
53
- strokeWidth="1.75"
54
- strokeLinecap="round"
55
- />
56
- </svg>
57
- );
58
- }
59
-
60
- function ChevronDownIcon({ className }: { className?: string }) {
61
- return (
62
- <svg
63
- aria-hidden="true"
64
- viewBox="0 0 16 16"
65
- width="1em"
66
- height="1em"
67
- fill="none"
68
- className={className}
69
- >
70
- <path
71
- d="M4 6l4 4 4-4"
72
- stroke="currentColor"
73
- strokeWidth="1.75"
74
- strokeLinecap="round"
75
- strokeLinejoin="round"
76
- />
77
- </svg>
78
- );
79
- }
80
-
81
- /**
82
- * Coerce a raw value (from bridge / props) into a canonical shape:
83
- *
84
- * - Multi-select: returns `string[]` (empty for null/undefined)
85
- * - Single-select: returns `string | null`
86
- */
87
- function coerceValue(raw: unknown, isMulti: boolean): AutocompleteValue {
88
- if (isMulti) {
89
- if (raw == null) return [];
90
- if (Array.isArray(raw)) return raw.map(String);
91
- return [String(raw)];
92
- }
93
- if (raw == null) return null;
94
- if (Array.isArray(raw)) return raw[0] != null ? String(raw[0]) : null;
95
- return String(raw);
96
- }
97
-
98
- /** Derive a Set of selected keys from a canonical value. */
99
- function getSelectedKeys(value: AutocompleteValue): Set<string> {
100
- if (value == null) return new Set();
101
- if (Array.isArray(value)) return new Set(value);
102
- return new Set([value]);
103
- }
104
-
105
- // Default accessor implementations. Assume `AutocompleteOption` shape —
106
- // safe because the generic defaults to that shape, and consumers using
107
- // a custom `TOption` must pass their own `getOptionValue` / `Label`.
108
- // We type the input as `unknown` here to keep the defaults assignable
109
- // to `(o: TOption) => …` for arbitrary `TOption` without forcing a cast
110
- // at every call site.
111
- const defaultGetOptionValue = (option: unknown): string =>
112
- (option as AutocompleteOption).value;
113
- const defaultGetOptionLabel = (option: unknown) =>
114
- (option as AutocompleteOption).label;
115
- const defaultGetOptionDisabled = (option: unknown): boolean =>
116
- Boolean((option as AutocompleteOption).disabled);
117
-
118
- /**
119
- * Dashforge TW Autocomplete — bridge-integrated single-select combobox.
120
- *
121
- * **Implementation note (F5-A, rev. 2)**
122
- *
123
- * The first revision wrapped `react-aria-components`' `<ComboBox>` to get
124
- * AAA-grade a11y for free. In practice React Aria's internal state
125
- * machine fought every controlled-state pattern we tried (the input text
126
- * would not clear when `selectedKey` went to `null`, even with a forced
127
- * remount via `key` bumping). To get deterministic behaviour we own all
128
- * of the state ourselves:
129
- *
130
- * - `inputValue` — the visible text in the `<input>`
131
- * - `isOpen` — popover visibility
132
- * - `highlightedIndex` — keyboard navigation cursor
133
- *
134
- * A11y is still solid:
135
- * - `role="combobox"` + `aria-autocomplete="list"` on the input
136
- * - `aria-expanded`, `aria-controls`, `aria-activedescendant`
137
- * - `role="listbox"` / `role="option"` with `aria-selected`
138
- * - Full keyboard nav (↑/↓ navigate, Enter selects, Escape closes,
139
- * Home/End jump, Tab closes, click-outside closes)
140
- * - Required field gets `aria-required="true"`
141
- *
142
- * F5-A scope: single-select, static options, contains-substring filter
143
- * (case-insensitive). Multi-select, free-solo text, generic option
144
- * shapes, and async runtime options (`optionsFromFieldData`) are
145
- * deferred to F5-A-bis.
146
- */
147
- export function Autocomplete<TOption = AutocompleteOption>(
148
- props: AutocompleteProps<TOption>
149
- ) {
150
- const {
151
- name,
152
- rules,
153
- options,
154
- visibleWhen,
155
- layout = 'stacked',
156
- size,
157
- label,
158
- helperText,
159
- required,
160
- error,
161
- disabled,
162
- fullWidth,
163
- access,
164
- sx,
165
- slotProps,
166
- placeholder,
167
- value: explicitValue,
168
- defaultValue,
169
- onValueChange,
170
- emptyMessage = 'No matching options',
171
- getOptionValue = defaultGetOptionValue as (option: TOption) => string,
172
- getOptionLabel = defaultGetOptionLabel as (option: TOption) => ReactNode,
173
- getOptionDisabled = defaultGetOptionDisabled as (option: TOption) => boolean,
174
- getOptionKey,
175
- multiple,
176
- freeSolo,
177
- loadOptions,
178
- loadDebounceMs = 250,
179
- loadingMessage = 'Loading…',
180
- } = props;
181
-
182
- const isMulti = Boolean(multiple);
183
-
184
- // Default the React-key accessor to `getOptionValue` (typical case:
185
- // value is unique). Consumers override only when distinct option
186
- // records can share the same persisted value.
187
- const resolveOptionKey = getOptionKey ?? getOptionValue;
188
-
189
- /**
190
- * Resolve the string label of an option, if it is a string. Returns
191
- * `undefined` for non-string labels (e.g., `<span>`) so callers can
192
- * fall back to the persisted value for `textValue` / input text.
193
- */
194
- const labelAsString = (option: TOption): string | undefined => {
195
- const lbl = getOptionLabel(option);
196
- return typeof lbl === 'string' ? lbl : undefined;
197
- };
198
-
199
- // ───── Hooks (unconditional) ─────
200
- const bridge = useContext(DashFormContext) as DashFormBridge | null;
201
- const isVisible = useEngineVisibility(bridge?.engine, visibleWhen);
202
- useDashFieldMeta(name);
203
- const accessState = useAccessState(access);
204
-
205
- const baseId = useId();
206
- const labelId = `${baseId}-label`;
207
- const helperId = `${baseId}-help`;
208
- const listboxId = `${baseId}-listbox`;
209
- const optionIdPrefix = `${baseId}-opt-`;
210
-
211
- // StrictMode-safe unregister-on-unmount
212
- const unregisterRef = useRef({ bridge, name });
213
- unregisterRef.current = { bridge, name };
214
- const isMountedRef = useRef(false);
215
-
216
- useEffect(() => {
217
- isMountedRef.current = true;
218
- return () => {
219
- isMountedRef.current = false;
220
- const { bridge: cap, name: capName } = unregisterRef.current;
221
- queueMicrotask(() => {
222
- if (!isMountedRef.current) cap?.unregister?.(capName);
223
- });
224
- };
225
- }, []);
226
-
227
- // ───── Local UI state (we own EVERYTHING) ─────
228
-
229
- /**
230
- * The visible text in the `<input>`. Seeded from the initial value
231
- * (single-select only — in multi mode the chips show the selection
232
- * separately and the input always starts empty).
233
- */
234
- const [inputValue, setInputValue] = useState<string>(() => {
235
- if (isMulti) return '';
236
- let initialKey: string | null = null;
237
- if (explicitValue !== undefined) {
238
- initialKey = typeof explicitValue === 'string' ? explicitValue : null;
239
- } else if (defaultValue !== undefined) {
240
- initialKey = typeof defaultValue === 'string' ? defaultValue : null;
241
- } else if (bridge?.register) {
242
- const bv = bridge.getValue(name);
243
- initialKey = bv == null ? null : String(bv);
244
- }
245
- if (initialKey == null) return '';
246
- const found = options.find((opt) => getOptionValue(opt) === initialKey);
247
- // Free-solo: if the initial key doesn't match any option, fall back
248
- // to the key string itself (it IS the user-visible text).
249
- return found
250
- ? labelAsString(found) ?? (freeSolo ? initialKey : '')
251
- : freeSolo
252
- ? initialKey
253
- : '';
254
- });
255
-
256
- /** Uncontrolled-mode internal selection (form mode uses the bridge). */
257
- const [uncontrolledValue, setUncontrolledValue] = useState<AutocompleteValue>(
258
- () => {
259
- if (explicitValue !== undefined) return coerceValue(explicitValue, isMulti);
260
- if (defaultValue !== undefined) return coerceValue(defaultValue, isMulti);
261
- return isMulti ? [] : null;
262
- }
263
- );
264
-
265
- /**
266
- * Whether the user has interacted with the field in uncontrolled mode.
267
- * Before first interaction, `defaultValue` wins (so a rerender with a
268
- * new `defaultValue` is reflected in `resolvedValue` / `inputValue`).
269
- * After first interaction, the user's choice (stored in
270
- * `uncontrolledValue` / `inputValue`) wins.
271
- */
272
- const [hasInteracted, setHasInteracted] = useState(false);
273
-
274
- const [isOpen, setIsOpen] = useState(false);
275
- const [highlightedIndex, setHighlightedIndex] = useState(-1);
276
-
277
- // Async-options state (only used when `loadOptions` is provided).
278
- // - `asyncOptions` is `null` before the first fetch resolves; after,
279
- // it holds whatever the loader returned. It always takes
280
- // precedence over the static `options` prop once non-null.
281
- // - `isLoading` true between a debounce-firing fetch and its
282
- // resolution.
283
- const [asyncOptions, setAsyncOptions] = useState<TOption[] | null>(null);
284
- const [isLoading, setIsLoading] = useState(false);
285
-
286
- const rootRef = useRef<HTMLDivElement | null>(null);
287
- const inputRef = useRef<HTMLInputElement | null>(null);
288
- const listboxRef = useRef<HTMLUListElement | null>(null);
289
- /**
290
- * Monotonic counter for the in-flight `loadOptions` fetch so a
291
- * later-typed query can't get overtaken by a slower earlier fetch.
292
- * Each fire bumps the counter; only the response whose generation
293
- * matches the current counter is allowed to commit state.
294
- */
295
- const fetchGenRef = useRef(0);
296
-
297
- // ───── Derived (no hooks below this point can be skipped) ─────
298
- //
299
- // Everything in this block runs every render so the hooks below
300
- // (useMemo / useCallback / useEffect) stay above any early-return
301
- // guards — see rules of hooks.
302
-
303
- const effectiveDisabled = Boolean(disabled) || accessState.disabled;
304
- const effectiveReadOnly = accessState.readonly;
305
- const isFormMode = Boolean(bridge?.register);
306
-
307
- let resolvedError = error;
308
- let resolvedHelperText: typeof helperText = helperText;
309
- let registration: FieldRegistration | null = null;
310
- let resolvedValue: AutocompleteValue;
311
-
312
- if (isFormMode && bridge) {
313
- registration = bridge.register(name, rules);
314
- const validation = resolveValidationState(name, bridge, error, helperText);
315
- resolvedError = validation.error;
316
- resolvedHelperText = validation.helperText;
317
- if (explicitValue !== undefined) {
318
- resolvedValue = coerceValue(explicitValue, isMulti);
319
- } else {
320
- resolvedValue = coerceValue(bridge.getValue(name), isMulti);
321
- }
322
- } else if (explicitValue !== undefined) {
323
- resolvedValue = coerceValue(explicitValue, isMulti);
324
- } else if (!hasInteracted && defaultValue !== undefined) {
325
- resolvedValue = coerceValue(defaultValue, isMulti);
326
- } else {
327
- resolvedValue = uncontrolledValue;
328
- }
329
-
330
- // Set of currently-selected keys. Always exists (empty Set when no
331
- // selection) — internal lookups can call `.has(key)` without null
332
- // checks.
333
- const selectedKeys = getSelectedKeys(resolvedValue);
334
- // Convenience: for the clear button visibility + the "should keep
335
- // popover open after pick" decision.
336
- const hasAnySelection = selectedKeys.size > 0;
337
- // Single-select compatibility: the one selected key (null in multi
338
- // or when nothing is picked).
339
- const singleSelectedKey =
340
- !isMulti && typeof resolvedValue === 'string' ? resolvedValue : null;
341
-
342
- // Visible input value:
343
- // - Multi-select: always the locally-managed `inputValue` (selection
344
- // is rendered as chips, not echoed in the input).
345
- // - Single-select, before first interaction: derive from `resolvedValue`
346
- // so a rerender with a new `defaultValue` is reflected immediately.
347
- // - Single-select, after first interaction: trust `inputValue` (which
348
- // `commitSelection` + `handleInputChange` keep in sync).
349
- let displayInputValue: string = inputValue;
350
- if (!isMulti && !hasInteracted) {
351
- if (singleSelectedKey == null) {
352
- displayInputValue = '';
353
- } else {
354
- const found = options.find(
355
- (opt) => getOptionValue(opt) === singleSelectedKey
356
- );
357
- const found_label = found ? labelAsString(found) : undefined;
358
- if (found_label !== undefined) {
359
- displayInputValue = found_label;
360
- } else if (freeSolo) {
361
- // No matching option ⇒ this is a free-solo value; the key
362
- // itself is the displayable text.
363
- displayInputValue = singleSelectedKey;
364
- }
365
- }
366
- }
367
-
368
- // ───── Effective options source ─────
369
- //
370
- // Precedence: live async results > static `options` prop.
371
- //
372
- // While `loadOptions` is configured, the SERVER is responsible for
373
- // filtering, so the client-side `contains` filter below is bypassed
374
- // (using it would double-filter and hide rows the server intentionally
375
- // returned).
376
- const effectiveOptions: TOption[] =
377
- loadOptions && asyncOptions !== null ? asyncOptions : options;
378
- const useServerFilter = Boolean(loadOptions);
379
-
380
- // ───── Filtering ─────
381
- const filteredOptions = useMemo<TOption[]>(() => {
382
- // Async mode: trust the server's filter; never apply a client-side
383
- // one on top.
384
- if (useServerFilter) return effectiveOptions;
385
-
386
- // Single-select: when the field is closed and the input shows the
387
- // exact selected label, show the FULL list on the next open (rather
388
- // than the single-match filter the label would produce).
389
- //
390
- // Multi-select: the input value is purely the filter query (the
391
- // selection lives in the chips), so this short-circuit doesn't apply.
392
- if (!isMulti) {
393
- const selectedOpt = effectiveOptions.find(
394
- (o) => getOptionValue(o) === singleSelectedKey
395
- );
396
- const selectedLabel = selectedOpt ? labelAsString(selectedOpt) : undefined;
397
- const isShowingSelected =
398
- selectedLabel !== undefined && selectedLabel === displayInputValue;
399
- if (!displayInputValue || isShowingSelected) return effectiveOptions;
400
- } else if (!displayInputValue) {
401
- return effectiveOptions;
402
- }
403
- const query = displayInputValue.toLowerCase();
404
- return effectiveOptions.filter((opt) => {
405
- const text = labelAsString(opt) ?? getOptionValue(opt);
406
- return text.toLowerCase().includes(query);
407
- });
408
- // Accessors omitted by design — see commitSelection useCallback comment.
409
- // eslint-disable-next-line react-hooks/exhaustive-deps
410
- }, [
411
- effectiveOptions,
412
- displayInputValue,
413
- singleSelectedKey,
414
- isMulti,
415
- useServerFilter,
416
- ]);
417
-
418
- // ───── Helpers ─────
419
-
420
- /**
421
- * Commit a value change to the bridge / state.
422
- *
423
- * **Single-select**: `next` is the new selected key, or `null` to
424
- * clear. Replaces the previous selection.
425
- *
426
- * **Multi-select**:
427
- * - `next === null` → clear all (emit `[]`).
428
- * - `next === <key>` → toggle that key in the selection set.
429
- *
430
- * Sync to `inputValue`:
431
- * - Single + non-null key → write the option's label into the input.
432
- * - Single + null → empty the input.
433
- * - Multi → always clear the input (the filter query is irrelevant
434
- * once a chip has been added).
435
- */
436
- const commitSelection = useCallback(
437
- (next: string | null) => {
438
- // 1) Mark dirty so `defaultValue` no longer takes precedence.
439
- setHasInteracted(true);
440
-
441
- // 2) Compute the new persisted value.
442
- let nextValue: AutocompleteValue;
443
- if (isMulti) {
444
- if (next == null) {
445
- nextValue = [];
446
- } else {
447
- const set = new Set(selectedKeys);
448
- if (set.has(next)) set.delete(next);
449
- else set.add(next);
450
- nextValue = Array.from(set);
451
- }
452
- // Multi mode: clear the filter input after every toggle.
453
- setInputValue('');
454
- } else {
455
- nextValue = next;
456
- // Single mode: sync the input label.
457
- if (next == null) {
458
- setInputValue('');
459
- } else {
460
- // Look up the label in the EFFECTIVE option pool (static
461
- // `options` + async `asyncOptions` when `loadOptions` is
462
- // configured). Looking only at the static `options` here
463
- // means async-loaded picks never update the input — the
464
- // user sees their search query stick instead of the
465
- // selected label after click. Same lookup logic mirrored in
466
- // `displayInputValue` so the two paths can't drift.
467
- const pool = loadOptions && asyncOptions !== null
468
- ? asyncOptions
469
- : options;
470
- const found = pool.find((opt) => getOptionValue(opt) === next);
471
- const found_label = found ? labelAsString(found) : undefined;
472
- if (found_label !== undefined) {
473
- setInputValue(found_label);
474
- }
475
- }
476
- }
477
-
478
- // 3) Persist.
479
- if (isFormMode && bridge) {
480
- bridge.setValue?.(name, nextValue);
481
- void registration?.onChange?.({
482
- target: {
483
- name,
484
- // RHF + native form: array values are passed through as-is
485
- // (RHF accepts any shape); the empty string fallback for
486
- // null preserves the single-select contract.
487
- value: nextValue == null ? '' : (nextValue as unknown as string),
488
- },
489
- type: 'change',
490
- });
491
- } else if (explicitValue === undefined) {
492
- setUncontrolledValue(nextValue);
493
- }
494
-
495
- // 4) Notify consumer.
496
- onValueChange?.(nextValue);
497
- },
498
- // Accessors omitted by design — see filteredOptions useMemo comment.
499
- // eslint-disable-next-line react-hooks/exhaustive-deps
500
- [
501
- options,
502
- asyncOptions,
503
- loadOptions,
504
- isFormMode,
505
- bridge,
506
- name,
507
- registration,
508
- explicitValue,
509
- onValueChange,
510
- isMulti,
511
- selectedKeys,
512
- ]
513
- );
514
-
515
- const openPopover = useCallback(() => {
516
- if (effectiveDisabled || effectiveReadOnly) return;
517
- setIsOpen(true);
518
- // Highlight the (single-)selected item on open, falling back to the
519
- // first item. In multi mode, always start at the first visible item.
520
- const selectedIdx =
521
- !isMulti && singleSelectedKey
522
- ? filteredOptions.findIndex(
523
- (o) => getOptionValue(o) === singleSelectedKey
524
- )
525
- : -1;
526
- setHighlightedIndex(selectedIdx >= 0 ? selectedIdx : 0);
527
- // Accessors omitted by design — see filteredOptions useMemo comment.
528
- // eslint-disable-next-line react-hooks/exhaustive-deps
529
- }, [
530
- effectiveDisabled,
531
- effectiveReadOnly,
532
- filteredOptions,
533
- singleSelectedKey,
534
- isMulti,
535
- ]);
536
-
537
- const closePopover = useCallback(() => {
538
- setIsOpen(false);
539
- setHighlightedIndex(-1);
540
- }, []);
541
-
542
- // ───── Click-outside ─────
543
- useEffect(() => {
544
- if (!isOpen) return;
545
- const handlePointerDown = (event: globalThis.MouseEvent) => {
546
- const root = rootRef.current;
547
- if (!root) return;
548
- if (root.contains(event.target as Node)) return;
549
- closePopover();
550
- // Multi mode: just clear the filter query (chips remain).
551
- // Single + free-solo: commit the typed text as the value (the
552
- // user's text IS the truth) — leave input alone afterwards.
553
- // Single (no free-solo): revert input to the selected option's
554
- // label or empty.
555
- if (isMulti) {
556
- setInputValue('');
557
- } else if (freeSolo && inputValue.trim() !== '') {
558
- const typed = inputValue.trim();
559
- const matched = options.find((o) => labelAsString(o) === typed);
560
- const nextKey = matched ? getOptionValue(matched) : typed;
561
- if (singleSelectedKey !== nextKey) {
562
- commitSelection(nextKey);
563
- }
564
- } else if (singleSelectedKey) {
565
- const selected = options.find(
566
- (o) => getOptionValue(o) === singleSelectedKey
567
- );
568
- const selected_label = selected ? labelAsString(selected) : undefined;
569
- if (selected_label !== undefined) {
570
- setInputValue(selected_label);
571
- }
572
- } else {
573
- setInputValue('');
574
- }
575
- };
576
- document.addEventListener('mousedown', handlePointerDown);
577
- return () => document.removeEventListener('mousedown', handlePointerDown);
578
- // Accessors omitted by design — see filteredOptions useMemo comment.
579
- // eslint-disable-next-line react-hooks/exhaustive-deps
580
- }, [
581
- isOpen,
582
- closePopover,
583
- options,
584
- singleSelectedKey,
585
- isMulti,
586
- freeSolo,
587
- inputValue,
588
- commitSelection,
589
- ]);
590
-
591
- // ───── Async option loading (debounced) ─────
592
- useEffect(() => {
593
- if (!loadOptions) return;
594
- const myGen = ++fetchGenRef.current;
595
- setIsLoading(true);
596
- const timer = setTimeout(() => {
597
- // Bail if the user re-typed during the debounce window — a newer
598
- // generation has already been kicked off.
599
- if (myGen !== fetchGenRef.current) return;
600
- Promise.resolve(loadOptions(inputValue))
601
- .then((next) => {
602
- if (myGen === fetchGenRef.current) {
603
- setAsyncOptions(next);
604
- setIsLoading(false);
605
- }
606
- })
607
- .catch(() => {
608
- // Surface failures as an empty list; consumer's preferred
609
- // error UI can be threaded via `emptyMessage` once they
610
- // detect their own loader fault upstream.
611
- if (myGen === fetchGenRef.current) {
612
- setAsyncOptions([]);
613
- setIsLoading(false);
614
- }
615
- });
616
- }, loadDebounceMs);
617
- return () => clearTimeout(timer);
618
- // `loadOptions` is intentionally not in the deps — consumers
619
- // typically pass an inline arrow which would otherwise trigger a
620
- // refetch on every render. Treat it as stable (mirror of MUI's
621
- // `loadOptions` contract).
622
- // eslint-disable-next-line react-hooks/exhaustive-deps
623
- }, [inputValue, loadDebounceMs]);
624
-
625
- // ───── Handlers ─────
626
-
627
- const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
628
- const next = event.target.value;
629
- setHasInteracted(true);
630
- setInputValue(next);
631
- if (!isOpen) setIsOpen(true);
632
- // Reset highlight to first visible option on every keystroke; the
633
- // filtered list will recompute on next render.
634
- setHighlightedIndex(0);
635
- };
636
-
637
- const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
638
- if (effectiveDisabled || effectiveReadOnly) return;
639
-
640
- // Multi-select: Backspace at the start of an empty input removes
641
- // the last selected chip. (Matches the de-facto pattern used by
642
- // chip-input UIs across the React ecosystem.)
643
- if (
644
- isMulti &&
645
- event.key === 'Backspace' &&
646
- inputValue === '' &&
647
- selectedKeys.size > 0
648
- ) {
649
- event.preventDefault();
650
- // Set guarantees ≥ 1 entry here (size > 0 above), but TS can't
651
- // prove it — fall back to the loop variant to avoid the non-null
652
- // assertion lint rule.
653
- const arr = Array.from(selectedKeys);
654
- const last = arr[arr.length - 1];
655
- if (last !== undefined) {
656
- commitSelection(last); // toggle removes
657
- }
658
- return;
659
- }
660
-
661
- if (event.key === 'ArrowDown') {
662
- event.preventDefault();
663
- if (!isOpen) {
664
- openPopover();
665
- return;
666
- }
667
- setHighlightedIndex((idx) => {
668
- const max = filteredOptions.length - 1;
669
- if (max < 0) return -1;
670
- return idx >= max ? 0 : idx + 1;
671
- });
672
- return;
673
- }
674
- if (event.key === 'ArrowUp') {
675
- event.preventDefault();
676
- if (!isOpen) {
677
- openPopover();
678
- return;
679
- }
680
- setHighlightedIndex((idx) => {
681
- const max = filteredOptions.length - 1;
682
- if (max < 0) return -1;
683
- return idx <= 0 ? max : idx - 1;
684
- });
685
- return;
686
- }
687
- if (event.key === 'Home' && isOpen) {
688
- event.preventDefault();
689
- setHighlightedIndex(filteredOptions.length ? 0 : -1);
690
- return;
691
- }
692
- if (event.key === 'End' && isOpen) {
693
- event.preventDefault();
694
- setHighlightedIndex(filteredOptions.length ? filteredOptions.length - 1 : -1);
695
- return;
696
- }
697
- if (event.key === 'Enter') {
698
- // Preferred path: highlighted option is committed.
699
- if (
700
- isOpen &&
701
- highlightedIndex >= 0 &&
702
- highlightedIndex < filteredOptions.length
703
- ) {
704
- event.preventDefault();
705
- const opt = filteredOptions[highlightedIndex];
706
- if (!getOptionDisabled(opt)) {
707
- commitSelection(getOptionValue(opt));
708
- if (!isMulti) {
709
- closePopover();
710
- } else {
711
- setHighlightedIndex(0);
712
- }
713
- }
714
- return;
715
- }
716
- // Free-solo path: commit the typed text as the value.
717
- if (freeSolo && inputValue.trim() !== '') {
718
- event.preventDefault();
719
- const typed = inputValue.trim();
720
- // Snap to an exact-label match if one exists, even when the
721
- // dropdown was filtered to zero rows — protects the contract
722
- // that "the value is always a known option when one matches."
723
- const matched = options.find(
724
- (o) => labelAsString(o) === typed
725
- );
726
- commitSelection(matched ? getOptionValue(matched) : typed);
727
- if (!isMulti) closePopover();
728
- else setHighlightedIndex(0);
729
- }
730
- return;
731
- }
732
- if (event.key === 'Escape') {
733
- if (isOpen) {
734
- event.preventDefault();
735
- closePopover();
736
- // Multi: clear filter; selection stays in chips.
737
- // Single: revert input to the displayable form of the committed
738
- // value — either the matched option's label, or (in free-solo)
739
- // the raw string that was committed.
740
- if (isMulti) {
741
- setInputValue('');
742
- } else if (singleSelectedKey) {
743
- const selected = options.find(
744
- (o) => getOptionValue(o) === singleSelectedKey
745
- );
746
- const selected_label = selected ? labelAsString(selected) : undefined;
747
- setInputValue(selected_label ?? (freeSolo ? singleSelectedKey : ''));
748
- } else {
749
- setInputValue('');
750
- }
751
- }
752
- return;
753
- }
754
- if (event.key === 'Tab') {
755
- // Let focus leave naturally; just close.
756
- if (isOpen) closePopover();
757
- return;
758
- }
759
- };
760
-
761
- const handleBlur = () => {
762
- // Free-solo (single mode): commit the typed text on blur so the
763
- // user doesn't have to remember to press Enter. Multi + free-solo
764
- // is Enter-only (blur would commit accidental whitespace from
765
- // tab-out).
766
- if (freeSolo && !isMulti && inputValue.trim() !== '') {
767
- const typed = inputValue.trim();
768
- const matched = options.find((o) => labelAsString(o) === typed);
769
- const nextKey = matched ? getOptionValue(matched) : typed;
770
- // Only commit if this would actually change the value.
771
- if (singleSelectedKey !== nextKey) {
772
- commitSelection(nextKey);
773
- }
774
- }
775
- if (!isFormMode || !bridge) return;
776
- const raw = bridge.getValue(name);
777
- // Multi mode: bridge value is an array — passthrough.
778
- const current = raw == null ? '' : (raw as unknown as string);
779
- registration?.onBlur?.({
780
- target: { name, value: current },
781
- type: 'blur',
782
- });
783
- };
784
-
785
- const handleOptionClick = (opt: TOption) => {
786
- if (getOptionDisabled(opt)) return;
787
- commitSelection(getOptionValue(opt));
788
- // Multi-select: keep the popover open for chained picks.
789
- if (!isMulti) {
790
- closePopover();
791
- } else {
792
- setHighlightedIndex(0);
793
- }
794
- inputRef.current?.focus();
795
- };
796
-
797
- // Pattern: `onMouseDown` preventDefault keeps focus on the `<input>`
798
- // (without this, clicking the chevron blurs the input and we lose the
799
- // popover-on-focus open state). `onClick` does the actual work so
800
- // testing-library's `fireEvent.click(...)` also triggers it.
801
- const preventBlur = (event: MouseEvent<HTMLElement>) => {
802
- event.preventDefault();
803
- };
804
-
805
- const handleTriggerClick = () => {
806
- if (effectiveDisabled || effectiveReadOnly) return;
807
- if (isOpen) {
808
- closePopover();
809
- } else {
810
- openPopover();
811
- }
812
- inputRef.current?.focus();
813
- };
814
-
815
- const handleClearClick = () => {
816
- if (effectiveDisabled || effectiveReadOnly) return;
817
- commitSelection(null);
818
- inputRef.current?.focus();
819
- };
820
-
821
- // ───── Render-time guards (after all hooks) ─────
822
-
823
- if (!isVisible) return null;
824
- if (!accessState.visible) return null;
825
-
826
- const v = autocompleteVariants({
827
- size,
828
- layout,
829
- error: resolvedError,
830
- fullWidth,
831
- disabled: effectiveDisabled,
832
- });
833
-
834
- const activeOptionId =
835
- isOpen && highlightedIndex >= 0 && highlightedIndex < filteredOptions.length
836
- ? `${optionIdPrefix}${getOptionValue(filteredOptions[highlightedIndex])}`
837
- : undefined;
838
-
839
- return (
840
- <div
841
- ref={rootRef}
842
- className={cn(v.root(), sx, slotProps?.root?.className)}
843
- >
844
- {label && (
845
- <label
846
- id={labelId}
847
- htmlFor={`${baseId}-input`}
848
- className={cn(v.label(), slotProps?.label?.className)}
849
- >
850
- {label}
851
- {required && (
852
- <span
853
- aria-hidden="true"
854
- className={cn(
855
- v.requiredMark(),
856
- slotProps?.requiredMark?.className
857
- )}
858
- >
859
- *
860
- </span>
861
- )}
862
- </label>
863
- )}
864
-
865
- <div
866
- className={cn(
867
- v.inputWrapper(),
868
- // Multi mode: let chips wrap and replace the fixed `h-*`
869
- // with `min-h-*` so the wrapper grows with selections.
870
- isMulti && 'flex-wrap min-h-[var(--ac-min-h,2.5rem)] !h-auto py-1',
871
- slotProps?.inputWrapper?.className
872
- )}
873
- >
874
- {isMulti && selectedKeys.size > 0 && (
875
- <div
876
- className={cn(
877
- v.chipsList(),
878
- slotProps?.chipsList?.className
879
- )}
880
- >
881
- {Array.from(selectedKeys).map((key) => {
882
- const opt = options.find((o) => getOptionValue(o) === key);
883
- const labelNode = opt ? getOptionLabel(opt) : key;
884
- const labelText = opt ? labelAsString(opt) ?? key : key;
885
- return (
886
- <span
887
- key={key}
888
- className={cn(v.chip(), slotProps?.chip?.className)}
889
- data-chip-key={key}
890
- >
891
- {labelNode}
892
- {!effectiveDisabled && !effectiveReadOnly && (
893
- <button
894
- type="button"
895
- onMouseDown={preventBlur}
896
- onClick={() => {
897
- commitSelection(key); // toggle removes
898
- inputRef.current?.focus();
899
- }}
900
- aria-label={`Remove ${labelText}`}
901
- tabIndex={-1}
902
- className={cn(
903
- v.chipRemove(),
904
- slotProps?.chipRemove?.className
905
- )}
906
- >
907
- <CloseIcon />
908
- </button>
909
- )}
910
- </span>
911
- );
912
- })}
913
- </div>
914
- )}
915
-
916
- <input
917
- ref={inputRef}
918
- id={`${baseId}-input`}
919
- name={name}
920
- type="text"
921
- role="combobox"
922
- aria-autocomplete="list"
923
- aria-expanded={isOpen}
924
- aria-controls={isOpen ? listboxId : undefined}
925
- aria-activedescendant={activeOptionId}
926
- aria-labelledby={label ? labelId : undefined}
927
- aria-describedby={resolvedHelperText ? helperId : undefined}
928
- aria-invalid={resolvedError ? true : undefined}
929
- aria-required={required ? true : undefined}
930
- placeholder={
931
- // In multi mode hide the placeholder once the user has at
932
- // least one chip — the chips already convey "something is
933
- // selected" and the placeholder would clutter the UI.
934
- isMulti && selectedKeys.size > 0 ? undefined : placeholder
935
- }
936
- value={displayInputValue}
937
- onChange={handleInputChange}
938
- onKeyDown={handleKeyDown}
939
- onFocus={openPopover}
940
- onBlur={handleBlur}
941
- disabled={effectiveDisabled}
942
- readOnly={effectiveReadOnly}
943
- autoComplete="off"
944
- // Multi mode: give the input some min-width so it stays
945
- // clickable even with many chips, and let it shrink/grow with
946
- // the available row space.
947
- className={cn(
948
- v.input(),
949
- isMulti && 'min-w-[6rem] flex-1 basis-24',
950
- slotProps?.input?.className
951
- )}
952
- />
953
-
954
- {hasAnySelection && !effectiveDisabled && !effectiveReadOnly && (
955
- <button
956
- type="button"
957
- onMouseDown={preventBlur}
958
- onClick={handleClearClick}
959
- aria-label="Clear selection"
960
- tabIndex={-1}
961
- className={cn(v.clearButton(), slotProps?.clearButton?.className)}
962
- >
963
- <CloseIcon />
964
- </button>
965
- )}
966
-
967
- <button
968
- type="button"
969
- onMouseDown={preventBlur}
970
- onClick={handleTriggerClick}
971
- aria-label="Open"
972
- aria-haspopup="listbox"
973
- aria-expanded={isOpen}
974
- tabIndex={-1}
975
- disabled={effectiveDisabled}
976
- className={cn(v.trigger(), slotProps?.trigger?.className)}
977
- >
978
- {/*
979
- * Chevron flips up when popover is open (CSS-only, driven by
980
- * the `aria-expanded` attribute selector on the parent
981
- * `<button>`). See autocomplete.variants.ts → `trigger` slot
982
- * for the `aria-expanded:rotate-180` rule.
983
- */}
984
- <ChevronDownIcon />
985
- </button>
986
-
987
- {isOpen && (
988
- <ul
989
- ref={listboxRef}
990
- id={listboxId}
991
- role="listbox"
992
- aria-labelledby={label ? labelId : undefined}
993
- className={cn(
994
- v.popover(),
995
- 'absolute left-0 right-0 top-full',
996
- v.listBox(),
997
- slotProps?.popover?.className,
998
- slotProps?.listBox?.className
999
- )}
1000
- >
1001
- {isLoading ? (
1002
- <li
1003
- className={cn(
1004
- v.emptyState(),
1005
- slotProps?.emptyState?.className
1006
- )}
1007
- aria-live="polite"
1008
- aria-busy="true"
1009
- >
1010
- {loadingMessage}
1011
- </li>
1012
- ) : filteredOptions.length === 0 ? (
1013
- <li
1014
- className={cn(
1015
- v.emptyState(),
1016
- slotProps?.emptyState?.className
1017
- )}
1018
- >
1019
- {emptyMessage}
1020
- </li>
1021
- ) : (
1022
- filteredOptions.map((opt, idx) => {
1023
- const optValue = getOptionValue(opt);
1024
- const optDisabled = getOptionDisabled(opt);
1025
- const isHighlighted = idx === highlightedIndex;
1026
- const isSelected = selectedKeys.has(optValue);
1027
- return (
1028
- <li
1029
- key={resolveOptionKey(opt)}
1030
- id={`${optionIdPrefix}${optValue}`}
1031
- role="option"
1032
- aria-selected={isSelected}
1033
- aria-disabled={optDisabled || undefined}
1034
- data-focused={isHighlighted || undefined}
1035
- data-selected={isSelected || undefined}
1036
- data-disabled={optDisabled || undefined}
1037
- onMouseDown={preventBlur}
1038
- onClick={() => handleOptionClick(opt)}
1039
- onMouseEnter={() => setHighlightedIndex(idx)}
1040
- className={cn(
1041
- v.listItem(),
1042
- slotProps?.listItem?.className
1043
- )}
1044
- >
1045
- {getOptionLabel(opt)}
1046
- </li>
1047
- );
1048
- })
1049
- )}
1050
- </ul>
1051
- )}
1052
- </div>
1053
-
1054
- {resolvedHelperText && (
1055
- <p
1056
- id={helperId}
1057
- className={cn(
1058
- resolvedError ? v.errorText() : v.helperText(),
1059
- resolvedError
1060
- ? slotProps?.errorText?.className
1061
- : slotProps?.helperText?.className
1062
- )}
1063
- >
1064
- {resolvedHelperText}
1065
- </p>
1066
- )}
1067
- </div>
1068
- );
1069
- }