@torpor/ui 0.4.0 → 1.0.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 (342) hide show
  1. package/README.md +83 -7
  2. package/dist/Accordion/Accordion.torp +42 -165
  3. package/dist/Accordion/AccordionContent.torp +6 -6
  4. package/dist/Accordion/AccordionHeader.torp +7 -11
  5. package/dist/Accordion/AccordionItem.torp +24 -24
  6. package/dist/Accordion/AccordionTrigger.torp +17 -8
  7. package/dist/Accordion/AccordionTypes.ts +1 -1
  8. package/dist/Accordion/index.d.ts +23 -25
  9. package/dist/Accordion/index.js +6 -22
  10. package/dist/Breadcrumb/Breadcrumb.torp +12 -10
  11. package/dist/Breadcrumb/BreadcrumbItem.torp +6 -6
  12. package/dist/Breadcrumb/BreadcrumbList.torp +21 -0
  13. package/dist/Breadcrumb/BreadcrumbSeparator.torp +4 -4
  14. package/dist/Breadcrumb/index.d.ts +39 -15
  15. package/dist/Breadcrumb/index.js +5 -14
  16. package/dist/Calendar/Calendar.torp +163 -156
  17. package/dist/Calendar/CalendarDay.torp +91 -45
  18. package/dist/Calendar/CalendarDayHeader.torp +36 -0
  19. package/dist/Calendar/CalendarGrid.torp +104 -0
  20. package/dist/Calendar/CalendarGridHeader.torp +43 -0
  21. package/dist/Calendar/CalendarHeader.torp +48 -0
  22. package/dist/Calendar/CalendarHeaderTrigger.torp +54 -0
  23. package/dist/Calendar/CalendarTypes.ts +24 -3
  24. package/dist/Calendar/buildDays.ts +54 -0
  25. package/dist/Calendar/index.d.ts +191 -5
  26. package/dist/Calendar/index.js +8 -10
  27. package/dist/Carousel/Carousel.torp +180 -0
  28. package/dist/Carousel/CarouselIndicators.torp +65 -0
  29. package/dist/Carousel/CarouselNext.torp +43 -0
  30. package/dist/Carousel/CarouselPrevious.torp +44 -0
  31. package/dist/Carousel/CarouselSlide.torp +57 -0
  32. package/dist/Carousel/CarouselTypes.ts +28 -0
  33. package/dist/Carousel/index.d.ts +147 -0
  34. package/dist/Carousel/index.js +6 -0
  35. package/dist/Charts/BarChart.torp +55 -144
  36. package/dist/Charts/Chart.torp +120 -0
  37. package/dist/Charts/ChartBars.torp +78 -0
  38. package/dist/Charts/ChartColumns.torp +66 -0
  39. package/dist/Charts/ChartGrid.torp +54 -0
  40. package/dist/Charts/ChartLines.torp +60 -0
  41. package/dist/Charts/ChartTypes.ts +25 -0
  42. package/dist/Charts/ChartXAxis.torp +83 -0
  43. package/dist/Charts/ChartYAxis.torp +105 -0
  44. package/dist/Charts/ColumnChart.torp +70 -0
  45. package/dist/Charts/GridLines.torp +4 -4
  46. package/dist/Charts/LineChart.torp +55 -178
  47. package/dist/Charts/PieChart.torp +14 -20
  48. package/dist/Charts/ScatterChart.torp +45 -38
  49. package/dist/Charts/SparkLine.torp +16 -19
  50. package/dist/Charts/XAxis.torp +11 -11
  51. package/dist/Charts/YAxis.torp +8 -8
  52. package/dist/Charts/index.d.ts +328 -79
  53. package/dist/Charts/index.js +17 -34
  54. package/dist/ColorPicker/ColorPalette.torp +154 -0
  55. package/dist/ColorPicker/ColorPicker.torp +157 -0
  56. package/dist/ColorPicker/index.d.ts +85 -0
  57. package/dist/ColorPicker/index.js +3 -0
  58. package/dist/ComboBox/ComboBox.torp +205 -0
  59. package/dist/ComboBox/ComboBoxContent.torp +112 -0
  60. package/dist/ComboBox/ComboBoxInput.torp +145 -0
  61. package/dist/ComboBox/ComboBoxTrigger.torp +49 -0
  62. package/dist/ComboBox/ComboBoxTypes.ts +39 -0
  63. package/dist/ComboBox/index.d.ts +152 -0
  64. package/dist/ComboBox/index.js +6 -0
  65. package/dist/CommandPalette/CommandPalette.torp +220 -0
  66. package/dist/CommandPalette/CommandTypes.ts +14 -0
  67. package/dist/CommandPalette/index.d.ts +45 -0
  68. package/dist/CommandPalette/index.js +2 -0
  69. package/dist/Contextual/Contextual.torp +32 -50
  70. package/dist/Contextual/ContextualContent.torp +29 -73
  71. package/dist/Contextual/ContextualHover.torp +112 -0
  72. package/dist/Contextual/ContextualTrigger.torp +31 -42
  73. package/dist/Contextual/ContextualTypes.ts +4 -17
  74. package/dist/Contextual/index.d.ts +44 -27
  75. package/dist/Contextual/index.js +5 -14
  76. package/dist/DataGrid/DataGrid.torp +496 -0
  77. package/dist/DataGrid/DataGridTypes.ts +25 -0
  78. package/dist/DataGrid/index.d.ts +90 -0
  79. package/dist/DataGrid/index.js +3 -0
  80. package/dist/DataGrid/sortRows.ts +32 -0
  81. package/dist/DatePicker/DatePicker.torp +222 -0
  82. package/dist/DatePicker/DatePickerHeader.torp +20 -0
  83. package/dist/DatePicker/index.d.ts +55 -0
  84. package/dist/DatePicker/index.js +2 -0
  85. package/dist/DateRangePicker/DateRangePicker.torp +311 -0
  86. package/dist/DateRangePicker/DateRangePickerDay.torp +137 -0
  87. package/dist/DateRangePicker/DateRangePickerTypes.ts +21 -0
  88. package/dist/DateRangePicker/index.d.ts +76 -0
  89. package/dist/DateRangePicker/index.js +3 -0
  90. package/dist/Dialog/Dialog.torp +24 -17
  91. package/dist/Dialog/DialogBody.torp +5 -9
  92. package/dist/Dialog/DialogButton.torp +12 -12
  93. package/dist/Dialog/DialogFooter.torp +3 -7
  94. package/dist/Dialog/DialogHeader.torp +6 -10
  95. package/dist/Dialog/index.d.ts +27 -27
  96. package/dist/Dialog/index.js +6 -22
  97. package/dist/Disclosure/Disclosure.torp +18 -18
  98. package/dist/Disclosure/DisclosureContent.torp +5 -6
  99. package/dist/Disclosure/DisclosureHeader.torp +7 -11
  100. package/dist/Disclosure/DisclosureTrigger.torp +4 -5
  101. package/dist/Disclosure/index.d.ts +14 -16
  102. package/dist/Disclosure/index.js +5 -18
  103. package/dist/Drawer/Drawer.torp +19 -49
  104. package/dist/Drawer/DrawerContent.torp +67 -13
  105. package/dist/Drawer/DrawerOverlay.torp +4 -4
  106. package/dist/Drawer/DrawerTrigger.torp +16 -4
  107. package/dist/Drawer/DrawerTypes.ts +3 -0
  108. package/dist/Drawer/index.d.ts +16 -20
  109. package/dist/Drawer/index.js +5 -18
  110. package/dist/FocusGroup/FocusGroup.torp +70 -0
  111. package/dist/FocusGroup/FocusGroupButton.torp +59 -0
  112. package/dist/FocusGroup/FocusGroupTypes.ts +14 -0
  113. package/dist/FocusGroup/index.d.ts +44 -0
  114. package/dist/FocusGroup/index.js +3 -0
  115. package/dist/Form/CheckBox.torp +27 -18
  116. package/dist/Form/Field.torp +17 -17
  117. package/dist/Form/File.torp +31 -22
  118. package/dist/Form/FileDrop.torp +187 -0
  119. package/dist/Form/Form.torp +24 -22
  120. package/dist/Form/FormTypes.ts +2 -0
  121. package/dist/Form/FormValues.torp +37 -0
  122. package/dist/Form/Hidden.torp +4 -12
  123. package/dist/Form/Input.torp +38 -32
  124. package/dist/Form/Label.torp +3 -3
  125. package/dist/Form/Message.torp +13 -5
  126. package/dist/Form/MessageSummary.torp +5 -4
  127. package/dist/Form/Option.torp +11 -7
  128. package/dist/Form/Select.torp +32 -28
  129. package/dist/Form/TextArea.torp +35 -31
  130. package/dist/Form/Toggle.torp +109 -0
  131. package/dist/Form/index.d.ts +178 -79
  132. package/dist/Form/index.js +16 -50
  133. package/dist/ListBox/ListBox.torp +253 -150
  134. package/dist/ListBox/ListBoxItem.torp +54 -30
  135. package/dist/ListBox/ListBoxSeparator.torp +5 -2
  136. package/dist/ListBox/ListBoxTypes.ts +23 -5
  137. package/dist/ListBox/LoadedListBoxItems.torp +41 -0
  138. package/dist/ListBox/index.d.ts +56 -17
  139. package/dist/ListBox/index.js +4 -14
  140. package/dist/MaskedInput/MaskedInput.torp +143 -0
  141. package/dist/MaskedInput/index.d.ts +52 -0
  142. package/dist/MaskedInput/index.js +2 -0
  143. package/dist/Menu/Menu.torp +86 -71
  144. package/dist/Menu/MenuButton.torp +17 -10
  145. package/dist/Menu/MenuCheck.torp +22 -18
  146. package/dist/Menu/MenuGroup.torp +4 -4
  147. package/dist/Menu/MenuIndicator.torp +5 -13
  148. package/dist/Menu/MenuPopout.torp +20 -50
  149. package/dist/Menu/MenuPopoutContent.torp +15 -66
  150. package/dist/Menu/MenuPopoutTrigger.torp +19 -100
  151. package/dist/Menu/MenuRadio.torp +25 -26
  152. package/dist/Menu/MenuRadioGroup.torp +25 -68
  153. package/dist/Menu/MenuSeparator.torp +3 -2
  154. package/dist/Menu/MenuTypes.ts +9 -19
  155. package/dist/Menu/index.d.ts +45 -51
  156. package/dist/Menu/index.js +12 -46
  157. package/dist/MenuBar/MenuBar.torp +31 -83
  158. package/dist/MenuBar/MenuBarItem.torp +33 -51
  159. package/dist/MenuBar/MenuBarItemContent.torp +15 -101
  160. package/dist/MenuBar/MenuBarItemTrigger.torp +20 -55
  161. package/dist/MenuBar/MenuBarTypes.ts +11 -10
  162. package/dist/MenuBar/index.d.ts +17 -19
  163. package/dist/MenuBar/index.js +5 -18
  164. package/dist/Modal/Modal.torp +22 -47
  165. package/dist/Modal/ModalContent.torp +21 -44
  166. package/dist/Modal/ModalOverlay.torp +4 -4
  167. package/dist/Modal/ModalTrigger.torp +16 -32
  168. package/dist/Modal/ModalTypes.ts +5 -16
  169. package/dist/Modal/index.d.ts +17 -29
  170. package/dist/Modal/index.js +29 -28
  171. package/dist/Modal/index.js.map +1 -1
  172. package/dist/Modal/showPrompt.ts +2 -2
  173. package/dist/NavMenu/NavMenu.torp +22 -111
  174. package/dist/NavMenu/NavMenuButton.torp +7 -8
  175. package/dist/NavMenu/NavMenuElement.torp +6 -7
  176. package/dist/NavMenu/NavMenuGroup.torp +5 -5
  177. package/dist/NavMenu/NavMenuGroupHeader.torp +5 -8
  178. package/dist/NavMenu/NavMenuLink.torp +7 -10
  179. package/dist/NavMenu/NavMenuPopout.torp +15 -60
  180. package/dist/NavMenu/NavMenuPopoutContent.torp +14 -73
  181. package/dist/NavMenu/NavMenuPopoutTrigger.torp +17 -99
  182. package/dist/NavMenu/NavMenuSeparator.torp +3 -7
  183. package/dist/NavMenu/NavMenuTypes.ts +4 -14
  184. package/dist/NavMenu/index.d.ts +40 -44
  185. package/dist/NavMenu/index.js +11 -42
  186. package/dist/Notification/Notification.torp +12 -24
  187. package/dist/Notification/NotificationClose.torp +6 -3
  188. package/dist/Notification/NotificationContainer.torp +3 -7
  189. package/dist/Notification/NotificationContent.torp +3 -7
  190. package/dist/Notification/NotificationTypes.ts +1 -1
  191. package/dist/Notification/index.d.ts +15 -12
  192. package/dist/Notification/index.js +22 -32
  193. package/dist/Notification/index.js.map +1 -1
  194. package/dist/NumberInput/NumberInput.torp +255 -0
  195. package/dist/NumberInput/index.d.ts +44 -0
  196. package/dist/NumberInput/index.js +2 -0
  197. package/dist/Pagination/Pagination.torp +41 -34
  198. package/dist/Pagination/PaginationItem.torp +50 -13
  199. package/dist/Pagination/PaginationTypes.ts +3 -6
  200. package/dist/Pagination/buildPages.ts +9 -5
  201. package/dist/Pagination/index.d.ts +19 -9
  202. package/dist/Pagination/index.js +3 -40
  203. package/dist/Popover/Popover.torp +32 -50
  204. package/dist/Popover/PopoverContent.torp +27 -76
  205. package/dist/Popover/PopoverHover.torp +90 -0
  206. package/dist/Popover/PopoverTrigger.torp +17 -40
  207. package/dist/Popover/PopoverTypes.ts +4 -17
  208. package/dist/Popover/index.d.ts +40 -25
  209. package/dist/Popover/index.js +18 -18
  210. package/dist/Popover/index.js.map +1 -1
  211. package/dist/Progress/Progress.torp +88 -0
  212. package/dist/Progress/index.d.ts +37 -0
  213. package/dist/Progress/index.js +2 -0
  214. package/dist/Rating/Rating.torp +225 -0
  215. package/dist/Rating/index.d.ts +44 -0
  216. package/dist/Rating/index.js +2 -0
  217. package/dist/SegmentedControl/SegmentedControl.torp +233 -0
  218. package/dist/SegmentedControl/SegmentedControlTypes.ts +26 -0
  219. package/dist/SegmentedControl/SegmentedItem.torp +78 -0
  220. package/dist/SegmentedControl/index.d.ts +74 -0
  221. package/dist/SegmentedControl/index.js +3 -0
  222. package/dist/SelectBox/SelectBox.torp +107 -53
  223. package/dist/SelectBox/SelectBoxContent.torp +45 -19
  224. package/dist/SelectBox/SelectBoxTrigger.torp +108 -8
  225. package/dist/SelectBox/SelectBoxTypes.ts +26 -11
  226. package/dist/SelectBox/index.d.ts +39 -54
  227. package/dist/SelectBox/index.js +5 -18
  228. package/dist/Slider/Slider.torp +248 -0
  229. package/dist/Slider/index.d.ts +45 -0
  230. package/dist/Slider/index.js +2 -0
  231. package/dist/Splitter/Splitter.torp +156 -0
  232. package/dist/Splitter/SplitterHandle.torp +161 -0
  233. package/dist/Splitter/SplitterPane.torp +63 -0
  234. package/dist/Splitter/SplitterTypes.ts +31 -0
  235. package/dist/Splitter/index.d.ts +103 -0
  236. package/dist/Splitter/index.js +4 -0
  237. package/dist/Stepper/Stepper.torp +146 -0
  238. package/dist/Stepper/StepperMarker.torp +24 -0
  239. package/dist/Stepper/StepperNext.torp +44 -0
  240. package/dist/Stepper/StepperPrevious.torp +44 -0
  241. package/dist/Stepper/StepperStep.torp +90 -0
  242. package/dist/Stepper/StepperTypes.ts +30 -0
  243. package/dist/Stepper/index.d.ts +149 -0
  244. package/dist/Stepper/index.js +6 -0
  245. package/dist/TabGroup/TabContent.torp +6 -6
  246. package/dist/TabGroup/TabGroup.torp +97 -113
  247. package/dist/TabGroup/TabGroupTypes.ts +1 -1
  248. package/dist/TabGroup/TabHeader.torp +23 -19
  249. package/dist/TabGroup/TabList.torp +8 -21
  250. package/dist/TabGroup/TabTrigger.torp +6 -7
  251. package/dist/TabGroup/index.d.ts +26 -26
  252. package/dist/TabGroup/index.js +6 -22
  253. package/dist/TagInput/TagInput.torp +378 -0
  254. package/dist/TagInput/index.d.ts +48 -0
  255. package/dist/TagInput/index.js +2 -0
  256. package/dist/TimePicker/TimePicker.torp +462 -0
  257. package/dist/TimePicker/index.d.ts +53 -0
  258. package/dist/TimePicker/index.js +2 -0
  259. package/dist/ToolBar/ToolBar.torp +29 -110
  260. package/dist/ToolBar/ToolBarButton.torp +7 -8
  261. package/dist/ToolBar/ToolBarElement.torp +7 -11
  262. package/dist/ToolBar/ToolBarPopout.torp +16 -68
  263. package/dist/ToolBar/ToolBarPopoutContent.torp +15 -73
  264. package/dist/ToolBar/ToolBarPopoutTrigger.torp +19 -110
  265. package/dist/ToolBar/ToolBarSeparator.torp +3 -7
  266. package/dist/ToolBar/ToolBarTypes.ts +7 -14
  267. package/dist/ToolBar/index.d.ts +30 -32
  268. package/dist/ToolBar/index.js +8 -30
  269. package/dist/Tree/Tree.torp +395 -0
  270. package/dist/Tree/TreeItem.torp +176 -0
  271. package/dist/Tree/TreeItemChildren.torp +60 -0
  272. package/dist/Tree/TreeItemContent.torp +46 -0
  273. package/dist/Tree/TreeItemLabel.torp +94 -0
  274. package/dist/Tree/TreeItemTrigger.torp +78 -0
  275. package/dist/Tree/TreeLoadedChildren.torp +122 -0
  276. package/dist/Tree/TreeTypes.ts +63 -0
  277. package/dist/Tree/index.d.ts +262 -0
  278. package/dist/Tree/index.js +7 -0
  279. package/dist/icons/ChevronDoubleLeft.torp +19 -0
  280. package/dist/icons/ChevronDoubleRight.torp +19 -0
  281. package/dist/icons/index.d.ts +61 -1
  282. package/dist/icons/index.js +17 -58
  283. package/dist/loader-DEeG5vFY.js +44 -0
  284. package/dist/loader-DEeG5vFY.js.map +1 -0
  285. package/dist/motion/index.d.ts +12 -8
  286. package/dist/motion/index.d.ts.map +1 -0
  287. package/dist/motion/index.js +72 -88
  288. package/dist/motion/index.js.map +1 -1
  289. package/dist/mount/draggable.ts +63 -0
  290. package/dist/mount/dropTarget.ts +108 -0
  291. package/dist/mount/hoverTrigger.ts +1 -1
  292. package/dist/utils/$handle.ts +23 -0
  293. package/dist/utils/LoadedItems.torp +47 -0
  294. package/dist/utils/PopoutClickTrigger.torp +99 -0
  295. package/dist/utils/PopoutTypes.ts +59 -12
  296. package/dist/utils/SubmenuPopout.torp +89 -0
  297. package/dist/utils/SubmenuPopoutContent.torp +87 -0
  298. package/dist/utils/SubmenuPopoutTrigger.torp +160 -0
  299. package/dist/utils/chartColors.ts +13 -13
  300. package/dist/utils/chartLayout.ts +143 -0
  301. package/dist/utils/colorUtils.ts +103 -0
  302. package/dist/utils/createItemGroup.ts +152 -0
  303. package/dist/utils/dateUtils.ts +8 -7
  304. package/dist/utils/documentEvents.ts +4 -4
  305. package/dist/utils/dragState.ts +86 -0
  306. package/dist/utils/focusGroup.ts +204 -0
  307. package/dist/utils/focusItem.ts +44 -0
  308. package/dist/utils/formField.ts +70 -0
  309. package/dist/utils/hoverOpen.ts +50 -0
  310. package/dist/utils/hoverReveal.ts +194 -0
  311. package/dist/utils/loader.ts +113 -0
  312. package/dist/utils/maskUtils.ts +98 -0
  313. package/dist/utils/popoutContent.ts +137 -0
  314. package/dist/utils/reorderList.ts +52 -0
  315. package/package.json +97 -22
  316. package/dist/Accordion/index.js.map +0 -1
  317. package/dist/Breadcrumb/index.js.map +0 -1
  318. package/dist/Calendar/index.js.map +0 -1
  319. package/dist/Charts/index.js.map +0 -1
  320. package/dist/Contextual/index.js.map +0 -1
  321. package/dist/Dialog/index.js.map +0 -1
  322. package/dist/Disclosure/index.js.map +0 -1
  323. package/dist/Drawer/index.js.map +0 -1
  324. package/dist/Form/index.js.map +0 -1
  325. package/dist/ListBox/index.js.map +0 -1
  326. package/dist/Menu/index.js.map +0 -1
  327. package/dist/MenuBar/index.js.map +0 -1
  328. package/dist/NavMenu/index.js.map +0 -1
  329. package/dist/Pagination/index.js.map +0 -1
  330. package/dist/SelectBox/SelectBoxInput.torp +0 -95
  331. package/dist/SelectBox/index.js.map +0 -1
  332. package/dist/TabGroup/index.js.map +0 -1
  333. package/dist/ToolBar/index.js.map +0 -1
  334. package/dist/icons/index.js.map +0 -1
  335. package/dist/index.d.ts +0 -19
  336. package/dist/metafile-esm.json +0 -1
  337. package/dist/motion/fade.ts +0 -16
  338. package/dist/motion/grow.ts +0 -23
  339. package/dist/motion/measure.ts +0 -28
  340. package/dist/motion/slide.ts +0 -45
  341. package/dist/mount/index.d.ts +0 -2
  342. package/dist/utils/index.d.ts +0 -2
