@proyecto-viviana/solidaria 0.2.5 → 0.3.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 (555) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -236
  3. package/dist/actiongroup/createActionGroup.d.ts +29 -0
  4. package/dist/actiongroup/createActionGroup.d.ts.map +1 -0
  5. package/dist/actiongroup/index.d.ts +2 -0
  6. package/dist/actiongroup/index.d.ts.map +1 -0
  7. package/dist/autocomplete/createAutocomplete.d.ts +16 -12
  8. package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
  9. package/dist/autocomplete/index.d.ts +1 -1
  10. package/dist/autocomplete/index.d.ts.map +1 -1
  11. package/dist/breadcrumbs/createBreadcrumbs.d.ts +11 -7
  12. package/dist/breadcrumbs/createBreadcrumbs.d.ts.map +1 -1
  13. package/dist/breadcrumbs/index.d.ts +1 -1
  14. package/dist/button/createButton.d.ts +1 -1
  15. package/dist/button/createButton.d.ts.map +1 -1
  16. package/dist/button/createToggleButton.d.ts +3 -3
  17. package/dist/button/createToggleButtonGroup.d.ts +32 -0
  18. package/dist/button/createToggleButtonGroup.d.ts.map +1 -0
  19. package/dist/button/index.d.ts +6 -4
  20. package/dist/button/index.d.ts.map +1 -1
  21. package/dist/button/types.d.ts +18 -12
  22. package/dist/button/types.d.ts.map +1 -1
  23. package/dist/calendar/createCalendar.d.ts +15 -5
  24. package/dist/calendar/createCalendar.d.ts.map +1 -1
  25. package/dist/calendar/createCalendarCell.d.ts +8 -2
  26. package/dist/calendar/createCalendarCell.d.ts.map +1 -1
  27. package/dist/calendar/createCalendarGrid.d.ts +4 -4
  28. package/dist/calendar/createCalendarGrid.d.ts.map +1 -1
  29. package/dist/calendar/createRangeCalendar.d.ts +15 -5
  30. package/dist/calendar/createRangeCalendar.d.ts.map +1 -1
  31. package/dist/calendar/createRangeCalendarCell.d.ts +7 -3
  32. package/dist/calendar/createRangeCalendarCell.d.ts.map +1 -1
  33. package/dist/calendar/index.d.ts +5 -5
  34. package/dist/calendar/index.d.ts.map +1 -1
  35. package/dist/calendar/intl/index.d.ts +12 -0
  36. package/dist/calendar/intl/index.d.ts.map +1 -0
  37. package/dist/calendar/utils.d.ts +12 -0
  38. package/dist/calendar/utils.d.ts.map +1 -0
  39. package/dist/checkbox/createCheckbox.d.ts +6 -6
  40. package/dist/checkbox/createCheckbox.d.ts.map +1 -1
  41. package/dist/checkbox/createCheckboxGroup.d.ts +11 -7
  42. package/dist/checkbox/createCheckboxGroup.d.ts.map +1 -1
  43. package/dist/checkbox/createCheckboxGroupItem.d.ts +4 -4
  44. package/dist/checkbox/createCheckboxGroupItem.d.ts.map +1 -1
  45. package/dist/checkbox/createCheckboxGroupState.d.ts +2 -2
  46. package/dist/checkbox/createCheckboxGroupState.d.ts.map +1 -1
  47. package/dist/checkbox/index.d.ts +8 -8
  48. package/dist/checkbox/index.d.ts.map +1 -1
  49. package/dist/collections/index.d.ts +56 -0
  50. package/dist/collections/index.d.ts.map +1 -0
  51. package/dist/color/createColorArea.d.ts +3 -3
  52. package/dist/color/createColorArea.d.ts.map +1 -1
  53. package/dist/color/createColorField.d.ts +4 -4
  54. package/dist/color/createColorField.d.ts.map +1 -1
  55. package/dist/color/createColorSlider.d.ts +4 -4
  56. package/dist/color/createColorSlider.d.ts.map +1 -1
  57. package/dist/color/createColorSwatch.d.ts +2 -2
  58. package/dist/color/createColorSwatch.d.ts.map +1 -1
  59. package/dist/color/createColorWheel.d.ts +3 -3
  60. package/dist/color/createColorWheel.d.ts.map +1 -1
  61. package/dist/color/index.d.ts +6 -6
  62. package/dist/color/types.d.ts +98 -16
  63. package/dist/color/types.d.ts.map +1 -1
  64. package/dist/combobox/createComboBox.d.ts +16 -7
  65. package/dist/combobox/createComboBox.d.ts.map +1 -1
  66. package/dist/combobox/index.d.ts +1 -1
  67. package/dist/combobox/intl/index.d.ts +1 -1
  68. package/dist/datepicker/createDateField.d.ts +18 -6
  69. package/dist/datepicker/createDateField.d.ts.map +1 -1
  70. package/dist/datepicker/createDatePicker.d.ts +57 -5
  71. package/dist/datepicker/createDatePicker.d.ts.map +1 -1
  72. package/dist/datepicker/createDatePickerGroup.d.ts +19 -0
  73. package/dist/datepicker/createDatePickerGroup.d.ts.map +1 -0
  74. package/dist/datepicker/createDateRangePicker.d.ts +42 -0
  75. package/dist/datepicker/createDateRangePicker.d.ts.map +1 -0
  76. package/dist/datepicker/createDateSegment.d.ts +11 -3
  77. package/dist/datepicker/createDateSegment.d.ts.map +1 -1
  78. package/dist/datepicker/createTimeField.d.ts +11 -5
  79. package/dist/datepicker/createTimeField.d.ts.map +1 -1
  80. package/dist/datepicker/createTimeSegment.d.ts +29 -0
  81. package/dist/datepicker/createTimeSegment.d.ts.map +1 -0
  82. package/dist/datepicker/index.d.ts +7 -4
  83. package/dist/datepicker/index.d.ts.map +1 -1
  84. package/dist/dialog/createDialog.d.ts +5 -5
  85. package/dist/dialog/createDialog.d.ts.map +1 -1
  86. package/dist/dialog/index.d.ts +2 -2
  87. package/dist/dialog/index.d.ts.map +1 -1
  88. package/dist/dialog/types.d.ts +4 -4
  89. package/dist/disclosure/createDisclosure.d.ts +5 -2
  90. package/dist/disclosure/createDisclosure.d.ts.map +1 -1
  91. package/dist/disclosure/createDisclosureGroup.d.ts +4 -3
  92. package/dist/disclosure/createDisclosureGroup.d.ts.map +1 -1
  93. package/dist/disclosure/index.d.ts +2 -2
  94. package/dist/dnd/createDrag.d.ts +2 -2
  95. package/dist/dnd/createDrag.d.ts.map +1 -1
  96. package/dist/dnd/createDraggableCollection.d.ts +6 -2
  97. package/dist/dnd/createDraggableCollection.d.ts.map +1 -1
  98. package/dist/dnd/createDraggableItem.d.ts +3 -3
  99. package/dist/dnd/createDraggableItem.d.ts.map +1 -1
  100. package/dist/dnd/createDrop.d.ts +2 -2
  101. package/dist/dnd/createDrop.d.ts.map +1 -1
  102. package/dist/dnd/createDroppableCollection.d.ts +55 -4
  103. package/dist/dnd/createDroppableCollection.d.ts.map +1 -1
  104. package/dist/dnd/createDroppableItem.d.ts +3 -3
  105. package/dist/dnd/createDroppableItem.d.ts.map +1 -1
  106. package/dist/dnd/index.d.ts +12 -12
  107. package/dist/dnd/index.d.ts.map +1 -1
  108. package/dist/dnd/types.d.ts +2 -2
  109. package/dist/dnd/types.d.ts.map +1 -1
  110. package/dist/dnd/utils.d.ts +1 -1
  111. package/dist/dnd/utils.d.ts.map +1 -1
  112. package/dist/focus/FocusScope.d.ts +1 -1
  113. package/dist/focus/FocusScope.d.ts.map +1 -1
  114. package/dist/focus/createAutoFocus.d.ts.map +1 -1
  115. package/dist/focus/createFocusRestore.d.ts.map +1 -1
  116. package/dist/focus/createVirtualFocus.d.ts +4 -4
  117. package/dist/focus/createVirtualFocus.d.ts.map +1 -1
  118. package/dist/focus/index.d.ts +4 -4
  119. package/dist/focus/index.d.ts.map +1 -1
  120. package/dist/form/createFormReset.d.ts +1 -1
  121. package/dist/form/createFormValidation.d.ts +3 -3
  122. package/dist/form/createFormValidation.d.ts.map +1 -1
  123. package/dist/form/index.d.ts +2 -2
  124. package/dist/form/index.d.ts.map +1 -1
  125. package/dist/grid/GridKeyboardDelegate.d.ts +5 -5
  126. package/dist/grid/createGrid.d.ts +3 -3
  127. package/dist/grid/createGrid.d.ts.map +1 -1
  128. package/dist/grid/createGridCell.d.ts +3 -3
  129. package/dist/grid/createGridRow.d.ts +3 -3
  130. package/dist/grid/index.d.ts +5 -5
  131. package/dist/grid/types.d.ts +8 -8
  132. package/dist/gridlist/createGridList.d.ts +6 -4
  133. package/dist/gridlist/createGridList.d.ts.map +1 -1
  134. package/dist/gridlist/createGridListItem.d.ts +4 -4
  135. package/dist/gridlist/createGridListItem.d.ts.map +1 -1
  136. package/dist/gridlist/createGridListSelectionCheckbox.d.ts +3 -3
  137. package/dist/gridlist/createGridListSelectionCheckbox.d.ts.map +1 -1
  138. package/dist/gridlist/index.d.ts +4 -4
  139. package/dist/gridlist/types.d.ts +11 -7
  140. package/dist/gridlist/types.d.ts.map +1 -1
  141. package/dist/i18n/createCollator.d.ts.map +1 -1
  142. package/dist/i18n/createDateFormatter.d.ts.map +1 -1
  143. package/dist/i18n/createFilter.d.ts.map +1 -1
  144. package/dist/i18n/createNumberFormatter.d.ts +1 -1
  145. package/dist/i18n/createNumberFormatter.d.ts.map +1 -1
  146. package/dist/i18n/createStringFormatter.d.ts +2 -2
  147. package/dist/i18n/createStringFormatter.d.ts.map +1 -1
  148. package/dist/i18n/index.d.ts +8 -8
  149. package/dist/i18n/index.d.ts.map +1 -1
  150. package/dist/i18n/locale.d.ts +2 -2
  151. package/dist/i18n/locale.d.ts.map +1 -1
  152. package/dist/i18n/utils.d.ts.map +1 -1
  153. package/dist/index.d.ts +52 -49
  154. package/dist/index.d.ts.map +1 -1
  155. package/dist/index.js +18089 -15690
  156. package/dist/index.js.map +1 -7
  157. package/dist/index.jsx +18242 -0
  158. package/dist/index.jsx.map +1 -0
  159. package/dist/interactions/FocusableProvider.d.ts +2 -2
  160. package/dist/interactions/FocusableProvider.d.ts.map +1 -1
  161. package/dist/interactions/PressEvent.d.ts +2 -2
  162. package/dist/interactions/createFocus.d.ts +1 -1
  163. package/dist/interactions/createFocus.d.ts.map +1 -1
  164. package/dist/interactions/createFocusRing.d.ts +1 -1
  165. package/dist/interactions/createFocusRing.d.ts.map +1 -1
  166. package/dist/interactions/createFocusWithin.d.ts +1 -1
  167. package/dist/interactions/createFocusWithin.d.ts.map +1 -1
  168. package/dist/interactions/createFocusable.d.ts +3 -3
  169. package/dist/interactions/createFocusable.d.ts.map +1 -1
  170. package/dist/interactions/createHover.d.ts +5 -5
  171. package/dist/interactions/createHover.d.ts.map +1 -1
  172. package/dist/interactions/createInteractionModality.d.ts +3 -3
  173. package/dist/interactions/createInteractionModality.d.ts.map +1 -1
  174. package/dist/interactions/createKeyboard.d.ts +1 -1
  175. package/dist/interactions/createLongPress.d.ts +5 -5
  176. package/dist/interactions/createMove.d.ts +5 -5
  177. package/dist/interactions/createMove.d.ts.map +1 -1
  178. package/dist/interactions/createPress.d.ts +4 -4
  179. package/dist/interactions/createPress.d.ts.map +1 -1
  180. package/dist/interactions/index.d.ts +12 -12
  181. package/dist/interactions/index.d.ts.map +1 -1
  182. package/dist/label/createField.d.ts +4 -4
  183. package/dist/label/createField.d.ts.map +1 -1
  184. package/dist/label/createLabel.d.ts +7 -7
  185. package/dist/label/createLabel.d.ts.map +1 -1
  186. package/dist/label/createLabels.d.ts +1 -1
  187. package/dist/label/createLabels.d.ts.map +1 -1
  188. package/dist/label/index.d.ts +5 -5
  189. package/dist/landmark/createLandmark.d.ts +5 -5
  190. package/dist/landmark/createLandmark.d.ts.map +1 -1
  191. package/dist/landmark/index.d.ts +1 -1
  192. package/dist/link/createLink.d.ts +23 -7
  193. package/dist/link/createLink.d.ts.map +1 -1
  194. package/dist/link/index.d.ts +1 -1
  195. package/dist/listbox/createListBox.d.ts +12 -6
  196. package/dist/listbox/createListBox.d.ts.map +1 -1
  197. package/dist/listbox/createOption.d.ts +21 -4
  198. package/dist/listbox/createOption.d.ts.map +1 -1
  199. package/dist/listbox/index.d.ts +2 -2
  200. package/dist/listbox/index.d.ts.map +1 -1
  201. package/dist/live-announcer/announce.d.ts +2 -2
  202. package/dist/live-announcer/announce.d.ts.map +1 -1
  203. package/dist/live-announcer/index.d.ts +1 -1
  204. package/dist/menu/createMenu.d.ts +8 -7
  205. package/dist/menu/createMenu.d.ts.map +1 -1
  206. package/dist/menu/createMenuItem.d.ts +16 -4
  207. package/dist/menu/createMenuItem.d.ts.map +1 -1
  208. package/dist/menu/createMenuTrigger.d.ts +4 -4
  209. package/dist/menu/index.d.ts +3 -3
  210. package/dist/menu/index.d.ts.map +1 -1
  211. package/dist/meter/createMeter.d.ts +6 -6
  212. package/dist/meter/createMeter.d.ts.map +1 -1
  213. package/dist/meter/index.d.ts +1 -1
  214. package/dist/numberfield/createNumberField.d.ts +27 -8
  215. package/dist/numberfield/createNumberField.d.ts.map +1 -1
  216. package/dist/numberfield/index.d.ts +1 -1
  217. package/dist/overlays/ariaHideOutside.d.ts.map +1 -1
  218. package/dist/overlays/createModal.d.ts +19 -3
  219. package/dist/overlays/createModal.d.ts.map +1 -1
  220. package/dist/overlays/createOverlay.d.ts +1 -1
  221. package/dist/overlays/createOverlay.d.ts.map +1 -1
  222. package/dist/overlays/createOverlayTrigger.d.ts +6 -6
  223. package/dist/overlays/index.d.ts +6 -6
  224. package/dist/overlays/index.d.ts.map +1 -1
  225. package/dist/popover/calculatePosition.d.ts +4 -4
  226. package/dist/popover/calculatePosition.d.ts.map +1 -1
  227. package/dist/popover/createOverlayPosition.d.ts +3 -3
  228. package/dist/popover/createOverlayPosition.d.ts.map +1 -1
  229. package/dist/popover/createPopover.d.ts +4 -4
  230. package/dist/popover/createPopover.d.ts.map +1 -1
  231. package/dist/popover/index.d.ts +3 -3
  232. package/dist/progress/createProgressBar.d.ts +7 -5
  233. package/dist/progress/createProgressBar.d.ts.map +1 -1
  234. package/dist/progress/index.d.ts +1 -1
  235. package/dist/radio/createRadio.d.ts +7 -7
  236. package/dist/radio/createRadio.d.ts.map +1 -1
  237. package/dist/radio/createRadioGroup.d.ts +11 -11
  238. package/dist/radio/createRadioGroup.d.ts.map +1 -1
  239. package/dist/radio/createRadioGroupState.d.ts +3 -3
  240. package/dist/radio/createRadioGroupState.d.ts.map +1 -1
  241. package/dist/radio/index.d.ts +3 -3
  242. package/dist/radio/index.d.ts.map +1 -1
  243. package/dist/searchfield/createSearchField.d.ts +7 -7
  244. package/dist/searchfield/createSearchField.d.ts.map +1 -1
  245. package/dist/searchfield/index.d.ts +2 -2
  246. package/dist/select/createHiddenSelect.d.ts +4 -4
  247. package/dist/select/createHiddenSelect.d.ts.map +1 -1
  248. package/dist/select/createSelect.d.ts +14 -6
  249. package/dist/select/createSelect.d.ts.map +1 -1
  250. package/dist/select/index.d.ts +2 -2
  251. package/dist/select/index.d.ts.map +1 -1
  252. package/dist/selection/createTypeSelect.d.ts +2 -2
  253. package/dist/selection/index.d.ts +1 -1
  254. package/dist/separator/createSeparator.d.ts +9 -5
  255. package/dist/separator/createSeparator.d.ts.map +1 -1
  256. package/dist/separator/index.d.ts +1 -1
  257. package/dist/slider/createSlider.d.ts +11 -7
  258. package/dist/slider/createSlider.d.ts.map +1 -1
  259. package/dist/slider/index.d.ts +2 -2
  260. package/dist/ssr/index.d.ts +1 -1
  261. package/dist/ssr/index.d.ts.map +1 -1
  262. package/dist/steplist/createStepList.d.ts +36 -0
  263. package/dist/steplist/createStepList.d.ts.map +1 -0
  264. package/dist/steplist/index.d.ts +2 -0
  265. package/dist/steplist/index.d.ts.map +1 -0
  266. package/dist/switch/createSwitch.d.ts +6 -4
  267. package/dist/switch/createSwitch.d.ts.map +1 -1
  268. package/dist/switch/index.d.ts +1 -1
  269. package/dist/table/createTable.d.ts +3 -3
  270. package/dist/table/createTable.d.ts.map +1 -1
  271. package/dist/table/createTableCell.d.ts +3 -3
  272. package/dist/table/createTableCell.d.ts.map +1 -1
  273. package/dist/table/createTableColumnHeader.d.ts +3 -3
  274. package/dist/table/createTableColumnHeader.d.ts.map +1 -1
  275. package/dist/table/createTableColumnResize.d.ts +41 -0
  276. package/dist/table/createTableColumnResize.d.ts.map +1 -0
  277. package/dist/table/createTableHeaderRow.d.ts +3 -3
  278. package/dist/table/createTableRow.d.ts +3 -3
  279. package/dist/table/createTableRow.d.ts.map +1 -1
  280. package/dist/table/createTableRowGroup.d.ts +2 -2
  281. package/dist/table/createTableRowGroup.d.ts.map +1 -1
  282. package/dist/table/createTableSelectAllCheckbox.d.ts +3 -3
  283. package/dist/table/createTableSelectAllCheckbox.d.ts.map +1 -1
  284. package/dist/table/createTableSelectionCheckbox.d.ts +3 -3
  285. package/dist/table/index.d.ts +11 -9
  286. package/dist/table/index.d.ts.map +1 -1
  287. package/dist/table/types.d.ts +15 -7
  288. package/dist/table/types.d.ts.map +1 -1
  289. package/dist/tabs/createTabs.d.ts +28 -25
  290. package/dist/tabs/createTabs.d.ts.map +1 -1
  291. package/dist/tabs/index.d.ts +1 -1
  292. package/dist/tag/createTag.d.ts +2 -2
  293. package/dist/tag/createTag.d.ts.map +1 -1
  294. package/dist/tag/createTagGroup.d.ts +5 -5
  295. package/dist/tag/createTagGroup.d.ts.map +1 -1
  296. package/dist/tag/index.d.ts +2 -2
  297. package/dist/tag/index.d.ts.map +1 -1
  298. package/dist/textfield/createTextField.d.ts +17 -11
  299. package/dist/textfield/createTextField.d.ts.map +1 -1
  300. package/dist/textfield/index.d.ts +1 -1
  301. package/dist/textfield/index.d.ts.map +1 -1
  302. package/dist/toast/createToast.d.ts +6 -2
  303. package/dist/toast/createToast.d.ts.map +1 -1
  304. package/dist/toast/createToastRegion.d.ts +5 -3
  305. package/dist/toast/createToastRegion.d.ts.map +1 -1
  306. package/dist/toast/index.d.ts +2 -2
  307. package/dist/toast/index.d.ts.map +1 -1
  308. package/dist/toggle/createToggle.d.ts +9 -9
  309. package/dist/toggle/createToggle.d.ts.map +1 -1
  310. package/dist/toggle/createToggleState.d.ts +2 -2
  311. package/dist/toggle/createToggleState.d.ts.map +1 -1
  312. package/dist/toggle/index.d.ts +4 -4
  313. package/dist/toggle/index.d.ts.map +1 -1
  314. package/dist/toolbar/createToolbar.d.ts +9 -9
  315. package/dist/toolbar/createToolbar.d.ts.map +1 -1
  316. package/dist/toolbar/index.d.ts +1 -1
  317. package/dist/toolbar/index.d.ts.map +1 -1
  318. package/dist/tooltip/createTooltip.d.ts +5 -5
  319. package/dist/tooltip/createTooltip.d.ts.map +1 -1
  320. package/dist/tooltip/createTooltipTrigger.d.ts +10 -5
  321. package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
  322. package/dist/tooltip/index.d.ts +2 -2
  323. package/dist/tree/createTree.d.ts +3 -3
  324. package/dist/tree/createTree.d.ts.map +1 -1
  325. package/dist/tree/createTreeItem.d.ts +4 -4
  326. package/dist/tree/createTreeItem.d.ts.map +1 -1
  327. package/dist/tree/createTreeSelectionCheckbox.d.ts +3 -3
  328. package/dist/tree/createTreeSelectionCheckbox.d.ts.map +1 -1
  329. package/dist/tree/index.d.ts +4 -4
  330. package/dist/tree/types.d.ts +13 -5
  331. package/dist/tree/types.d.ts.map +1 -1
  332. package/dist/utils/createDescription.d.ts +2 -2
  333. package/dist/utils/createDescription.d.ts.map +1 -1
  334. package/dist/utils/dom.d.ts.map +1 -1
  335. package/dist/utils/env.d.ts +1 -1
  336. package/dist/utils/env.d.ts.map +1 -1
  337. package/dist/utils/focus.d.ts +1 -1
  338. package/dist/utils/focus.d.ts.map +1 -1
  339. package/dist/utils/geometry.d.ts.map +1 -1
  340. package/dist/utils/index.d.ts +12 -12
  341. package/dist/utils/index.d.ts.map +1 -1
  342. package/dist/utils/mergeProps.d.ts.map +1 -1
  343. package/dist/utils/platform.d.ts.map +1 -1
  344. package/dist/utils/reactivity.d.ts +1 -1
  345. package/dist/visually-hidden/createVisuallyHidden.d.ts +2 -2
  346. package/dist/visually-hidden/createVisuallyHidden.d.ts.map +1 -1
  347. package/dist/visually-hidden/index.d.ts +1 -1
  348. package/package.json +34 -32
  349. package/src/actiongroup/createActionGroup.ts +334 -0
  350. package/src/actiongroup/index.ts +8 -0
  351. package/src/autocomplete/createAutocomplete.ts +137 -131
  352. package/src/autocomplete/index.ts +1 -1
  353. package/src/breadcrumbs/createBreadcrumbs.ts +37 -51
  354. package/src/breadcrumbs/index.ts +1 -1
  355. package/src/button/createButton.ts +102 -73
  356. package/src/button/createToggleButton.ts +10 -10
  357. package/src/button/createToggleButtonGroup.ts +121 -0
  358. package/src/button/index.ts +10 -4
  359. package/src/button/types.ts +18 -12
  360. package/src/calendar/createCalendar.ts +62 -29
  361. package/src/calendar/createCalendarCell.ts +102 -48
  362. package/src/calendar/createCalendarGrid.ts +78 -47
  363. package/src/calendar/createRangeCalendar.ts +66 -31
  364. package/src/calendar/createRangeCalendarCell.ts +115 -37
  365. package/src/calendar/index.ts +5 -9
  366. package/src/calendar/intl/index.ts +210 -0
  367. package/src/calendar/utils.ts +227 -0
  368. package/src/checkbox/createCheckbox.ts +13 -21
  369. package/src/checkbox/createCheckboxGroup.ts +86 -45
  370. package/src/checkbox/createCheckboxGroupItem.ts +16 -27
  371. package/src/checkbox/createCheckboxGroupState.ts +3 -22
  372. package/src/checkbox/index.ts +8 -10
  373. package/src/collections/index.ts +246 -0
  374. package/src/color/createColorArea.ts +458 -314
  375. package/src/color/createColorField.ts +186 -137
  376. package/src/color/createColorSlider.ts +444 -197
  377. package/src/color/createColorSwatch.ts +65 -40
  378. package/src/color/createColorWheel.ts +343 -208
  379. package/src/color/index.ts +24 -24
  380. package/src/color/types.ts +198 -116
  381. package/src/combobox/createComboBox.ts +727 -647
  382. package/src/combobox/index.ts +6 -6
  383. package/src/combobox/intl/index.ts +5 -5
  384. package/src/datepicker/createDateField.ts +192 -39
  385. package/src/datepicker/createDatePicker.ts +294 -63
  386. package/src/datepicker/createDatePickerGroup.ts +149 -0
  387. package/src/datepicker/createDateRangePicker.ts +294 -0
  388. package/src/datepicker/createDateSegment.ts +316 -75
  389. package/src/datepicker/createTimeField.ts +38 -34
  390. package/src/datepicker/createTimeSegment.ts +352 -0
  391. package/src/datepicker/index.ts +24 -11
  392. package/src/dialog/createDialog.ts +127 -120
  393. package/src/dialog/index.ts +2 -2
  394. package/src/dialog/types.ts +19 -19
  395. package/src/disclosure/createDisclosure.ts +138 -33
  396. package/src/disclosure/createDisclosureGroup.ts +8 -18
  397. package/src/disclosure/index.ts +2 -2
  398. package/src/dnd/createDrag.ts +218 -209
  399. package/src/dnd/createDraggableCollection.ts +96 -63
  400. package/src/dnd/createDraggableItem.ts +260 -243
  401. package/src/dnd/createDrop.ts +313 -321
  402. package/src/dnd/createDroppableCollection.ts +799 -293
  403. package/src/dnd/createDroppableItem.ts +215 -213
  404. package/src/dnd/index.ts +66 -47
  405. package/src/dnd/types.ts +86 -89
  406. package/src/dnd/utils.ts +281 -294
  407. package/src/focus/FocusScope.tsx +155 -164
  408. package/src/focus/createAutoFocus.ts +305 -321
  409. package/src/focus/createFocusRestore.ts +300 -313
  410. package/src/focus/createVirtualFocus.ts +380 -396
  411. package/src/focus/index.ts +4 -8
  412. package/src/form/createFormReset.ts +4 -4
  413. package/src/form/createFormValidation.ts +201 -224
  414. package/src/form/index.ts +8 -11
  415. package/src/grid/GridKeyboardDelegate.ts +30 -30
  416. package/src/grid/createGrid.ts +38 -36
  417. package/src/grid/createGridCell.ts +18 -18
  418. package/src/grid/createGridRow.ts +14 -14
  419. package/src/grid/index.ts +5 -5
  420. package/src/grid/types.ts +8 -8
  421. package/src/gridlist/createGridList.ts +45 -24
  422. package/src/gridlist/createGridListItem.ts +68 -23
  423. package/src/gridlist/createGridListSelectionCheckbox.ts +12 -9
  424. package/src/gridlist/index.ts +4 -4
  425. package/src/gridlist/types.ts +11 -7
  426. package/src/i18n/createCollator.ts +66 -79
  427. package/src/i18n/createDateFormatter.ts +75 -83
  428. package/src/i18n/createFilter.ts +118 -131
  429. package/src/i18n/createNumberFormatter.ts +50 -52
  430. package/src/i18n/createStringFormatter.ts +19 -15
  431. package/src/i18n/index.ts +37 -40
  432. package/src/i18n/locale.tsx +163 -188
  433. package/src/i18n/utils.ts +95 -99
  434. package/src/index.ts +114 -164
  435. package/src/interactions/FocusableProvider.tsx +3 -7
  436. package/src/interactions/PressEvent.ts +4 -4
  437. package/src/interactions/createFocus.ts +16 -11
  438. package/src/interactions/createFocusRing.ts +21 -19
  439. package/src/interactions/createFocusWithin.ts +24 -16
  440. package/src/interactions/createFocusable.ts +15 -16
  441. package/src/interactions/createHover.ts +70 -55
  442. package/src/interactions/createInteractionModality.ts +75 -82
  443. package/src/interactions/createKeyboard.ts +2 -2
  444. package/src/interactions/createLongPress.ts +174 -174
  445. package/src/interactions/createMove.ts +299 -289
  446. package/src/interactions/createPress.ts +168 -91
  447. package/src/interactions/index.ts +24 -16
  448. package/src/label/createField.ts +18 -19
  449. package/src/label/createLabel.ts +18 -30
  450. package/src/label/createLabels.ts +8 -12
  451. package/src/label/index.ts +5 -5
  452. package/src/landmark/createLandmark.ts +356 -377
  453. package/src/landmark/index.ts +8 -8
  454. package/src/link/createLink.ts +96 -54
  455. package/src/link/index.ts +1 -1
  456. package/src/listbox/createListBox.ts +319 -269
  457. package/src/listbox/createOption.ts +208 -151
  458. package/src/listbox/index.ts +8 -12
  459. package/src/live-announcer/announce.ts +295 -322
  460. package/src/live-announcer/index.ts +9 -9
  461. package/src/menu/createMenu.ts +434 -396
  462. package/src/menu/createMenuItem.ts +201 -149
  463. package/src/menu/createMenuTrigger.ts +88 -88
  464. package/src/menu/index.ts +9 -18
  465. package/src/meter/createMeter.ts +7 -20
  466. package/src/meter/index.ts +1 -1
  467. package/src/numberfield/createNumberField.ts +368 -268
  468. package/src/numberfield/index.ts +5 -5
  469. package/src/overlays/ariaHideOutside.ts +223 -219
  470. package/src/overlays/createInteractOutside.ts +152 -149
  471. package/src/overlays/createModal.tsx +238 -202
  472. package/src/overlays/createOverlay.ts +195 -155
  473. package/src/overlays/createOverlayTrigger.ts +85 -85
  474. package/src/overlays/createPreventScroll.ts +288 -266
  475. package/src/overlays/index.ts +37 -44
  476. package/src/popover/calculatePosition.ts +117 -119
  477. package/src/popover/createOverlayPosition.ts +52 -43
  478. package/src/popover/createPopover.ts +63 -24
  479. package/src/popover/index.ts +3 -3
  480. package/src/progress/createProgressBar.ts +36 -32
  481. package/src/progress/index.ts +1 -1
  482. package/src/radio/createRadio.ts +95 -73
  483. package/src/radio/createRadioGroup.ts +142 -62
  484. package/src/radio/createRadioGroupState.ts +7 -31
  485. package/src/radio/index.ts +3 -8
  486. package/src/searchfield/createSearchField.ts +269 -186
  487. package/src/searchfield/index.ts +2 -2
  488. package/src/select/createHiddenSelect.tsx +276 -236
  489. package/src/select/createSelect.ts +430 -395
  490. package/src/select/index.ts +9 -14
  491. package/src/selection/createTypeSelect.ts +11 -11
  492. package/src/selection/index.ts +1 -1
  493. package/src/separator/createSeparator.ts +20 -25
  494. package/src/separator/index.ts +1 -1
  495. package/src/slider/createSlider.ts +333 -349
  496. package/src/slider/index.ts +2 -2
  497. package/src/ssr/index.tsx +331 -370
  498. package/src/steplist/createStepList.ts +106 -0
  499. package/src/steplist/index.ts +8 -0
  500. package/src/switch/createSwitch.ts +9 -14
  501. package/src/switch/index.ts +1 -1
  502. package/src/table/createTable.ts +155 -86
  503. package/src/table/createTableCell.ts +17 -16
  504. package/src/table/createTableColumnHeader.ts +67 -20
  505. package/src/table/createTableColumnResize.ts +256 -0
  506. package/src/table/createTableHeaderRow.ts +7 -7
  507. package/src/table/createTableRow.ts +149 -29
  508. package/src/table/createTableRowGroup.ts +5 -7
  509. package/src/table/createTableSelectAllCheckbox.ts +12 -11
  510. package/src/table/createTableSelectionCheckbox.ts +8 -8
  511. package/src/table/index.ts +14 -9
  512. package/src/table/types.ts +15 -7
  513. package/src/tabs/createTabs.ts +138 -127
  514. package/src/tabs/index.ts +1 -1
  515. package/src/tag/createTag.ts +171 -40
  516. package/src/tag/createTagGroup.ts +50 -39
  517. package/src/tag/index.ts +2 -6
  518. package/src/textfield/createTextField.ts +67 -35
  519. package/src/textfield/index.ts +1 -5
  520. package/src/toast/createToast.ts +34 -26
  521. package/src/toast/createToastRegion.ts +169 -27
  522. package/src/toast/index.ts +2 -6
  523. package/src/toggle/createToggle.ts +95 -53
  524. package/src/toggle/createToggleState.ts +2 -10
  525. package/src/toggle/index.ts +4 -5
  526. package/src/toolbar/createToolbar.ts +226 -169
  527. package/src/toolbar/index.ts +1 -1
  528. package/src/tooltip/createTooltip.ts +66 -79
  529. package/src/tooltip/createTooltipTrigger.ts +238 -222
  530. package/src/tooltip/index.ts +6 -6
  531. package/src/tree/createTree.ts +259 -246
  532. package/src/tree/createTreeItem.ts +282 -233
  533. package/src/tree/createTreeSelectionCheckbox.ts +71 -68
  534. package/src/tree/index.ts +16 -16
  535. package/src/tree/types.ts +95 -87
  536. package/src/utils/createDescription.ts +6 -23
  537. package/src/utils/dom.ts +61 -54
  538. package/src/utils/env.ts +53 -54
  539. package/src/utils/events.ts +7 -7
  540. package/src/utils/filterDOMProps.ts +49 -49
  541. package/src/utils/focus.ts +60 -68
  542. package/src/utils/geometry.ts +1 -4
  543. package/src/utils/globalListeners.ts +9 -9
  544. package/src/utils/index.ts +12 -22
  545. package/src/utils/mergeProps.ts +42 -15
  546. package/src/utils/platform.ts +16 -6
  547. package/src/utils/reactivity.ts +3 -3
  548. package/src/utils/textSelection.ts +16 -16
  549. package/src/visually-hidden/createVisuallyHidden.ts +127 -124
  550. package/src/visually-hidden/index.ts +6 -6
  551. package/dist/i18n/NumberFormatter.d.ts +0 -43
  552. package/dist/i18n/NumberFormatter.d.ts.map +0 -1
  553. package/dist/index.ssr.js +0 -15875
  554. package/dist/index.ssr.js.map +0 -7
  555. package/src/i18n/NumberFormatter.ts +0 -266
