@primitiv-ui/react 0.1.28 → 0.1.30

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 (456) hide show
  1. package/README.md +19 -2
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -0,0 +1,475 @@
1
+ import {
2
+ useEffect,
3
+ useId,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type ReactElement,
8
+ } from "react";
9
+
10
+ import { useDirection } from "../DirectionProvider/index.ts";
11
+ import { Slot, composeEventHandlers, composeRefs } from "../Slot/index.ts";
12
+
13
+ import {
14
+ ListboxGroupProvider,
15
+ ListboxProvider,
16
+ useListboxContext,
17
+ useListboxGroupContext,
18
+ } from "./ListboxContext";
19
+ import { useListboxRoot } from "./hooks/index.ts";
20
+ import type {
21
+ ListboxGroupLabelProps,
22
+ ListboxGroupProps,
23
+ ListboxOptionProps,
24
+ ListboxRootProps,
25
+ } from "./types";
26
+
27
+ /**
28
+ * The root of a Listbox — a `<div role="listbox">` that owns the selection,
29
+ * holds the single tab stop, and provides {@link ListboxContext} to its
30
+ * descendant {@link ListboxOption | `Listbox.Option`}s.
31
+ *
32
+ * **Virtual focus, not roving tabindex.** This is the distinguishing choice.
33
+ * Real DOM focus stays on the root; the visually-focused option is published
34
+ * as `aria-activedescendant` and marked `data-highlighted`. Options are never
35
+ * given a `tabIndex` and are never `focus()`ed. That is what lets a text input
36
+ * elsewhere keep DOM focus while driving this list — the combobox / command
37
+ * palette case — which a roving tabstop cannot do. Per APG, the cursor is
38
+ * seeded when the root takes focus (onto the first selected option, or the
39
+ * first enabled one) and the active option is scrolled into view as it moves.
40
+ *
41
+ * Two type modes, statically discriminated at the type level:
42
+ *
43
+ * - **`type="single"`** — at most one option selected. Re-selecting the current
44
+ * option is a no-op, not a deselect. Controlled via `value: string` +
45
+ * `onValueChange: (value: string) => void`.
46
+ * - **`type="multiple"`** — options toggle independently, and the root carries
47
+ * `aria-multiselectable`. Controlled via `value: string[]` +
48
+ * `onValueChange: (value: string[]) => void`.
49
+ *
50
+ * Each mode supports **uncontrolled** (`defaultValue`, or omit for an empty
51
+ * selection) and **controlled** (`value` + `onValueChange`) shapes.
52
+ *
53
+ * **Keyboard.** Arrow keys move the cursor along the
54
+ * {@link ListboxRootBaseProps.orientation | orientation} axis, wrapping at
55
+ * both ends; Home/End jump to the first/last option; Enter and Space select
56
+ * (single) or toggle (multiple) the cursor option; printable characters run a
57
+ * prefix typeahead. Selection is manual by default — set
58
+ * {@link ListboxRootBaseProps.selectionFollowsFocus | `selectionFollowsFocus`}
59
+ * for the APG example's select-as-you-arrow behaviour.
60
+ *
61
+ * In `type="multiple"`, APG's optional modifier shortcuts are implemented too:
62
+ * `Shift+Arrow` moves the cursor and selects what it lands on (without
63
+ * wrapping), `Ctrl`/`Cmd`+`Shift`+`Home`/`End` sweeps the selection to that
64
+ * edge, and `Ctrl`/`Cmd`+`A` selects every enabled option or clears the
65
+ * selection when all are already selected. Every **other** chorded shortcut is
66
+ * deliberately left alone — `Alt+Arrow` and unclaimed `Ctrl`/`Cmd` combinations
67
+ * reach your own `onKeyDown` and the browser untouched, so a toolbar can bind
68
+ * them.
69
+ *
70
+ * **Reordering.** Navigation order is read from the live DOM on each
71
+ * interaction rather than from mount order, so moving an option in place is
72
+ * picked up immediately.
73
+ *
74
+ * **ARIA.** `role="listbox"` on the root, plus `aria-activedescendant`,
75
+ * `aria-multiselectable` (multiple mode only) and `aria-orientation`
76
+ * (horizontal only — vertical is the ARIA default). The root needs an
77
+ * accessible name: pass `aria-label` or `aria-labelledby`.
78
+ *
79
+ * **Styling hooks.** `data-orientation="horizontal" | "vertical"` on the root.
80
+ *
81
+ * @extends HTMLDivElement
82
+ *
83
+ * @example Single selection
84
+ * ```tsx
85
+ * <Listbox.Root type="single" defaultValue="apple" aria-label="Fruit">
86
+ * <Listbox.Option value="apple">Apple</Listbox.Option>
87
+ * <Listbox.Option value="banana">Banana</Listbox.Option>
88
+ * </Listbox.Root>
89
+ * ```
90
+ *
91
+ * @example Multiple selection, controlled
92
+ * ```tsx
93
+ * const [value, setValue] = useState<string[]>([]);
94
+ *
95
+ * <Listbox.Root type="multiple" value={value} onValueChange={setValue} aria-label="Toppings">
96
+ * <Listbox.Option value="cheese">Cheese</Listbox.Option>
97
+ * <Listbox.Option value="olives">Olives</Listbox.Option>
98
+ * </Listbox.Root>
99
+ * ```
100
+ *
101
+ * @example As a semantic list
102
+ * ```tsx
103
+ * <Listbox.Root type="single" asChild aria-label="Fruit">
104
+ * <ul>
105
+ * <Listbox.Option value="apple" asChild>
106
+ * <li>Apple</li>
107
+ * </Listbox.Option>
108
+ * </ul>
109
+ * </Listbox.Root>
110
+ * ```
111
+ */
112
+ export function ListboxRoot({
113
+ type,
114
+ defaultValue,
115
+ value: controlledValue,
116
+ onValueChange,
117
+ selectionFollowsFocus = false,
118
+ orientation = "vertical",
119
+ dir,
120
+ asChild = false,
121
+ onFocus,
122
+ onBlur,
123
+ onKeyDown,
124
+ children,
125
+ ref,
126
+ ...rest
127
+ }: ListboxRootProps): ReactElement {
128
+ const resolvedDir = dir ?? useDirection();
129
+ const {
130
+ selectedValues,
131
+ select,
132
+ activeValue,
133
+ registerOption,
134
+ getOptionId,
135
+ seedActiveValue,
136
+ clearActiveValue,
137
+ handleKeyDown,
138
+ moveCursor,
139
+ } = useListboxRoot({
140
+ type,
141
+ defaultValue,
142
+ value: controlledValue,
143
+ onValueChange,
144
+ selectionFollowsFocus,
145
+ orientation,
146
+ dir: resolvedDir,
147
+ });
148
+
149
+ const contextValue = useMemo(
150
+ () => ({
151
+ selectedValues,
152
+ select,
153
+ activeValue,
154
+ registerOption,
155
+ getOptionId,
156
+ moveCursor,
157
+ }),
158
+ [
159
+ selectedValues,
160
+ select,
161
+ activeValue,
162
+ registerOption,
163
+ getOptionId,
164
+ moveCursor,
165
+ ],
166
+ );
167
+
168
+ const rootProps = {
169
+ ...rest,
170
+ ref,
171
+ role: "listbox" as const,
172
+ tabIndex: 0,
173
+ "aria-multiselectable": type === "multiple" ? true : undefined,
174
+ "aria-orientation":
175
+ orientation === "horizontal" ? ("horizontal" as const) : undefined,
176
+ "data-orientation": orientation,
177
+ "aria-activedescendant":
178
+ activeValue === undefined ? undefined : getOptionId(activeValue),
179
+ onFocus: composeEventHandlers(onFocus, seedActiveValue),
180
+ onBlur: composeEventHandlers(onBlur, clearActiveValue),
181
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
182
+ };
183
+
184
+ return (
185
+ <ListboxProvider value={contextValue}>
186
+ {asChild ? (
187
+ <Slot {...rootProps}>{children}</Slot>
188
+ ) : (
189
+ <div {...rootProps}>{children}</div>
190
+ )}
191
+ </ListboxProvider>
192
+ );
193
+ }
194
+
195
+ /** @internal */
196
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
197
+ ListboxRoot.displayName = "ListboxRoot";
198
+
199
+ /**
200
+ * A single choice inside a Listbox — a `<div role="option">` reporting its own
201
+ * selected and highlighted state.
202
+ *
203
+ * **No tab stop, no DOM focus.** The option is never focusable; the root owns
204
+ * focus and points at this element with `aria-activedescendant` when the
205
+ * cursor reaches it. Style the cursor from `data-highlighted`, not `:focus`.
206
+ *
207
+ * **Selection.** Clicking an option selects it (single) or toggles it
208
+ * (multiple), as does Enter / Space while the cursor is on it. A click also
209
+ * moves the cursor onto the clicked option, so a following arrow key resumes
210
+ * from there — APG treats the clicked option as the focused one.
211
+ *
212
+ * **Hover does nothing.** Unlike `Select`'s popup listbox, pointing at an
213
+ * option does not move the cursor: a mouse resting over the list would
214
+ * otherwise steal it mid-keystroke. Give hover its own weaker treatment in
215
+ * CSS, distinct from `data-highlighted`.
216
+ *
217
+ * **Disabled.** `disabled` sets `aria-disabled` and removes the option from
218
+ * cursor navigation, focus seeding, and typeahead — it stays in the
219
+ * accessibility tree and in the DOM. This is a house extension: APG's listbox
220
+ * pattern does not itself specify disabled options.
221
+ *
222
+ * **Typeahead matching** reads the option's rendered text, so keep the label
223
+ * in `children`.
224
+ *
225
+ * **Styling hooks.** `data-highlighted=""` when the cursor is on it,
226
+ * `data-disabled=""` when disabled. Selection is read from `aria-selected`.
227
+ *
228
+ * @throws if rendered outside a `Listbox.Root`.
229
+ *
230
+ * @extends HTMLDivElement
231
+ *
232
+ * @example
233
+ * ```tsx
234
+ * <Listbox.Option value="apple">Apple</Listbox.Option>
235
+ * <Listbox.Option value="durian" disabled>Durian (out of stock)</Listbox.Option>
236
+ * ```
237
+ */
238
+ export function ListboxOption({
239
+ value,
240
+ disabled = false,
241
+ asChild = false,
242
+ onClick,
243
+ children,
244
+ ref,
245
+ ...rest
246
+ }: ListboxOptionProps): ReactElement {
247
+ const {
248
+ selectedValues,
249
+ select,
250
+ activeValue,
251
+ registerOption,
252
+ getOptionId,
253
+ moveCursor,
254
+ } = useListboxContext();
255
+ const selected = selectedValues.includes(value);
256
+
257
+ const localRef = useRef<HTMLDivElement | null>(null);
258
+ // Stryker disable next-line ArrayDeclaration: equivalent — localRef is a stable RefObject, so the composed ref behaves identically however the deps are spelled.
259
+ const setRef = useMemo(() => composeRefs(localRef, ref), [ref]);
260
+
261
+ useEffect(() => {
262
+ registerOption(value, localRef.current, disabled);
263
+ return () => registerOption(value, null);
264
+ }, [value, disabled, registerOption]);
265
+
266
+ const optionProps = {
267
+ ...rest,
268
+ ref: setRef,
269
+ id: getOptionId(value),
270
+ role: "option" as const,
271
+ "aria-selected": selected,
272
+ "aria-disabled": disabled || undefined,
273
+ "data-disabled": disabled ? "" : undefined,
274
+ "data-highlighted": activeValue === value ? "" : undefined,
275
+ onClick: composeEventHandlers(onClick, () => {
276
+ if (disabled) return;
277
+ select(value);
278
+ // A click is a deliberate act of choosing, so it also becomes the
279
+ // cursor's position — APG treats the clicked option as the focused one.
280
+ // Without this the two ways of choosing disagree about where you are in
281
+ // the list: focus (which fires first) seeds the cursor onto the
282
+ // previously-selected option, and the next arrow key would resume from
283
+ // there rather than from the row just clicked. Deliberately not
284
+ // `navigateTo`, which would re-apply selectionFollowsFocus and, in
285
+ // multiple mode, toggle the option straight back off.
286
+ moveCursor(value);
287
+ }),
288
+ };
289
+
290
+ return asChild ? (
291
+ <Slot {...optionProps}>{children}</Slot>
292
+ ) : (
293
+ <div {...optionProps}>{children}</div>
294
+ );
295
+ }
296
+
297
+ /** @internal */
298
+ // Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
299
+ ListboxOption.displayName = "ListboxOption";
300
+
301
+ /**
302
+ * A titled cluster of options — a `<div role="group">` carrying the accessible
303
+ * name APG requires on every option group.
304
+ *
305
+ * Grouping is presentational only: the cursor still walks every option in DOM
306
+ * order, crossing group boundaries without stopping, and typeahead searches
307
+ * the whole list.
308
+ *
309
+ * @extends HTMLDivElement
310
+ *
311
+ * @example
312
+ * ```tsx
313
+ * <Listbox.Root type="single" aria-label="Fruit">
314
+ * <Listbox.Group label="Citrus">
315
+ * <Listbox.Option value="lemon">Lemon</Listbox.Option>
316
+ * </Listbox.Group>
317
+ * <Listbox.Group label="Berries">
318
+ * <Listbox.Option value="fig">Fig</Listbox.Option>
319
+ * </Listbox.Group>
320
+ * </Listbox.Root>
321
+ * ```
322
+ */
323
+ export function ListboxGroup({
324
+ label,
325
+ asChild = false,
326
+ children,
327
+ ref,
328
+ ...rest
329
+ }: ListboxGroupProps): ReactElement {
330
+ const labelId = useId();
331
+ const [hasLabel, setHasLabel] = useState(false);
332
+
333
+ const contextValue = useMemo(
334
+ () => ({ labelId, registerLabel: setHasLabel }),
335
+ // Stryker disable next-line ArrayDeclaration: equivalent — labelId is a useId() constant and setHasLabel is a stable setState, so an empty dep array memoises identically.
336
+ [labelId],
337
+ );
338
+
339
+ const groupProps = {
340
+ ...rest,
341
+ ref,
342
+ role: "group" as const,
343
+ // A rendered GroupLabel wins: aria-labelledby outranks aria-label in the
344
+ // accessible-name algorithm, so emitting both would be misleading.
345
+ "aria-labelledby": hasLabel ? labelId : undefined,
346
+ "aria-label": hasLabel ? undefined : label,
347
+ };
348
+
349
+ return (
350
+ <ListboxGroupProvider value={contextValue}>
351
+ {asChild ? (
352
+ <Slot {...groupProps}>{children}</Slot>
353
+ ) : (
354
+ <div {...groupProps}>{children}</div>
355
+ )}
356
+ </ListboxGroupProvider>
357
+ );
358
+ }
359
+
360
+ /** @internal */
361
+ // Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
362
+ ListboxGroup.displayName = "ListboxGroup";
363
+
364
+ /**
365
+ * The visible heading that names its enclosing
366
+ * {@link ListboxGroup | `Listbox.Group`} — APG's grouped-options example
367
+ * labels each group this way rather than with an invisible `aria-label`.
368
+ *
369
+ * Renders `role="presentation"` so the heading is not exposed as an option
370
+ * (only `role="option"` elements may sit inside a listbox), while still being
371
+ * referenceable: the Group points `aria-labelledby` at this element's id,
372
+ * which is derived and wired automatically.
373
+ *
374
+ * @throws if rendered outside a `Listbox.Group`.
375
+ *
376
+ * @extends HTMLDivElement
377
+ *
378
+ * @example
379
+ * ```tsx
380
+ * <Listbox.Group>
381
+ * <Listbox.GroupLabel>Land</Listbox.GroupLabel>
382
+ * <Listbox.Option value="cat">Cat</Listbox.Option>
383
+ * </Listbox.Group>
384
+ * ```
385
+ */
386
+ export function ListboxGroupLabel({
387
+ asChild = false,
388
+ children,
389
+ ref,
390
+ ...rest
391
+ }: ListboxGroupLabelProps): ReactElement {
392
+ const { labelId, registerLabel } = useListboxGroupContext();
393
+
394
+ useEffect(
395
+ () => {
396
+ registerLabel(true);
397
+ return () => registerLabel(false);
398
+ },
399
+ // Stryker disable next-line ArrayDeclaration: equivalent — registerLabel is a stable setState from the Group above.
400
+ [registerLabel],
401
+ );
402
+
403
+ const labelProps = {
404
+ ...rest,
405
+ ref,
406
+ id: labelId,
407
+ role: "presentation" as const,
408
+ };
409
+
410
+ return asChild ? (
411
+ <Slot {...labelProps}>{children}</Slot>
412
+ ) : (
413
+ <div {...labelProps}>{children}</div>
414
+ );
415
+ }
416
+
417
+ /** @internal */
418
+ // Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
419
+ ListboxGroupLabel.displayName = "ListboxGroupLabel";
420
+
421
+ /**
422
+ * The shape of the exported `Listbox` value — callable as `Listbox.Root` and
423
+ * carrying `Root`, `Option` and `Group` as static properties.
424
+ */
425
+ export type TListboxCompound = typeof ListboxRoot & {
426
+ Root: typeof ListboxRoot;
427
+ Option: typeof ListboxOption;
428
+ Group: typeof ListboxGroup;
429
+ GroupLabel: typeof ListboxGroupLabel;
430
+ };
431
+
432
+ /**
433
+ * Headless, accessible **Listbox** — a persistently-visible list of options
434
+ * implementing the WAI-ARIA APG listbox pattern with a virtual-focus cursor.
435
+ *
436
+ * Unlike `Select`, nothing here opens or closes: there is no trigger and no
437
+ * popup layer. And unlike every roving-tabindex collection in this library,
438
+ * DOM focus never moves to an option — the root keeps it and publishes
439
+ * `aria-activedescendant`, so an external control (a search input, a command
440
+ * palette) can hold focus and drive the list.
441
+ *
442
+ * `Listbox` is both callable (an alias of {@link ListboxRoot | `Listbox.Root`})
443
+ * and carries its sub-components as static properties.
444
+ *
445
+ * - {@link ListboxRoot | `Listbox.Root`} — state owner, context provider, tab
446
+ * stop, `<div role="listbox">` wrapper.
447
+ * - {@link ListboxOption | `Listbox.Option`} — one selectable choice.
448
+ * - {@link ListboxGroup | `Listbox.Group`} — a named cluster of options.
449
+ * - {@link ListboxGroupLabel | `Listbox.GroupLabel`} — a group's visible heading.
450
+ *
451
+ * @example
452
+ * ```tsx
453
+ * import { Listbox } from "@primitiv-ui/react";
454
+ *
455
+ * <Listbox.Root type="single" defaultValue="apple" aria-label="Fruit">
456
+ * <Listbox.Option value="apple">Apple</Listbox.Option>
457
+ * <Listbox.Option value="banana">Banana</Listbox.Option>
458
+ * </Listbox.Root>;
459
+ * ```
460
+ *
461
+ * @see {@link ListboxRoot} for selection modes, keyboard, and ARIA.
462
+ * @see {@link ListboxOption} for per-option state and disabling.
463
+ * @see {@link ListboxGroup} for grouping.
464
+ */
465
+ const ListboxCompound: TListboxCompound = Object.assign(ListboxRoot, {
466
+ Root: ListboxRoot,
467
+ Option: ListboxOption,
468
+ Group: ListboxGroup,
469
+ GroupLabel: ListboxGroupLabel,
470
+ });
471
+
472
+ // Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
473
+ ListboxCompound.displayName = "Listbox";
474
+
475
+ export { ListboxCompound as Listbox };
@@ -0,0 +1,48 @@
1
+ import type { Context, Provider } from "react";
2
+
3
+ import { createStrictContext } from "../utils/index.ts";
4
+
5
+ import type { ListboxContextValue, ListboxGroupContextValue } from "./types";
6
+
7
+ const listboxContextPair = createStrictContext<ListboxContextValue>(
8
+ "Listbox.Option must be rendered as a child of Listbox.Root",
9
+ // Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
10
+ "ListboxContext",
11
+ );
12
+
13
+ /** Strict React context carrying the {@link ListboxContextValue} from
14
+ * `Listbox.Root` to its options. `null` outside a Root. */
15
+ export const ListboxContext: Context<ListboxContextValue | null> =
16
+ listboxContextPair[0];
17
+
18
+ /** Reads the {@link ListboxContextValue}; throws outside a `Listbox.Root`. */
19
+ export const useListboxContext: () => ListboxContextValue =
20
+ listboxContextPair[1];
21
+
22
+ /** Provider for {@link ListboxContext}, used by `Listbox.Root`. */
23
+ const ListboxProvider: Provider<ListboxContextValue | null> =
24
+ ListboxContext.Provider;
25
+
26
+ export { ListboxProvider };
27
+
28
+ const listboxGroupContextPair = createStrictContext<ListboxGroupContextValue>(
29
+ "Listbox.GroupLabel must be rendered as a child of Listbox.Group",
30
+ // Stryker disable next-line StringLiteral: equivalent — a DevTools label, asserted by no behaviour.
31
+ "ListboxGroupContext",
32
+ );
33
+
34
+ /** Strict React context carrying the {@link ListboxGroupContextValue} from
35
+ * `Listbox.Group` to its label. `null` outside a Group. */
36
+ export const ListboxGroupContext: Context<ListboxGroupContextValue | null> =
37
+ listboxGroupContextPair[0];
38
+
39
+ /** Reads the {@link ListboxGroupContextValue}; throws outside a
40
+ * `Listbox.Group`. */
41
+ export const useListboxGroupContext: () => ListboxGroupContextValue =
42
+ listboxGroupContextPair[1];
43
+
44
+ /** Provider for {@link ListboxGroupContext}, used by `Listbox.Group`. */
45
+ const ListboxGroupProvider: Provider<ListboxGroupContextValue | null> =
46
+ ListboxGroupContext.Provider;
47
+
48
+ export { ListboxGroupProvider };