@@ -1,7 +1,19 @@
1
- import type { ClassValue, StyleValue } from "@torpor/view";
1
+ import type { Bindable, ClassValue, StyleValue } from "@torpor/view";
2
+ import $handle from "../utils/$handle";
3
+ import {
4
+ type FieldContext,
5
+ FieldContextName,
6
+ type FormContext,
7
+ FormContextName,
8
+ } from "../Form/FormTypes";
9
+ import FormValues from "../Form/FormValues.torp";
10
+ import { type LoadResult, type Loader, createItemLoader } from "../utils/loader";
11
+ import { createItemGroup } from "../utils/createItemGroup";
2
12
  import getId from "../utils/getId";
3
- import { PopoutContextName, type PopoutContext, type HandleButtonPress } from "../utils/PopoutTypes";
4
- import { ListBoxContextName, type ListBoxContext, type ItemState } from "./ListBoxTypes";
13
+ import createFormField from "../utils/formField";
14
+ import { type HandleButtonPress, type PopoutContext, PopoutContextName } from "../utils/PopoutTypes";
15
+ import LoadedListBoxItems from "./LoadedListBoxItems.torp";
16
+ import { type ItemState, type ListBoxContext, ListBoxContextName } from "./ListBoxTypes";
5
17
 
6
18
  // TODO: orientation