@@ -1,647 +1,727 @@
1
- /**
2
- * Provides the behavior and accessibility implementation for a combobox component.
3
- * A combobox combines a text input with a listbox, allowing users to filter a list of options.
4
- * Based on @react-aria/combobox useComboBox.
5
- */
6
-
7
- import { type JSX, type Accessor, createEffect, onCleanup } from 'solid-js';
8
- import { isServer } from 'solid-js/web';
9
- import { createPress } from '../interactions/createPress';
10
- import { createFocusRing } from '../interactions/createFocusRing';
11
- import { createLabel } from '../label/createLabel';
12
- import { filterDOMProps } from '../utils/filterDOMProps';
13
- import { mergeProps } from '../utils/mergeProps';
14
- import { createId } from '../ssr';
15
- import { access, type MaybeAccessor } from '../utils/reactivity';
16
- import { isAppleDevice } from '../utils/platform';
17
- import { openLink } from '../utils/dom';
18
- import { ariaHideOutside } from '../overlays/ariaHideOutside';
19
- import { announce } from '../live-announcer';
20
- import { createStringFormatter } from '../i18n';
21
- import { comboBoxIntlStrings } from './intl';
22
- import { isDevEnv } from '../utils/env';
23
- import type { ComboBoxState, CollectionNode, Key } from '@proyecto-viviana/solid-stately';
24
-
25
- /**
26
- * Helper to count items in a collection
27
- */
28
- function getItemCount<T>(collection: { getKeys(): Iterable<Key> }): number {
29
- let count = 0;
30
- for (const _ of collection.getKeys()) {
31
- count++;
32
- }
33
- return count;
34
- }
35
-
36
- export interface AriaComboBoxProps {
37
- /** An ID for the combobox. */
38
- id?: string;
39
- /** Whether the combobox is disabled. */
40
- isDisabled?: boolean;
41
- /** Whether the combobox is required. */
42
- isRequired?: boolean;
43
- /** Whether the combobox is read-only. */
44
- isReadOnly?: boolean;
45
- /** The label for the combobox. */
46
- label?: JSX.Element;
47
- /** An accessible label for the combobox when no visible label is provided. */
48
- 'aria-label'?: string;
49
- /** The ID of an element that labels the combobox. */
50
- 'aria-labelledby'?: string;
51
- /** The ID of an element that describes the combobox. */
52
- 'aria-describedby'?: string;
53
- /** Placeholder text for the input when no value is entered. */
54
- placeholder?: string;
55
- /** Whether the combobox should be auto-focused. */
56
- autoFocus?: boolean;
57
- /** Handler called when focus moves to the combobox input. */
58
- onFocus?: (e: FocusEvent) => void;
59
- /** Handler called when focus moves away from the combobox input. */
60
- onBlur?: (e: FocusEvent) => void;
61
- /** Handler called when the focus state changes. */
62
- onFocusChange?: (isFocused: boolean) => void;
63
- /** The name of the combobox, used when submitting an HTML form. */
64
- name?: string;
65
- /**
66
- * Describes the type of autocomplete functionality the input should provide.
67
- * @default 'list'
68
- */
69
- autoComplete?: 'list' | 'none' | 'inline' | 'both';
70
- /** Whether focus should wrap from the last item to the first. */
71
- shouldFocusWrap?: boolean;
72
- }
73
-
74
- export interface ComboBoxAria<T> {
75
- /** Props for the label element. */
76
- labelProps: JSX.HTMLAttributes<HTMLElement>;
77
- /** Props for the input element. */
78
- inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
79
- /** Props for the trigger button element. */
80
- buttonProps: JSX.HTMLAttributes<HTMLElement>;
81
- /** Props for the listbox popup. */
82
- listBoxProps: JSX.HTMLAttributes<HTMLElement>;
83
- /** Props for the description element, if any. */
84
- descriptionProps: JSX.HTMLAttributes<HTMLElement>;
85
- /** Props for the error message element, if any. */
86
- errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
87
- /** Whether the input is currently focused. */
88
- isFocused: Accessor<boolean>;
89
- /** Whether the input has keyboard focus. */
90
- isFocusVisible: Accessor<boolean>;
91
- /** Whether the listbox is currently open. */
92
- isOpen: Accessor<boolean>;
93
- /** The currently selected item. */
94
- selectedItem: Accessor<CollectionNode<T> | null>;
95
- }
96
-
97
- // Shared data between combobox and options
98
- const comboBoxData = new WeakMap<object, ComboBoxData>();
99
-
100
- interface ComboBoxData {
101
- id: string;
102
- }
103
-
104
- export function getComboBoxData(state: ComboBoxState<unknown>): ComboBoxData | undefined {
105
- return comboBoxData.get(state);
106
- }
107
-
108
- /**
109
- * Provides the behavior and accessibility implementation for a combobox component.
110
- */
111
- export function createComboBox<T>(
112
- props: MaybeAccessor<AriaComboBoxProps>,
113
- state: ComboBoxState<T>,
114
- inputRef: () => HTMLInputElement | null,
115
- buttonRef?: () => HTMLElement | null,
116
- listBoxRef?: () => HTMLElement | null
117
- ): ComboBoxAria<T> {
118
- const getProps = () => access(props);
119
- const id = createId(getProps().id);
120
-
121
- // Development-time warning for missing accessibility labels
122
- if (isDevEnv()) {
123
- const p = getProps();
124
- if (!p.label && !p['aria-label'] && !p['aria-labelledby']) {
125
- console.warn(
126
- '[solidaria] A ComboBox requires a label, aria-label, or aria-labelledby attribute for accessibility.'
127
- );
128
- }
129
- }
130
-
131
- // Track if a pointerdown happened inside the listbox to prevent blur from closing
132
- let isPointerDownInsideListBox = false;
133
-
134
- // Generate IDs for associated elements
135
- const inputId = `${id}-input`;
136
- const buttonId = `${id}-button`;
137
- const listBoxId = `${id}-listbox`;
138
- const descriptionId = `${id}-description`;
139
- const errorMessageId = `${id}-error`;
140
-
141
- // Set up global pointerdown listener to track clicks inside listbox
142
- // This is needed because the option's createPress stops propagation
143
- createEffect(() => {
144
- if (typeof document === 'undefined') return;
145
-
146
- const handleGlobalPointerDown = (e: PointerEvent) => {
147
- const target = e.target as HTMLElement;
148
- // Check if the click is inside the listbox
149
- if (target.closest(`[id="${listBoxId}"]`)) {
150
- isPointerDownInsideListBox = true;
151
- }
152
- };
153
-
154
- document.addEventListener('pointerdown', handleGlobalPointerDown, true);
155
-
156
- onCleanup(() => {
157
- document.removeEventListener('pointerdown', handleGlobalPointerDown, true);
158
- });
159
- });
160
-
161
- // Filter DOM props
162
- const domProps = () =>
163
- filterDOMProps(getProps() as unknown as Record<string, unknown>, { labelable: true });
164
-
165
- // Share data with child options
166
- createEffect(() => {
167
- comboBoxData.set(state, { id });
168
-
169
- onCleanup(() => {
170
- comboBoxData.delete(state);
171
- });
172
- });
173
-
174
- // Label handling
175
- const { labelProps, fieldProps } = createLabel({
176
- get id() {
177
- return inputId;
178
- },
179
- get label() {
180
- return getProps().label;
181
- },
182
- get 'aria-label'() {
183
- return getProps()['aria-label'];
184
- },
185
- get 'aria-labelledby'() {
186
- return getProps()['aria-labelledby'];
187
- },
188
- labelElementType: 'label',
189
- });
190
-
191
- // Focus ring for keyboard focus styling
192
- const { isFocusVisible, focusProps } = createFocusRing({
193
- get autoFocus() {
194
- return getProps().autoFocus;
195
- },
196
- });
197
-
198
- // Track focus state from state
199
- const isFocused = state.isFocused;
200
-
201
- // String formatter for VoiceOver announcements
202
- // Only create on client side
203
- const stringFormatter = !isServer ? createStringFormatter(comboBoxIntlStrings) : null;
204
-
205
- // Track previous values for announcements
206
- let lastFocusedKey: Key | null = null;
207
- let lastSelectedKey: Key | null = null;
208
- let lastOptionCount = 0;
209
- let lastIsOpen = false;
210
-
211
- // VoiceOver has issues with announcing aria-activedescendant properly on change
212
- // (especially on iOS). We use a live region announcer to announce focus changes
213
- // manually. This matches React Aria's behavior.
214
- createEffect(() => {
215
- if (isServer || !stringFormatter) return;
216
-
217
- const focusedKey = state.focusedKey();
218
- const isOpen = state.isOpen();
219
- const collection = state.collection();
220
-
221
- // Get the focused item
222
- const focusedItem = focusedKey != null && isOpen
223
- ? collection.getItem(focusedKey)
224
- : null;
225
-
226
- // Announce focus changes on Apple devices
227
- if (isAppleDevice() && focusedItem != null && focusedKey !== lastFocusedKey) {
228
- const isSelected = state.selectedKey() === focusedKey;
229
- const optionText = focusedItem.textValue || '';
230
-
231
- // For now, we don't support sections, so isGroupChange is always false
232
- const announcement = stringFormatter().format('focusAnnouncement', {
233
- isGroupChange: false,
234
- groupTitle: '',
235
- groupCount: 0,
236
- optionText,
237
- isSelected,
238
- });
239
-
240
- announce(announcement, 'polite');
241
- }
242
-
243
- lastFocusedKey = focusedKey;
244
- });
245
-
246
- // Announce the number of available suggestions when it changes
247
- createEffect(() => {
248
- if (isServer || !stringFormatter) return;
249
-
250
- const isOpen = state.isOpen();
251
- const collection = state.collection();
252
- const optionCount = getItemCount(collection);
253
- const focusedKey = state.focusedKey();
254
-
255
- // Only announce the number of options available when the menu opens if there is no
256
- // focused item, otherwise screen readers will typically read e.g. "1 of 6".
257
- // The exception is VoiceOver since this isn't included in the message above.
258
- const didOpenWithoutFocusedItem =
259
- isOpen !== lastIsOpen &&
260
- (focusedKey == null || isAppleDevice());
261
-
262
- if (isOpen && (didOpenWithoutFocusedItem || optionCount !== lastOptionCount)) {
263
- const announcement = stringFormatter().format('countAnnouncement', { optionCount });
264
- announce(announcement, 'polite');
265
- }
266
-
267
- lastOptionCount = optionCount;
268
- lastIsOpen = isOpen;
269
- });
270
-
271
- // Announce when a selection occurs for VoiceOver. Other screen readers typically do this automatically.
272
- createEffect(() => {
273
- if (isServer || !stringFormatter) return;
274
-
275
- const selectedKey = state.selectedKey();
276
- const selectedItem = state.selectedItem();
277
-
278
- if (isAppleDevice() && state.isFocused() && selectedItem && selectedKey !== lastSelectedKey) {
279
- const optionText = selectedItem.textValue || '';
280
- const announcement = stringFormatter().format('selectedAnnouncement', { optionText });
281
- announce(announcement, 'polite');
282
- }
283
-
284
- lastSelectedKey = selectedKey;
285
- });
286
-
287
- // Hide other page content from screen readers when the listbox is open.
288
- // This requires both the input and listbox refs to be available.
289
- // Note: This feature is important for screen reader accessibility but
290
- // only works when a popoverRef/listBoxRef is provided.
291
- createEffect(() => {
292
- if (isServer) return;
293
-
294
- const isOpen = state.isOpen();
295
- const inputEl = inputRef();
296
- const listBoxEl = listBoxRef?.();
297
-
298
- // Only apply ariaHideOutside if we have both elements available
299
- // This ensures the listbox won't be accidentally hidden
300
- if (isOpen && inputEl && listBoxEl) {
301
- const cleanup = ariaHideOutside([inputEl, listBoxEl]);
302
- onCleanup(cleanup);
303
- }
304
- });
305
-
306
- // Handle press on button trigger
307
- const { pressProps } = createPress({
308
- get isDisabled() {
309
- return getProps().isDisabled ?? state.isDisabled;
310
- },
311
- onPress() {
312
- state.toggle(null, 'manual');
313
- // Focus input after toggling
314
- inputRef()?.focus();
315
- },
316
- });
317
-
318
- // Handle input change
319
- const onInputChange: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => {
320
- const target = e.target as HTMLInputElement;
321
- state.setInputValue(target.value);
322
- };
323
-
324
- // Keyboard navigation for input
325
- const onInputKeyDown: JSX.EventHandler<HTMLInputElement, KeyboardEvent> = (e) => {
326
- const p = getProps();
327
- if (p.isDisabled || p.isReadOnly) return;
328
-
329
- const collection = state.collection();
330
- const focusedKey = state.focusedKey();
331
- const shouldWrap = p.shouldFocusWrap ?? false;
332
-
333
- switch (e.key) {
334
- case 'Enter':
335
- if (state.isOpen() && focusedKey != null) {
336
- e.preventDefault();
337
-
338
- // Check if the focused item is a link
339
- // Link href can be in props (for components) or value (for dynamic items)
340
- const collectionItem = collection.getItem(focusedKey);
341
- const itemHref = collectionItem?.props?.href ?? (collectionItem?.value as Record<string, unknown> | null)?.href;
342
- if (itemHref) {
343
- // Find the actual anchor element in the DOM and trigger navigation
344
- const listBox = listBoxRef?.();
345
- if (listBox) {
346
- const item = listBox.querySelector(
347
- `[data-key="${CSS.escape(String(focusedKey))}"]`
348
- );
349
- if (item instanceof HTMLAnchorElement) {
350
- openLink(item, e);
351
- }
352
- }
353
- state.close();
354
- } else {
355
- state.commit();
356
- }
357
- }
358
- break;
359
-
360
- case 'Escape':
361
- if (state.isOpen()) {
362
- e.preventDefault();
363
- e.stopPropagation();
364
- state.revert();
365
- }
366
- break;
367
-
368
- case 'ArrowDown':
369
- e.preventDefault();
370
- if (!state.isOpen()) {
371
- state.open('first', 'manual');
372
- } else {
373
- // Move to next item
374
- if (focusedKey == null) {
375
- const firstKey = collection.getFirstKey();
376
- if (firstKey != null) {
377
- state.setFocusedKey(firstKey);
378
- }
379
- } else {
380
- let nextKey = collection.getKeyAfter(focusedKey);
381
- // Skip disabled keys
382
- while (nextKey != null && state.isKeyDisabled(nextKey)) {
383
- nextKey = collection.getKeyAfter(nextKey);
384
- }
385
- if (nextKey != null) {
386
- state.setFocusedKey(nextKey);
387
- } else if (shouldWrap) {
388
- // Wrap to first
389
- let firstKey = collection.getFirstKey();
390
- while (firstKey != null && state.isKeyDisabled(firstKey)) {
391
- firstKey = collection.getKeyAfter(firstKey);
392
- }
393
- if (firstKey != null) {
394
- state.setFocusedKey(firstKey);
395
- }
396
- }
397
- }
398
- }
399
- break;
400
-
401
- case 'ArrowUp':
402
- e.preventDefault();
403
- if (!state.isOpen()) {
404
- state.open('last', 'manual');
405
- } else {
406
- // Move to previous item
407
- if (focusedKey == null) {
408
- const lastKey = collection.getLastKey();
409
- if (lastKey != null) {
410
- state.setFocusedKey(lastKey);
411
- }
412
- } else {
413
- let prevKey = collection.getKeyBefore(focusedKey);
414
- // Skip disabled keys
415
- while (prevKey != null && state.isKeyDisabled(prevKey)) {
416
- prevKey = collection.getKeyBefore(prevKey);
417
- }
418
- if (prevKey != null) {
419
- state.setFocusedKey(prevKey);
420
- } else if (shouldWrap) {
421
- // Wrap to last
422
- let lastKey = collection.getLastKey();
423
- while (lastKey != null && state.isKeyDisabled(lastKey)) {
424
- lastKey = collection.getKeyBefore(lastKey);
425
- }
426
- if (lastKey != null) {
427
- state.setFocusedKey(lastKey);
428
- }
429
- }
430
- }
431
- }
432
- break;
433
-
434
- case 'Home':
435
- if (state.isOpen()) {
436
- e.preventDefault();
437
- let firstKey = collection.getFirstKey();
438
- while (firstKey != null && state.isKeyDisabled(firstKey)) {
439
- firstKey = collection.getKeyAfter(firstKey);
440
- }
441
- if (firstKey != null) {
442
- state.setFocusedKey(firstKey);
443
- }
444
- }
445
- break;
446
-
447
- case 'End':
448
- if (state.isOpen()) {
449
- e.preventDefault();
450
- let lastKey = collection.getLastKey();
451
- while (lastKey != null && state.isKeyDisabled(lastKey)) {
452
- lastKey = collection.getKeyBefore(lastKey);
453
- }
454
- if (lastKey != null) {
455
- state.setFocusedKey(lastKey);
456
- }
457
- }
458
- break;
459
-
460
- case 'Tab':
461
- // Commit on Tab if menu is open
462
- if (state.isOpen() && focusedKey != null) {
463
- state.commit();
464
- }
465
- break;
466
- }
467
- };
468
-
469
- // Handle focus events
470
- const handleFocus = (e: FocusEvent) => {
471
- state.setFocused(true);
472
- getProps().onFocus?.(e);
473
- getProps().onFocusChange?.(true);
474
- };
475
-
476
- // Track the last touch event time for iPad VoiceOver double-tap debouncing
477
- let lastEventTime = 0;
478
-
479
- const handleBlur = (e: FocusEvent) => {
480
- // Use synchronous ref checks instead of requestAnimationFrame
481
- // This matches React Aria's implementation and is more reliable
482
- const relatedTarget = e.relatedTarget as HTMLElement | null;
483
- const button = buttonRef?.();
484
- const listBox = listBoxRef?.();
485
-
486
- // Don't blur if focus is moving to the button
487
- const blurFromButton = button && button === relatedTarget;
488
-
489
- // Don't blur if focus is moving into the listbox/popover
490
- const blurIntoPopover = listBox?.contains(relatedTarget);
491
-
492
- if (blurFromButton || blurIntoPopover) {
493
- return;
494
- }
495
-
496
- // If a pointerdown happened inside the listbox, don't close
497
- // This handles the case when clicking on a non-focusable option
498
- if (isPointerDownInsideListBox) {
499
- isPointerDownInsideListBox = false;
500
- return;
501
- }
502
-
503
- // Call user's onBlur handler
504
- getProps().onBlur?.(e);
505
-
506
- state.setFocused(false);
507
- getProps().onFocusChange?.(false);
508
- };
509
-
510
- // Handle touch events for iPad VoiceOver
511
- // VoiceOver on iOS fires a touchend at the center of the element on double-tap.
512
- // We detect this and toggle the combobox manually to avoid issues with focus management.
513
- const handleTouchEnd = (e: TouchEvent) => {
514
- const p = getProps();
515
- const isDisabled = p.isDisabled ?? state.isDisabled;
516
- const isReadOnly = p.isReadOnly ?? state.isReadOnly;
517
-
518
- if (isDisabled || isReadOnly) {
519
- return;
520
- }
521
-
522
- // Debounce rapid consecutive touchend events (< 500ms)
523
- // This handles VoiceOver's double-tap behavior
524
- if (e.timeStamp - lastEventTime < 500) {
525
- e.preventDefault();
526
- inputRef()?.focus();
527
- return;
528
- }
529
-
530
- // Detect VoiceOver virtual click - it fires at the exact center of the element
531
- const rect = (e.target as Element).getBoundingClientRect();
532
- const touch = e.changedTouches[0];
533
- const centerX = Math.ceil(rect.left + 0.5 * rect.width);
534
- const centerY = Math.ceil(rect.top + 0.5 * rect.height);
535
-
536
- if (touch.clientX === centerX && touch.clientY === centerY) {
537
- e.preventDefault();
538
- inputRef()?.focus();
539
- state.toggle(null, 'manual');
540
- lastEventTime = e.timeStamp;
541
- }
542
- };
543
-
544
- return {
545
- get labelProps() {
546
- return labelProps as JSX.HTMLAttributes<HTMLElement>;
547
- },
548
- get inputProps() {
549
- const p = getProps();
550
- const isOpen = state.isOpen();
551
- const isDisabled = p.isDisabled ?? state.isDisabled;
552
- const isReadOnly = p.isReadOnly ?? state.isReadOnly;
553
- const focusedKey = state.focusedKey();
554
-
555
- return mergeProps(
556
- domProps(),
557
- focusProps as Record<string, unknown>,
558
- fieldProps as Record<string, unknown>,
559
- {
560
- id: inputId,
561
- type: 'text',
562
- role: 'combobox',
563
- get value() {
564
- return state.inputValue();
565
- },
566
- tabIndex: isDisabled ? undefined : 0,
567
- disabled: isDisabled || undefined,
568
- readOnly: isReadOnly || undefined,
569
- placeholder: p.placeholder,
570
- autoComplete: 'off',
571
- 'aria-autocomplete': p.autoComplete ?? 'list',
572
- 'aria-haspopup': 'listbox',
573
- 'aria-expanded': isOpen,
574
- 'aria-controls': isOpen ? listBoxId : undefined,
575
- 'aria-activedescendant': isOpen && focusedKey != null
576
- ? `${listBoxId}-option-${focusedKey}`
577
- : undefined,
578
- 'aria-disabled': isDisabled || undefined,
579
- 'aria-required': p.isRequired || undefined,
580
- 'aria-describedby': p['aria-describedby'] || undefined,
581
- name: p.name,
582
- onInput: onInputChange,
583
- onKeyDown: onInputKeyDown,
584
- onFocus: handleFocus,
585
- onBlur: handleBlur,
586
- onTouchEnd: handleTouchEnd,
587
- 'data-open': isOpen || undefined,
588
- 'data-disabled': isDisabled || undefined,
589
- 'data-readonly': isReadOnly || undefined,
590
- 'data-focus-visible': isFocusVisible() || undefined,
591
- } as Record<string, unknown>
592
- ) as JSX.InputHTMLAttributes<HTMLInputElement>;
593
- },
594
- get buttonProps() {
595
- const p = getProps();
596
- const isOpen = state.isOpen();
597
- const isDisabled = p.isDisabled ?? state.isDisabled;
598
-
599
- return mergeProps(
600
- pressProps as Record<string, unknown>,
601
- {
602
- id: buttonId,
603
- type: 'button',
604
- tabIndex: -1,
605
- 'aria-haspopup': 'listbox',
606
- 'aria-expanded': isOpen,
607
- 'aria-controls': isOpen ? listBoxId : undefined,
608
- 'aria-disabled': isDisabled || undefined,
609
- 'aria-label': stringFormatter?.().format('buttonLabel') ?? 'Show suggestions',
610
- 'data-open': isOpen || undefined,
611
- 'data-disabled': isDisabled || undefined,
612
- } as Record<string, unknown>
613
- ) as JSX.HTMLAttributes<HTMLElement>;
614
- },
615
- get listBoxProps() {
616
- return {
617
- id: listBoxId,
618
- role: 'listbox',
619
- 'aria-labelledby': inputId,
620
- tabIndex: -1,
621
- // Track pointerdown inside listbox to prevent blur from closing
622
- // Use capture phase because createPress calls stopPropagation on pointerdown
623
- onPointerDownCapture: () => {
624
- isPointerDownInsideListBox = true;
625
- },
626
- onMouseDownCapture: () => {
627
- // Fallback for environments without PointerEvent
628
- isPointerDownInsideListBox = true;
629
- },
630
- } as JSX.HTMLAttributes<HTMLElement>;
631
- },
632
- get descriptionProps() {
633
- return {
634
- id: descriptionId,
635
- } as JSX.HTMLAttributes<HTMLElement>;
636
- },
637
- get errorMessageProps() {
638
- return {
639
- id: errorMessageId,
640
- } as JSX.HTMLAttributes<HTMLElement>;
641
- },
642
- isFocused,
643
- isFocusVisible: () => isFocused() && isFocusVisible(),
644
- isOpen: state.isOpen,
645
- selectedItem: state.selectedItem,
646
- };
647
- }
1
+ /**
2
+ * Provides the behavior and accessibility implementation for a combobox component.
3
+ * A combobox combines a text input with a listbox, allowing users to filter a list of options.
4
+ * Based on @react-aria/combobox useComboBox.
5
+ */
6
+
7
+ import { type JSX, type Accessor, createEffect, onCleanup } from "solid-js";
8
+ import { isServer } from "solid-js/web";
9
+ import { createPress } from "../interactions/createPress";
10
+ import { createFocusRing } from "../interactions/createFocusRing";
11
+ import { createLabel } from "../label/createLabel";
12
+ import { filterDOMProps } from "../utils/filterDOMProps";
13
+ import { mergeProps } from "../utils/mergeProps";
14
+ import { createId } from "../ssr";
15
+ import { access, type MaybeAccessor } from "../utils/reactivity";
16
+ import { isAppleDevice } from "../utils/platform";
17
+ import { openLink } from "../utils/dom";
18
+ import { ariaHideOutside } from "../overlays/ariaHideOutside";
19
+ import { announce } from "../live-announcer";
20
+ import { createStringFormatter } from "../i18n";
21
+ import { comboBoxIntlStrings } from "./intl";
22
+ import { isDevEnv } from "../utils/env";
23
+ import type { ComboBoxState, CollectionNode, Key } from "@proyecto-viviana/solid-stately";
24
+
25
+ /**
26
+ * Helper to count items in a collection
27
+ */
28
+ function getItemCount<T>(collection: { getKeys(): Iterable<Key> }): number {
29
+ let count = 0;
30
+ for (const _ of collection.getKeys()) {
31
+ count++;
32
+ }
33
+ return count;
34
+ }
35
+
36
+ export interface AriaComboBoxProps {
37
+ /** An ID for the combobox. */
38
+ id?: string;
39
+ /** Whether the combobox is disabled. */
40
+ isDisabled?: boolean;
41
+ /** Whether the combobox is required. */
42
+ isRequired?: boolean;
43
+ /** Whether the combobox is read-only. */
44
+ isReadOnly?: boolean;
45
+ /** The label for the combobox. */
46
+ label?: JSX.Element;
47
+ /** An accessible label for the combobox when no visible label is provided. */
48
+ "aria-label"?: string;
49
+ /** The ID of an element that labels the combobox. */
50
+ "aria-labelledby"?: string;
51
+ /** The ID of an element that describes the combobox. */
52
+ "aria-describedby"?: string;
53
+ /** Description text for assistive technology and form help. */
54
+ description?: string;
55
+ /** Error message text for assistive technology and validation feedback. */
56
+ errorMessage?: string;
57
+ /** Whether the current value is invalid. */
58
+ isInvalid?: boolean;
59
+ /** Placeholder text for the input when no value is entered. */
60
+ placeholder?: string;
61
+ /** Whether the combobox should be auto-focused. */
62
+ autoFocus?: boolean;
63
+ /** Handler called when focus moves to the combobox input. */
64
+ onFocus?: (e: FocusEvent) => void;
65
+ /** Handler called when focus moves away from the combobox input. */
66
+ onBlur?: (e: FocusEvent) => void;
67
+ /** Handler called when the focus state changes. */
68
+ onFocusChange?: (isFocused: boolean) => void;
69
+ /** The name of the combobox, used when submitting an HTML form. */
70
+ name?: string;
71
+ /** The form owner for the combobox input. */
72
+ form?: string;
73
+ /**
74
+ * Describes the type of autocomplete functionality the input should provide.
75
+ * @default 'list'
76
+ */
77
+ autoComplete?: "list" | "none" | "inline" | "both";
78
+ /** Whether focus should wrap from the last item to the first. */
79
+ shouldFocusWrap?: boolean;
80
+ }
81
+
82
+ export interface ComboBoxAria<T> {
83
+ /** Props for the label element. */
84
+ labelProps: JSX.HTMLAttributes<HTMLElement>;
85
+ /** Props for the input element. */
86
+ inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
87
+ /** Props for the trigger button element. */
88
+ buttonProps: JSX.HTMLAttributes<HTMLElement>;
89
+ /** Props for the listbox popup. */
90
+ listBoxProps: JSX.HTMLAttributes<HTMLElement>;
91
+ /** Props for the description element, if any. */
92
+ descriptionProps: JSX.HTMLAttributes<HTMLElement>;
93
+ /** Props for the error message element, if any. */
94
+ errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
95
+ /** Whether the input is currently focused. */
96
+ isFocused: Accessor<boolean>;
97
+ /** Whether the input has keyboard focus. */
98
+ isFocusVisible: Accessor<boolean>;
99
+ /** Whether the listbox is currently open. */
100
+ isOpen: Accessor<boolean>;
101
+ /** The currently selected item. */
102
+ selectedItem: Accessor<CollectionNode<T> | null>;
103
+ }
104
+
105
+ // Shared data between combobox and options
106
+ const comboBoxData = new WeakMap<object, ComboBoxData>();
107
+
108
+ interface ComboBoxData {
109
+ id: string;
110
+ listBoxId: string;
111
+ }
112
+
113
+ export function getComboBoxData(state: ComboBoxState<unknown>): ComboBoxData | undefined {
114
+ return comboBoxData.get(state);
115
+ }
116
+
117
+ /**
118
+ * Provides the behavior and accessibility implementation for a combobox component.
119
+ */
120
+ export function createComboBox<T>(
121
+ props: MaybeAccessor<AriaComboBoxProps>,
122
+ state: ComboBoxState<T>,
123
+ inputRef: () => HTMLInputElement | null,
124
+ buttonRef?: () => HTMLElement | null,
125
+ listBoxRef?: () => HTMLElement | null,
126
+ ): ComboBoxAria<T> {
127
+ const getProps = () => access(props);
128
+ const id = createId(getProps().id);
129
+
130
+ // Development-time warning for missing accessibility labels
131
+ if (isDevEnv()) {
132
+ const p = getProps();
133
+ if (!p.label && !p["aria-label"] && !p["aria-labelledby"]) {
134
+ console.warn(
135
+ "[solidaria] A ComboBox requires a label, aria-label, or aria-labelledby attribute for accessibility.",
136
+ );
137
+ }
138
+ }
139
+
140
+ // Track if a pointerdown happened inside the listbox to prevent blur from closing
141
+ let isPointerDownInsideListBox = false;
142
+
143
+ // Generate IDs for associated elements
144
+ const inputId = `${id}-input`;
145
+ const buttonId = `${id}-button`;
146
+ const listBoxId = `${id}-listbox`;
147
+ const descriptionId = `${id}-description`;
148
+ const errorMessageId = `${id}-error`;
149
+
150
+ const getAriaDescribedBy = () => {
151
+ const p = getProps();
152
+ const ids: string[] = [];
153
+ if (p["aria-describedby"]) {
154
+ ids.push(p["aria-describedby"]);
155
+ }
156
+ if (p.description) {
157
+ ids.push(descriptionId);
158
+ }
159
+ if (p.isInvalid && p.errorMessage) {
160
+ ids.push(errorMessageId);
161
+ }
162
+ return ids.length > 0 ? ids.join(" ") : undefined;
163
+ };
164
+
165
+ // Set up global pointerdown listener to track clicks inside listbox
166
+ // This is needed because the option's createPress stops propagation
167
+ createEffect(() => {
168
+ if (typeof document === "undefined") return;
169
+
170
+ const handleGlobalPointerDown = (e: PointerEvent) => {
171
+ const target = e.target as HTMLElement;
172
+ // Check if the click is inside the listbox
173
+ if (target.closest(`[id="${listBoxId}"]`)) {
174
+ isPointerDownInsideListBox = true;
175
+ }
176
+ };
177
+
178
+ document.addEventListener("pointerdown", handleGlobalPointerDown, true);
179
+
180
+ onCleanup(() => {
181
+ document.removeEventListener("pointerdown", handleGlobalPointerDown, true);
182
+ });
183
+ });
184
+
185
+ // Filter DOM props
186
+ const domProps = () =>
187
+ filterDOMProps(getProps() as unknown as Record<string, unknown>, {
188
+ labelable: true,
189
+ propNames: new Set(["form"]),
190
+ });
191
+
192
+ // Share data with child options
193
+ createEffect(() => {
194
+ comboBoxData.set(state, { id, listBoxId });
195
+
196
+ onCleanup(() => {
197
+ comboBoxData.delete(state);
198
+ });
199
+ });
200
+
201
+ // Label handling
202
+ const { labelProps, fieldProps } = createLabel({
203
+ get id() {
204
+ return inputId;
205
+ },
206
+ get label() {
207
+ return getProps().label;
208
+ },
209
+ get "aria-label"() {
210
+ return getProps()["aria-label"];
211
+ },
212
+ get "aria-labelledby"() {
213
+ return getProps()["aria-labelledby"];
214
+ },
215
+ labelElementType: "label",
216
+ });
217
+
218
+ // Focus ring for keyboard focus styling
219
+ const { isFocusVisible, focusProps } = createFocusRing({
220
+ get autoFocus() {
221
+ return getProps().autoFocus;
222
+ },
223
+ });
224
+
225
+ // Track focus state from state
226
+ const isFocused = state.isFocused;
227
+
228
+ // String formatter for VoiceOver announcements
229
+ // Only create on client side
230
+ const stringFormatter = !isServer ? createStringFormatter(comboBoxIntlStrings) : null;
231
+
232
+ // Track previous values for announcements
233
+ let lastFocusedKey: Key | null = null;
234
+ let lastSelectedKey: Key | null = null;
235
+ let lastOptionCount = 0;
236
+ let lastIsOpen = false;
237
+
238
+ // VoiceOver has issues with announcing aria-activedescendant properly on change
239
+ // (especially on iOS). We use a live region announcer to announce focus changes
240
+ // manually. This matches React Aria's behavior.
241
+ createEffect(() => {
242
+ if (isServer || !stringFormatter) return;
243
+
244
+ const focusedKey = state.focusedKey();
245
+ const isOpen = state.isOpen();
246
+ const collection = state.collection();
247
+
248
+ // Get the focused item
249
+ const focusedItem = focusedKey != null && isOpen ? collection.getItem(focusedKey) : null;
250
+
251
+ // Announce focus changes on Apple devices
252
+ if (isAppleDevice() && focusedItem != null && focusedKey !== lastFocusedKey) {
253
+ const isSelected = state.selectedKey() === focusedKey;
254
+ const optionText = focusedItem.textValue || "";
255
+
256
+ // For now, we don't support sections, so isGroupChange is always false
257
+ const announcement = stringFormatter().format("focusAnnouncement", {
258
+ isGroupChange: false,
259
+ groupTitle: "",
260
+ groupCount: 0,
261
+ optionText,
262
+ isSelected,
263
+ });
264
+
265
+ announce(announcement, "polite");
266
+ }
267
+
268
+ lastFocusedKey = focusedKey;
269
+ });
270
+
271
+ // Announce the number of available suggestions when it changes
272
+ createEffect(() => {
273
+ if (isServer || !stringFormatter) return;
274
+
275
+ const isOpen = state.isOpen();
276
+ const collection = state.collection();
277
+ const optionCount = getItemCount(collection);
278
+ const focusedKey = state.focusedKey();
279
+
280
+ // Only announce the number of options available when the menu opens if there is no
281
+ // focused item, otherwise screen readers will typically read e.g. "1 of 6".
282
+ // The exception is VoiceOver since this isn't included in the message above.
283
+ const didOpenWithoutFocusedItem =
284
+ isOpen !== lastIsOpen && (focusedKey == null || isAppleDevice());
285
+
286
+ if (isOpen && (didOpenWithoutFocusedItem || optionCount !== lastOptionCount)) {
287
+ const announcement = stringFormatter().format("countAnnouncement", { optionCount });
288
+ announce(announcement, "polite");
289
+ }
290
+
291
+ lastOptionCount = optionCount;
292
+ lastIsOpen = isOpen;
293
+ });
294
+
295
+ // Announce when a selection occurs for VoiceOver. Other screen readers typically do this automatically.
296
+ createEffect(() => {
297
+ if (isServer || !stringFormatter) return;
298
+
299
+ const selectedKey = state.selectedKey();
300
+ const selectedItem = state.selectedItem();
301
+
302
+ if (isAppleDevice() && state.isFocused() && selectedItem && selectedKey !== lastSelectedKey) {
303
+ const optionText = selectedItem.textValue || "";
304
+ const announcement = stringFormatter().format("selectedAnnouncement", { optionText });
305
+ announce(announcement, "polite");
306
+ }
307
+
308
+ lastSelectedKey = selectedKey;
309
+ });
310
+
311
+ // Hide other page content from screen readers when the listbox is open.
312
+ // This requires both the input and listbox refs to be available.
313
+ // Note: This feature is important for screen reader accessibility but
314
+ // only works when a popoverRef/listBoxRef is provided.
315
+ createEffect(() => {
316
+ if (isServer) return;
317
+
318
+ const isOpen = state.isOpen();
319
+ const inputEl = inputRef();
320
+ const listBoxEl = listBoxRef?.();
321
+
322
+ // Only apply ariaHideOutside if we have both elements available
323
+ // This ensures the listbox won't be accidentally hidden
324
+ if (isOpen && inputEl && listBoxEl) {
325
+ const cleanup = ariaHideOutside([inputEl, listBoxEl]);
326
+ onCleanup(cleanup);
327
+ }
328
+ });
329
+
330
+ // Handle press on button trigger
331
+ let wasOpenOnButtonPressStart = false;
332
+ const scheduleButtonOpenFallback = (startedOpen: boolean) => {
333
+ if (startedOpen) {
334
+ return;
335
+ }
336
+
337
+ setTimeout(() => {
338
+ const p = getProps();
339
+ const isDisabled = p.isDisabled ?? state.isDisabled;
340
+ const isReadOnly = p.isReadOnly ?? state.isReadOnly;
341
+ if (!state.isOpen() && !isDisabled && !isReadOnly) {
342
+ inputRef()?.focus();
343
+ state.open(null, "manual");
344
+ }
345
+ }, 0);
346
+ };
347
+
348
+ const { pressProps } = createPress({
349
+ get isDisabled() {
350
+ return (
351
+ (getProps().isDisabled ?? state.isDisabled) || (getProps().isReadOnly ?? state.isReadOnly)
352
+ );
353
+ },
354
+ preventFocusOnPress: true,
355
+ onPressStart(e) {
356
+ if (e.pointerType === "touch") {
357
+ return;
358
+ }
359
+
360
+ wasOpenOnButtonPressStart = state.isOpen();
361
+ inputRef()?.focus();
362
+ state.toggle(
363
+ e.pointerType === "keyboard" || e.pointerType === "virtual" ? "first" : null,
364
+ "manual",
365
+ );
366
+ scheduleButtonOpenFallback(wasOpenOnButtonPressStart);
367
+ },
368
+ onPress(e) {
369
+ if (e.pointerType !== "touch") {
370
+ return;
371
+ }
372
+
373
+ wasOpenOnButtonPressStart = state.isOpen();
374
+ inputRef()?.focus();
375
+ state.toggle(null, "manual");
376
+ scheduleButtonOpenFallback(wasOpenOnButtonPressStart);
377
+ },
378
+ });
379
+
380
+ // Handle input change
381
+ const onInputChange: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => {
382
+ const target = e.target as HTMLInputElement;
383
+ state.setInputValue(target.value);
384
+ };
385
+
386
+ // Keyboard navigation for input
387
+ const onInputKeyDown: JSX.EventHandler<HTMLInputElement, KeyboardEvent> = (e) => {
388
+ const p = getProps();
389
+ if (p.isDisabled || p.isReadOnly) return;
390
+
391
+ const collection = state.collection();
392
+ const focusedKey = state.focusedKey();
393
+ const shouldWrap = p.shouldFocusWrap ?? false;
394
+
395
+ switch (e.key) {
396
+ case "Enter":
397
+ if (state.isOpen() && focusedKey != null) {
398
+ e.preventDefault();
399
+
400
+ // Check if the focused item is a link
401
+ // Link href can be in props (for components) or value (for dynamic items)
402
+ const collectionItem = collection.getItem(focusedKey);
403
+ const itemHref =
404
+ collectionItem?.props?.href ??
405
+ (collectionItem?.value as Record<string, unknown> | null)?.href;
406
+ if (itemHref) {
407
+ // Find the actual anchor element in the DOM and trigger navigation
408
+ const listBox = listBoxRef?.();
409
+ if (listBox) {
410
+ const item = listBox.querySelector(`[data-key="${CSS.escape(String(focusedKey))}"]`);
411
+ if (item instanceof HTMLAnchorElement) {
412
+ openLink(item, e);
413
+ }
414
+ }
415
+ state.close();
416
+ } else {
417
+ state.commit();
418
+ }
419
+ }
420
+ break;
421
+
422
+ case "Escape":
423
+ if (state.isOpen()) {
424
+ e.preventDefault();
425
+ e.stopPropagation();
426
+ state.revert();
427
+ }
428
+ break;
429
+
430
+ case "ArrowDown":
431
+ e.preventDefault();
432
+ if (!state.isOpen()) {
433
+ state.open("first", "manual");
434
+ } else {
435
+ // Move to next item
436
+ if (focusedKey == null) {
437
+ const firstKey = collection.getFirstKey();
438
+ if (firstKey != null) {
439
+ state.setFocusedKey(firstKey);
440
+ }
441
+ } else {
442
+ let nextKey = collection.getKeyAfter(focusedKey);
443
+ // Skip disabled keys
444
+ while (nextKey != null && state.isKeyDisabled(nextKey)) {
445
+ nextKey = collection.getKeyAfter(nextKey);
446
+ }
447
+ if (nextKey != null) {
448
+ state.setFocusedKey(nextKey);
449
+ } else if (shouldWrap) {
450
+ // Wrap to first
451
+ let firstKey = collection.getFirstKey();
452
+ while (firstKey != null && state.isKeyDisabled(firstKey)) {
453
+ firstKey = collection.getKeyAfter(firstKey);
454
+ }
455
+ if (firstKey != null) {
456
+ state.setFocusedKey(firstKey);
457
+ }
458
+ }
459
+ }
460
+ }
461
+ break;
462
+
463
+ case "ArrowUp":
464
+ e.preventDefault();
465
+ if (!state.isOpen()) {
466
+ state.open("last", "manual");
467
+ } else {
468
+ // Move to previous item
469
+ if (focusedKey == null) {
470
+ const lastKey = collection.getLastKey();
471
+ if (lastKey != null) {
472
+ state.setFocusedKey(lastKey);
473
+ }
474
+ } else {
475
+ let prevKey = collection.getKeyBefore(focusedKey);
476
+ // Skip disabled keys
477
+ while (prevKey != null && state.isKeyDisabled(prevKey)) {
478
+ prevKey = collection.getKeyBefore(prevKey);
479
+ }
480
+ if (prevKey != null) {
481
+ state.setFocusedKey(prevKey);
482
+ } else if (shouldWrap) {
483
+ // Wrap to last
484
+ let lastKey = collection.getLastKey();
485
+ while (lastKey != null && state.isKeyDisabled(lastKey)) {
486
+ lastKey = collection.getKeyBefore(lastKey);
487
+ }
488
+ if (lastKey != null) {
489
+ state.setFocusedKey(lastKey);
490
+ }
491
+ }
492
+ }
493
+ }
494
+ break;
495
+
496
+ case "Home":
497
+ if (state.isOpen()) {
498
+ e.preventDefault();
499
+ let firstKey = collection.getFirstKey();
500
+ while (firstKey != null && state.isKeyDisabled(firstKey)) {
501
+ firstKey = collection.getKeyAfter(firstKey);
502
+ }
503
+ if (firstKey != null) {
504
+ state.setFocusedKey(firstKey);
505
+ }
506
+ }
507
+ break;
508
+
509
+ case "End":
510
+ if (state.isOpen()) {
511
+ e.preventDefault();
512
+ let lastKey = collection.getLastKey();
513
+ while (lastKey != null && state.isKeyDisabled(lastKey)) {
514
+ lastKey = collection.getKeyBefore(lastKey);
515
+ }
516
+ if (lastKey != null) {
517
+ state.setFocusedKey(lastKey);
518
+ }
519
+ }
520
+ break;
521
+
522
+ case "Backspace":
523
+ // In multiple mode, remove last selected key when input is empty
524
+ if (state.selectionMode() === "multiple" && state.inputValue() === "") {
525
+ const keys = state.selectedKeys();
526
+ if (keys.size > 0) {
527
+ const lastKey = Array.from(keys).pop()!;
528
+ state.removeSelectedKey(lastKey);
529
+ }
530
+ }
531
+ break;
532
+
533
+ case "Tab":
534
+ // Commit on Tab if menu is open
535
+ if (state.isOpen() && focusedKey != null) {
536
+ state.commit();
537
+ }
538
+ break;
539
+ }
540
+ };
541
+
542
+ // Handle focus events
543
+ const handleFocus = (e: FocusEvent) => {
544
+ state.setFocused(true);
545
+ getProps().onFocus?.(e);
546
+ getProps().onFocusChange?.(true);
547
+ };
548
+
549
+ // Track the last touch event time for iPad VoiceOver double-tap debouncing
550
+ let lastEventTime = 0;
551
+
552
+ const handleBlur = (e: FocusEvent) => {
553
+ // Use synchronous ref checks instead of requestAnimationFrame
554
+ // This matches React Aria's implementation and is more reliable
555
+ const relatedTarget = e.relatedTarget as HTMLElement | null;
556
+ const button = buttonRef?.();
557
+ const listBox = listBoxRef?.();
558
+
559
+ // Don't blur if focus is moving to the button
560
+ const blurFromButton = button && button === relatedTarget;
561
+
562
+ // Don't blur if focus is moving into the listbox/popover
563
+ const blurIntoPopover = listBox?.contains(relatedTarget);
564
+
565
+ if (blurFromButton || blurIntoPopover) {
566
+ return;
567
+ }
568
+
569
+ // If a pointerdown happened inside the listbox, don't close
570
+ // This handles the case when clicking on a non-focusable option
571
+ if (isPointerDownInsideListBox) {
572
+ isPointerDownInsideListBox = false;
573
+ return;
574
+ }
575
+
576
+ // Call user's onBlur handler
577
+ getProps().onBlur?.(e);
578
+
579
+ state.setFocused(false);
580
+ getProps().onFocusChange?.(false);
581
+ };
582
+
583
+ // Handle touch events for iPad VoiceOver
584
+ // VoiceOver on iOS fires a touchend at the center of the element on double-tap.
585
+ // We detect this and toggle the combobox manually to avoid issues with focus management.
586
+ const handleTouchEnd = (e: TouchEvent) => {
587
+ const p = getProps();
588
+ const isDisabled = p.isDisabled ?? state.isDisabled;
589
+ const isReadOnly = p.isReadOnly ?? state.isReadOnly;
590
+
591
+ if (isDisabled || isReadOnly) {
592
+ return;
593
+ }
594
+
595
+ // Debounce rapid consecutive touchend events (< 500ms)
596
+ // This handles VoiceOver's double-tap behavior
597
+ if (e.timeStamp - lastEventTime < 500) {
598
+ e.preventDefault();
599
+ inputRef()?.focus();
600
+ return;
601
+ }
602
+
603
+ // Detect VoiceOver virtual click - it fires at the exact center of the element
604
+ const rect = (e.target as Element).getBoundingClientRect();
605
+ const touch = e.changedTouches[0];
606
+ const centerX = Math.ceil(rect.left + 0.5 * rect.width);
607
+ const centerY = Math.ceil(rect.top + 0.5 * rect.height);
608
+
609
+ if (touch.clientX === centerX && touch.clientY === centerY) {
610
+ e.preventDefault();
611
+ inputRef()?.focus();
612
+ state.toggle(null, "manual");
613
+ lastEventTime = e.timeStamp;
614
+ }
615
+ };
616
+
617
+ return {
618
+ get labelProps() {
619
+ return labelProps as JSX.HTMLAttributes<HTMLElement>;
620
+ },
621
+ get inputProps() {
622
+ const p = getProps();
623
+ const isOpen = state.isOpen();
624
+ const isDisabled = p.isDisabled ?? state.isDisabled;
625
+ const isReadOnly = p.isReadOnly ?? state.isReadOnly;
626
+ const focusedKey = state.focusedKey();
627
+
628
+ return mergeProps(
629
+ domProps(),
630
+ focusProps as Record<string, unknown>,
631
+ fieldProps as Record<string, unknown>,
632
+ {
633
+ id: inputId,
634
+ type: "text",
635
+ role: "combobox",
636
+ get value() {
637
+ return state.inputValue();
638
+ },
639
+ tabIndex: isDisabled ? undefined : 0,
640
+ disabled: isDisabled || undefined,
641
+ readOnly: isReadOnly || undefined,
642
+ placeholder: p.placeholder,
643
+ autoComplete: "off",
644
+ "aria-autocomplete": p.autoComplete ?? "list",
645
+ "aria-haspopup": "listbox",
646
+ "aria-expanded": isOpen,
647
+ "aria-controls": isOpen ? listBoxId : undefined,
648
+ "aria-activedescendant":
649
+ isOpen && focusedKey != null ? `${listBoxId}-option-${focusedKey}` : undefined,
650
+ "aria-disabled": isDisabled || undefined,
651
+ "aria-required": p.isRequired || undefined,
652
+ "aria-invalid": p.isInvalid || undefined,
653
+ "aria-describedby": getAriaDescribedBy(),
654
+ name: p.name,
655
+ onInput: onInputChange,
656
+ onKeyDown: onInputKeyDown,
657
+ onFocus: handleFocus,
658
+ onBlur: handleBlur,
659
+ onTouchEnd: handleTouchEnd,
660
+ "data-open": isOpen || undefined,
661
+ "data-disabled": isDisabled || undefined,
662
+ "data-readonly": isReadOnly || undefined,
663
+ "data-focus-visible": isFocusVisible() || undefined,
664
+ } as Record<string, unknown>,
665
+ ) as JSX.InputHTMLAttributes<HTMLInputElement>;
666
+ },
667
+ get buttonProps() {
668
+ const p = getProps();
669
+ const isOpen = state.isOpen();
670
+ const isDisabled = p.isDisabled ?? state.isDisabled;
671
+ const isReadOnly = p.isReadOnly ?? state.isReadOnly;
672
+
673
+ return mergeProps(
674
+ pressProps as Record<string, unknown>,
675
+ {
676
+ id: buttonId,
677
+ type: "button",
678
+ tabIndex: -1,
679
+ "aria-haspopup": "listbox",
680
+ "aria-expanded": isOpen,
681
+ "aria-controls": isOpen ? listBoxId : undefined,
682
+ "aria-disabled": isDisabled || isReadOnly || undefined,
683
+ disabled: isDisabled || isReadOnly || undefined,
684
+ "aria-label": stringFormatter?.().format("buttonLabel") ?? "Show suggestions",
685
+ "data-open": isOpen || undefined,
686
+ "data-disabled": isDisabled || isReadOnly || undefined,
687
+ } as Record<string, unknown>,
688
+ ) as JSX.HTMLAttributes<HTMLElement>;
689
+ },
690
+ get listBoxProps() {
691
+ const isMulti = state.selectionMode() === "multiple";
692
+ return {
693
+ id: listBoxId,
694
+ role: "listbox",
695
+ "aria-labelledby": inputId,
696
+ "aria-multiselectable": isMulti || undefined,
697
+ tabIndex: -1,
698
+ shouldSelectOnPressUp: true,
699
+ shouldFocusOnHover: true,
700
+ // Track pointerdown inside listbox to prevent blur from closing
701
+ // Use capture phase because createPress calls stopPropagation on pointerdown
702
+ onPointerDownCapture: () => {
703
+ isPointerDownInsideListBox = true;
704
+ },
705
+ onMouseDownCapture: () => {
706
+ // Fallback for environments without PointerEvent
707
+ isPointerDownInsideListBox = true;
708
+ },
709
+ } as JSX.HTMLAttributes<HTMLElement>;
710
+ },
711
+ get descriptionProps() {
712
+ return {
713
+ id: descriptionId,
714
+ } as JSX.HTMLAttributes<HTMLElement>;
715
+ },
716
+ get errorMessageProps() {
717
+ return {
718
+ id: errorMessageId,
719
+ role: "alert",
720
+ } as JSX.HTMLAttributes<HTMLElement>;
721
+ },
722
+ isFocused,
723
+ isFocusVisible: () => isFocused() && isFocusVisible(),
724
+ isOpen: state.isOpen,
725
+ selectedItem: state.selectedItem,
726
+ };
727
+ }