7
19
 
@@ -9,19 +21,39 @@ interface ListBoxProps {
9
21
  /** An ID for the root element */
10
22
  id?: string;
11
23
  /** Classes for the root element */
12
- class?: ClassValue,
24
+ class?: ClassValue;
13
25
  /** Styles for the root element */
14
- style?: StyleValue,
26
+ style?: StyleValue;
15
27
  /** Whether a single ListBoxItem or multiple ListBoxItems can be selected at the same time */
16
- type?: "single" | "multiple",
28
+ type?: "single" | "multiple";
17
29
  /** The value of the selected ListBoxItem, or an array of values if the type is "multiple" */
18
- value?: any | any[],
30
+ value: Bindable<any | any[]>;
19
31
  /** If set to true, the user cannot interact with any items in the ListBox */
20
- disabled?: boolean,
32
+ disabled?: boolean;
21
33
  /** An ARIA label for describing the ListBox to screen readers */
22
- ariaLabel?: string | undefined,
34
+ ariaLabel?: string | undefined;
35
+ /** An ID of an element that labels the ListBox */
36
+ ariaLabelledBy?: string | undefined;
37
+ /** The orientation of the ListBox */
38
+ orientation?: "vertical" | "horizontal";
23
39
  /** Raised when the selected item(s) have changed */
24
- onchange?: (value: any | any[]) => void,
40
+ /** The name of the component's form data field */
41
+ name?: string;
42
+ /**
43
+ * Reads options from the network (or anywhere async), receiving
44
+ * `{ searchText }` whenever the bound `searchText` changes. When set and
45
+ * no ListBoxItems are slotted in, the options are rendered automatically
46
+ */
47
+ load?: Loader;
48
+ /** Called after each successful load with the normalized result */
49
+ onload?: (result: LoadResult) => void;
50
+ /** Extracts the display text for a loaded item (defaults to `label ?? text ?? item`) */
51
+ getItemLabel?: (item: any) => any;
52
+ /** Extracts the value for a loaded item (defaults to `value ?? id`) */
53
+ getItemValue?: (item: any) => any;
54
+ /** Filter text for a `load` function; two-way bindable */
55
+ searchText?: Bindable<string | undefined>;
56
+ onchange?: (value: any | any[]) => void;
25
57
  }
26
58
 
27
59
  /**
@@ -40,28 +72,44 @@ interface ListBoxProps {
40
72
  */
41
73
  export default function ListBox($props: ListBoxProps | undefined) {
42
74
  let defaultId = getId();
43
- let type = $props?.type ?? "single";
75
+ let root: HTMLElement;
44
76
 
45
- let $state = $watch({
46
- value: $props?.value ?? (type === "single" ? undefined : [])
47
- });
77
+ // Wire up the enclosing Form and Field, if any
78
+ const form = createFormField(
79
+ $props?.name,
80
+ $context[FormContextName] as FormContext | undefined,
81
+ $context[FieldContextName] as FieldContext | undefined,
82
+ );
48
83
 
49
- // Update $state on $props change
50
- $run(() => {
51
- $state.value = $props?.value ?? (type === "single" ? undefined : [])
52
- });
84
+ // Get the current selection type, re-reading $props so changes are picked up
85
+ function getType(): "single" | "multiple" {
86
+ return $props?.type ?? "single";
87
+ }
53
88
 
54
- // Update $props on $state change
55
- $run(() => {
56
- if ($props !== undefined) {
57
- $props.value = $state.value;
58
- }
89
+ // This may come from a PopoutContext, or we may need to handle it ourselves
90
+ let searchText: string | undefined;
91
+
92
+ let $state = $watch({
93
+ value: $props?.value ?? (getType() === "single" ? undefined : []),
94
+ searchText: undefined as string | undefined,
59
95
  });
60
96
 
61
- let itemStates: ItemState[] = [];
97
+ $bind($state, $props, "value", "searchText");
62
98
 
63
- // This is used to assign automatic indexes to items where needed
64
- let nextIndex = 0;
99
+ if ($props?.load !== undefined && typeof $props.load !== "function") {
100
+ throw new Error("ListBox: `load` must be a function");
101
+ }
102
+ const hasLoader = typeof $props?.load === "function";
103
+
104
+ // Load options whenever the search text changes; pass the loaded items
105
+ // and getters through the context for the slot fallback to render
106
+ const $load = hasLoader
107
+ ? createItemLoader({
108
+ load: $props!.load!,
109
+ getRequest: () => ({ searchText: $state.searchText }),
110
+ onload: (r) => $props?.onload?.(r),
111
+ })
112
+ : undefined;
65
113
 
66
114
  // Maybe get stuff out of a popout context
67
115
  let handleButton: HandleButtonPress | undefined = undefined;
@@ -74,185 +122,204 @@ export default function ListBox($props: ListBoxProps | undefined) {
74
122
  // called after everything has been shown
75
123
  popoutContext.focusFirstElement = focusFirstElement;
76
124
  popoutContext.focusLastElement = focusLastElement;
125
+ popoutContext.markElement = markElement;
126
+ popoutContext.selectMarkedElement = selectMarkedElement;
127
+ popoutContext.searchItems = searchItems;
77
128
 
78
- // Set the content ID so that it can be used with aria-controls in AccordionTrigger
129
+ // Set the content ID and role so that triggers can use them with
130
+ // aria-controls / aria-haspopup
79
131
  popoutContext.state.contentId = $props?.id ?? defaultId;
132
+ popoutContext.state.contentRole = "listbox";
80
133
  }
81
134
 
135
+ const group = createItemGroup<ItemState>({
136
+ state: $state,
137
+ type: getType,
138
+ selectedProperty: "selected",
139
+ disabled: () => $props?.disabled,
140
+ onToggle: () => {
141
+ // Clear the keyboard focus marker
142
+ for (let item of group.itemStates) {
143
+ item.active = false;
144
+ }
145
+ form.handleInput();
146
+ // Pass this along to the containing popout if applicable
147
+ if (handleButton) {
148
+ if (getType() === "single") {
149
+ handleButton("confirm", $state.value);
150
+ } else {
151
+ handleButton(undefined, $state.value);
152
+ }
153
+ }
154
+ },
155
+ });
156
+
82
157
  // Set the context to pass down to items, with functions for each item to call
83
158
  let context: ListBoxContext = {
84
- registerItem,
85
- removeItem,
86
- toggleItem,
159
+ get type() {
160
+ return getType();
161
+ },
162
+ state: $state,
163
+ registerItem: group.registerItem,
164
+ removeItem: group.removeItem,
165
+ toggleItem: group.toggleItem,
87
166
  handleKey,
167
+ setActiveDescendant: popoutContext ? (id) => { popoutContext.state.activeDescendant = id; } : undefined,
168
+ getLoadedItems: $load ? () => $load.items : undefined,
169
+ getItemLabel: $props?.getItemLabel,
170
+ getItemValue: $props?.getItemValue,
88
171
  };
89
172
  $context[ListBoxContextName] = context;
90
173
 
174
+ // Dispatch the change event when the value changes
175
+ $handle((first) => {
176
+ const value = $state.value;
177
+ if (!first) $props?.onchange?.(value);
178
+ });
179
+
91
180
  function focusFirstElement() {
92
- if (itemStates.length > 0) {
93
- itemStates[0].setFocused();
181
+ if (group.itemStates.length > 0) {
182
+ group.itemStates[0].setFocused();
94
183
  }
95
184
  }
96
185
 
97
186
  function focusLastElement() {
98
- if (itemStates.length > 0) {
99
- itemStates.at(-1)!.setFocused();
187
+ if (group.itemStates.length > 0) {
188
+ group.itemStates.at(-1)!.setFocused();
100
189
  }
101
190
  }
102
191
 
103
- $run(() => {
104
- // Get the value to ensure that this effect will be subscribed
105
- let value = $state.value;
106
-
107
- // And set the expanded item(s)
108
- switch (type) {
109
- case "single": {
110
- for (let item of itemStates) {
111
- item.selected = item.value === value;
112
- }
113
- break;
114
- }
115
- case "multiple": {
116
- if (!Array.isArray(value)) {
117
- value = [value];
118
- }
119
- for (let item of itemStates) {
120
- item.selected = value.indexOf(item.value) !== -1;
121
- }
122
- break;
123
- }
124
- }
125
- })
126
-
127
- $run(() => {
128
- let disabled = $props?.disabled;
129
- itemStates.forEach((s) => s.parentDisabled = disabled === true);
130
- });
131
-
132
- // Dispatch the change event when the value changes
133
- let firstChange = true;
134
- $run(() => {
135
- const onchange = $props?.onchange;
136
- const value = $state.value;
137
- if (firstChange) {
138
- firstChange = false;
139
- } else if (onchange) {
140
- onchange(value);
141
- }
142
- });
143
-
144
- function registerItem(itemState: ItemState) {
145
- // If an index wasn't set for the item, set it automatically
146
- if (itemState.index === -1) {
147
- itemState.index = nextIndex++;
148
- }
192
+ function markElement(id: string) {
193
+ // selected stays derived from the group value; active marks the
194
+ // keyboard-focus preview
195
+ group.itemStates.forEach((i) => {
196
+ i.active = i.id === id;
197
+ });
198
+ }
149
199
 
150
- // If a value wasn't set for the item, set it to the index
151
- if (!itemState.value) {
152
- itemState.value = itemState.index;
200
+ function selectMarkedElement() {
201
+ const marked = group.itemStates.find((i) => i.active === true);
202
+ if (marked && handleButton) {
203
+ handleButton("confirm", marked.value);
153
204
  }
205
+ }
154
206
 
155
- // If we have a value, set whether the item should be selected
156
- if ($state.value !== undefined && $state.value !== null) {
157
- switch (type) {
158
- case "single": {
159
- itemState.selected = $state.value === itemState.value;
160
- break;
161
- }
162
- case "multiple": {
163
- itemState.selected = $state.value.includes(itemState.value);
164
- break;
165
- }
207
+ function searchItems(searchText: string) {
208
+ const normalizedSearchText = searchText.toLocaleLowerCase();
209
+ for (let item of group.itemStates) {
210
+ if (item.text.toLocaleLowerCase().includes(normalizedSearchText)) {
211
+ return item;
166
212
  }
167
213
  }
168
-
169
- // Set whether the ListBox itself is disabled
170
- itemState.parentDisabled = $props?.disabled === true
171
-
172
- // Add the item's state to the array
173
- itemStates.splice(itemState.index, 0, itemState);
174
214
  }
175
215
 
176
- // TODO: I think this should be done by value
177
- function removeItem(itemIndex: number) {
178
- // Remove the item and re-index
179
- let index = itemStates.findIndex((item) => item.index === itemIndex);
180
- if (index !== -1) {
181
- itemStates.splice(itemIndex, 1);
182
- }
183
- itemStates.forEach((item, i) => (item.index = i));
216
+ function focusSelectedElement() {
217
+ // Called when the list box receives focus: focus the selected option,
218
+ // or the first option if none is selected
219
+ const itemStates = group.itemStates;
220
+ const target = itemStates.find((i) => i.selected) ?? itemStates[0];
221
+ target?.setFocused();
184
222
  }
185
223
 
186
- function toggleItem(value: string) {
187
- switch (type) {
188
- case "single": {
189
- for (let item of itemStates) {
190
- if (item.value === value) {
191
- $state.value = item.selected ? "" : item.value;
192
- break;
193
- }
194
- }
195
- // Pass this along to the containing popout if applicable, so
196
- // that the popout can be closed
197
- if (handleButton) {
198
- handleButton("confirm", value)
199
- }
200
- break;
224
+ function handleKey(index: number, e: KeyboardEvent) {
225
+ const itemStates = group.itemStates;
226
+ // Finds the nearest focusable item, skipping disabled ones
227
+ const findFocusable = (from: number, step: number): number => {
228
+ let i = from;
229
+ while (i >= 0 && i < itemStates.length && itemStates[i].disabled) {
230
+ i += step;
201
231
  }
202
- case "multiple": {
203
- let newValue = [];
204
- for (let item of itemStates) {
205
- if (
206
- (item.value === value && !item.selected) ||
207
- (item.value !== value && item.selected)
208
- ) {
209
- newValue.push(item.value);
210
- }
211
- }
212
- $state.value = newValue.sort();
213
- break;
232
+ return i;
233
+ };
234
+ const clearSearch = () => {
235
+ if (popoutContext) {
236
+ popoutContext.searchText = undefined;
237
+ } else {
238
+ searchText = undefined;
214
239
  }
215
- }
216
- }
217
-
218
- function handleKey(index: number, e: KeyboardEvent) {
240
+ };
219
241
  switch (e.key) {
220
- case "Up":
221
242
  case "ArrowUp": {
222
- if (index > 0) {
243
+ const target = findFocusable(index - 1, -1);
244
+ if (target >= 0) {
223
245
  e.preventDefault();
224
- itemStates[index - 1].setFocused();
246
+ itemStates[target].setFocused();
247
+ clearSearch();
225
248
  }
226
249
  break;
227
250
  }
228
- case "Down":
229
251
  case "ArrowDown": {
230
- if (index < itemStates.length - 1) {
252
+ const target = findFocusable(index + 1, 1);
253
+ if (target < itemStates.length) {
231
254
  e.preventDefault();
232
- itemStates[index + 1].setFocused();
255
+ itemStates[target].setFocused();
256
+ clearSearch();
233
257
  }
234
258
  break;
235
259
  }
236
260
  case "Home": {
237
261
  if (index > 0) {
238
262
  e.preventDefault();
239
- itemStates[0].setFocused();
263
+ itemStates[findFocusable(0, 1)].setFocused();
264
+ clearSearch();
240
265
  }
241
266
  break;
242
267
  }
243
268
  case "End": {
244
269
  if (index < itemStates.length - 1) {
245
270
  e.preventDefault();
246
- itemStates[itemStates.length - 1].setFocused();
271
+ itemStates[findFocusable(itemStates.length - 1, -1)].setFocused();
272
+ clearSearch();
247
273
  }
248
274
  break;
249
275
  }
250
276
  case "Escape": {
251
277
  if (handleButton) {
252
- handleButton("cancel")
278
+ handleButton("cancel");
279
+ if (popoutContext) {
280
+ popoutContext.searchText = undefined;
281
+ } else {
282
+ searchText = undefined;
283
+ }
284
+ }
285
+ break;
286
+ }
287
+ case "Backspace": {
288
+ if (popoutContext?.searchText || searchText) {
289
+ e.preventDefault();
290
+ const shorten = (str: string) => str.substring(0, str.length - 1);
291
+ if (popoutContext) {
292
+ popoutContext.searchText = shorten(popoutContext.searchText!);
293
+ searchText = popoutContext.searchText;
294
+ } else {
295
+ searchText = shorten(searchText!);
296
+ }
297
+ if (searchText.length > 0) {
298
+ const found = searchItems(searchText);
299
+ if (found) {
300
+ found.setFocused();
301
+ }
302
+ }
253
303
  }
254
304
  break;
255
305
  }
306
+ default: {
307
+ // TODO: Handle unicode
308
+ if (e.key.length === 1) {
309
+ e.preventDefault();
310
+ const lengthen = (str: string) => (str ??= "") + e.key;
311
+ if (popoutContext) {
312
+ popoutContext.searchText = lengthen(popoutContext.searchText!);
313
+ searchText = popoutContext.searchText;
314
+ } else {
315
+ searchText = lengthen(searchText!);
316
+ }
317
+ const found = searchItems(searchText);
318
+ if (found) {
319
+ found.setFocused();
320
+ }
321
+ }
322
+ }
256
323
  }
257
324
  }
258
325
 
@@ -262,10 +329,46 @@ export default function ListBox($props: ListBoxProps | undefined) {
262
329
  class={["torp-list-box", $props?.class]}
263
330
  {$props?.style}
264
331
  role="listbox"
332
+ tabindex="0"
333
+ onfocus={focusSelectedElement}
265
334
  aria-label={$props?.ariaLabel}
266
- aria-multiselectable={type === "multiple" ? "true" : undefined}
335
+ aria-labelledby={$props?.ariaLabelledBy}
336
+ aria-multiselectable={getType() === "multiple" ? "true" : undefined}
337
+ aria-disabled={$props?.disabled ? "true" : undefined}
338
+ aria-orientation={$props?.orientation}
339
+ data-valid={form.valid ? "valid" : "invalid"}
340
+ aria-describedby={form.messageId}
341
+ aria-invalid={form.inField && !form.valid ? "true" : undefined}
342
+ onfocusout={(e: FocusEvent) => {
343
+ // Only validate when focus leaves the whole list box
344
+ if (!root.contains(e.relatedTarget as Node | null)) {
345
+ form.handleBlur();
346
+ }
347
+ }}
348
+ &ref={root}
267
349
  >
268
- <slot />
350
+ <slot>
351
+ @if (context.getLoadedItems) {
352
+ @// Options loaded from a network loader, rendered automatically
353
+ @try {
354
+ @await {
355
+ <LoadedListBoxItems
356
+ items={context.getLoadedItems!()}
357
+ getItemLabel={context.getItemLabel}
358
+ getItemValue={context.getItemValue}
359
+ />
360
+ } with {
361
+ <div class="torp-list-box-loading" role="status">Loading…</div>
362
+ }
363
+ } catch (err) {
364
+ <div class="torp-list-box-error" role="alert">
365
+ {err instanceof Error ? err.message : String(err)}
366
+ </div>
367
+ }
368
+ }
369
+ </slot>
269
370
  </div>
371
+
372
+ <FormValues name={form.name} value={$state.value} />
270
373
  }
271
374
  }
@@ -1,35 +1,44 @@
1
1
  import type { ClassValue, StyleValue } from "@torpor/view";
2
- import { ListBoxContextName, type ItemState, type ListBoxContext } from "./ListBoxTypes";
2
+ import $handle from "../utils/$handle";
3
+ import getId from "../utils/getId";
4
+ import { type ItemState, type ListBoxContext, ListBoxContextName } from "./ListBoxTypes";
3
5
 
4
6
  interface ListBoxItemProps {
5
7
  /** An ID for the root element */
6
8
  id?: string;
7
9
  /** Classes for the root element */
8
- class?: ClassValue,
10
+ class?: ClassValue;
9
11
  /** Styles for the root element */
10
- style?: StyleValue,
12
+ style?: StyleValue;
11
13
  /** The index of this item in the ListBox. Set this if you will be inserting, removing or sorting items. If you don't set this, it will be set automatically */
12
- index?: number,
14
+ index?: number;
15
+ /** The text for the item. If not set, this will come from it's text content */
16
+ text?: string;
13
17
  /** A value for the item. Set this if you need more control over which item(s) are selected. If you don't set this, the value will be set to the item's initial index */
14
- value?: string,
18
+ value?: string;
15
19
  /** If set to true, the user cannot interact with this ListBoxItem */
16
- disabled?: boolean,
17
- /** Raised when this item is opened or closed */
18
- ontoggle?: (open: boolean) => void,
20
+ disabled?: boolean;
21
+ /** Raised when this item's selected state has changed */
22
+ onchange?: (selected: boolean) => void;
19
23
  }
20
24
 
21
25
  /**
22
26
  * An item in an ListBox.
23
27
  */
24
28
  export default function ListBoxItem($props: ListBoxItemProps | undefined) {
29
+ let defaultId = getId();
30
+
25
31
  let $state: ItemState = $watch({
32
+ id: $props?.id ?? defaultId,
26
33
  index: -1,
27
- value: "",
34
+ text: $props?.text ?? "",
35
+ value: $props?.value,
28
36
  selected: false,
37
+ active: false,
29
38
  disabled: $props?.disabled ?? false,
30
39
  parentDisabled: false,
31
- setFocused
32
- })
40
+ setFocused,
41
+ });
33
42
 
34
43
  let button: HTMLButtonElement;
35
44
 
@@ -40,25 +49,19 @@ export default function ListBoxItem($props: ListBoxItemProps | undefined) {
40
49
  }
41
50
 
42
51
  // Register this item with the parent ListBox, which will handle toggling selected for all items
43
- context.registerItem($state, setFocused);
52
+ context.registerItem($state);
44
53
 
45
54
  // Remove the item from the parent ListBox when this item is destroyed
46
55
  $run(() => {
47
56
  return () => {
48
57
  context.removeItem($state.index);
49
- }
58
+ };
50
59
  });
51
60
 
52
- // Dispatch the toggle event when the value of selected changes
53
- let firstToggle = true;
54
- $run(() => {
55
- const ontoggle = $props?.ontoggle;
61
+ // Dispatch the change event when the value of selected changes
62
+ $handle((first) => {
56
63
  const selected = $state.selected;
57
- if (firstToggle) {
58
- firstToggle = false;
59
- } else if (ontoggle) {
60
- ontoggle(selected);
61
- }
64
+ if (!first) $props?.onchange?.(selected);
62
65
  });
63
66
 
64
67
  // This function is called by the parent ListBox to focus this item's button
@@ -68,37 +71,58 @@ export default function ListBoxItem($props: ListBoxItemProps | undefined) {
68
71
 
69
72
  function handleClick() {
70
73
  // Pass the value to the toggleItem method in the parent ListBox which will handle toggling
71
- // the value of expanded for all items (e.g. to unselect other items if necessary)
74
+ // the value of selected for all items (e.g. to unselect other items if necessary)
72
75
  context.toggleItem($state.value);
73
76
  }
74
77
 
78
+ function handleFocus() {
79
+ if (context.setActiveDescendant) {
80
+ context.setActiveDescendant($state.id);
81
+ }
82
+ }
83
+
75
84
  function handleKey(e: KeyboardEvent) {
76
- // For testing
77
85
  switch (e.key) {
78
- case "Enter":
86
+ case "Enter": {
79
87
  e.preventDefault();
80
88
  context.toggleItem($state.value);
81
89
  break;
82
- default:
90
+ }
91
+ default: {
83
92
  // Pass the key press up to the parent ListBox which will handle
84
93
  // setting the focused button
85
94
  context.handleKey($state.index, e);
86
95
  break;
96
+ }
97
+ }
98
+ }
99
+
100
+ function handleMount(el: HTMLButtonElement) {
101
+ // If the item has no value, set it to the text content; selected is
102
+ // derived from the group value, so it picks this up automatically
103
+ if (!$state.text) {
104
+ $state.text = el.textContent.trim();
105
+ }
106
+ if ($state.value === undefined || $state.value === null) {
107
+ $state.value = $state.text;
87
108
  }
88
109
  }
89
110
 
90
111
  @render {
91
112
  <button
92
- {$props?.id}
113
+ type="button"
114
+ {$state.id}
93
115
  class={["torp-list-box-item", $props?.class]}
94
116
  {$props?.style}
95
117
  role="option"
118
+ tabindex="-1"
96
119
  aria-selected={$state.selected ? "true" : "false"}
97
- aria-disabled={$state.disabled ? "true" : undefined}
98
- data-state={$state.selected ? "active" : undefined}
99
- data-disabled={$state.disabled ? "true" : undefined}
120
+ aria-disabled={$state.disabled || $state.parentDisabled ? "true" : undefined}
121
+ data-state={$state.selected || $state.active ? "active" : undefined}
100
122
  onclick={handleClick}
101
123
  onkeydown={handleKey}
124
+ onfocus={handleFocus}
125
+ onmount={handleMount}
102
126
  &ref={button}
103
127
  >
104
128
  <slot />
@@ -4,9 +4,11 @@ interface ListBoxSeparatorProps {
4
4
  /** An ID for the root element */
5
5
  id?: string;
6
6
  /** Classes for the root element */
7
- class?: ClassValue,
7
+ class?: ClassValue;
8
8
  /** Styles for the root element */
9
- style?: StyleValue,
9
+ style?: StyleValue;
10
+ /** The orientation of the separator */
11
+ orientation?: "vertical" | "horizontal";
10
12
  }
11
13
 
12
14
  /**
@@ -19,6 +21,7 @@ export default function ListBoxSeparator($props: ListBoxSeparatorProps | undefin
19
21
  class={["torp-list-box-separator", $props?.class]}
20
22
  {$props?.style}
21
23
  role="separator"
24
+ aria-orientation={$props?.orientation ?? "horizontal"}
22
25
  >
23
26
  <slot />
24
27
  </div>