@primitiv-ui/react 0.1.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 (585) hide show
  1. package/README.md +79 -0
  2. package/package.json +59 -0
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +40 -0
  4. package/src/AccessibleIcon/README.md +42 -0
  5. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +47 -0
  6. package/src/AccessibleIcon/index.ts +2 -0
  7. package/src/AccessibleIcon/types.ts +8 -0
  8. package/src/Accordion/Accordion.tsx +412 -0
  9. package/src/Accordion/AccordionContext.ts +12 -0
  10. package/src/Accordion/README.md +202 -0
  11. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +237 -0
  12. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +333 -0
  13. package/src/Accordion/__tests__/Accordion.controlled-state.test.tsx +175 -0
  14. package/src/Accordion/__tests__/Accordion.data-attributes.test.tsx +272 -0
  15. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +311 -0
  16. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +119 -0
  17. package/src/Accordion/__tests__/Accordion.forceMount.test.tsx +119 -0
  18. package/src/Accordion/__tests__/Accordion.keyboard-interaction.test.tsx +736 -0
  19. package/src/Accordion/__tests__/Accordion.mouse-interaction.test.tsx +212 -0
  20. package/src/Accordion/__tests__/Accordion.multiple-mode.test.tsx +90 -0
  21. package/src/Accordion/__tests__/Accordion.reading-direction.test.tsx +139 -0
  22. package/src/Accordion/__tests__/Accordion.uncontrolled-state.test.tsx +154 -0
  23. package/src/Accordion/hooks/index.ts +6 -0
  24. package/src/Accordion/hooks/useAccordionContext.ts +1 -0
  25. package/src/Accordion/hooks/useAccordionHeaderContext.ts +10 -0
  26. package/src/Accordion/hooks/useAccordionItem.ts +22 -0
  27. package/src/Accordion/hooks/useAccordionItemContext.ts +1 -0
  28. package/src/Accordion/hooks/useAccordionRoot.ts +151 -0
  29. package/src/Accordion/hooks/useAccordionTrigger.ts +90 -0
  30. package/src/Accordion/index.ts +1 -0
  31. package/src/Accordion/types.ts +81 -0
  32. package/src/Alert/Alert.tsx +43 -0
  33. package/src/Alert/README.md +54 -0
  34. package/src/Alert/__tests__/Alert.test.tsx +28 -0
  35. package/src/Alert/index.ts +2 -0
  36. package/src/Alert/types.ts +5 -0
  37. package/src/Avatar/Avatar.tsx +149 -0
  38. package/src/Avatar/AvatarContext.ts +20 -0
  39. package/src/Avatar/README.md +116 -0
  40. package/src/Avatar/__tests__/Avatar.asChild.test.tsx +53 -0
  41. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +14 -0
  42. package/src/Avatar/__tests__/Avatar.error-handling.test.tsx +30 -0
  43. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +75 -0
  44. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +81 -0
  45. package/src/Avatar/hooks/index.ts +2 -0
  46. package/src/Avatar/hooks/useAvatarContext.ts +1 -0
  47. package/src/Avatar/hooks/useAvatarImage.ts +40 -0
  48. package/src/Avatar/index.ts +3 -0
  49. package/src/Avatar/types.ts +44 -0
  50. package/src/Breadcrumb/Breadcrumb.tsx +234 -0
  51. package/src/Breadcrumb/README.md +111 -0
  52. package/src/Breadcrumb/__tests__/Breadcrumb.asChild.test.tsx +33 -0
  53. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +132 -0
  54. package/src/Breadcrumb/index.ts +2 -0
  55. package/src/Breadcrumb/types.ts +22 -0
  56. package/src/Button/Button.tsx +95 -0
  57. package/src/Button/README.md +112 -0
  58. package/src/Button/__tests__/Button.asChild.test.tsx +91 -0
  59. package/src/Button/__tests__/Button.basic-rendering.test.tsx +126 -0
  60. package/src/Button/__tests__/Button.contract.test.tsx +72 -0
  61. package/src/Button/__tests__/Button.disabled.test.tsx +52 -0
  62. package/src/Button/__tests__/Button.icon-usage.test.tsx +57 -0
  63. package/src/Button/__tests__/Button.keyboard-interaction.test.tsx +70 -0
  64. package/src/Button/index.ts +2 -0
  65. package/src/Button/types.ts +8 -0
  66. package/src/Carousel/Carousel.tsx +708 -0
  67. package/src/Carousel/CarouselContext.ts +11 -0
  68. package/src/Carousel/README.md +848 -0
  69. package/src/Carousel/__tests__/Carousel.asChild.test.tsx +178 -0
  70. package/src/Carousel/__tests__/Carousel.auto-play.test.tsx +617 -0
  71. package/src/Carousel/__tests__/Carousel.basic-rendering.test.tsx +569 -0
  72. package/src/Carousel/__tests__/Carousel.controlled-state.test.tsx +137 -0
  73. package/src/Carousel/__tests__/Carousel.error-handling.test.tsx +81 -0
  74. package/src/Carousel/__tests__/Carousel.ids.test.tsx +111 -0
  75. package/src/Carousel/__tests__/Carousel.imperative-api.test.tsx +213 -0
  76. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +560 -0
  77. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +276 -0
  78. package/src/Carousel/__tests__/Carousel.keyboard-navigation.test.tsx +158 -0
  79. package/src/Carousel/__tests__/Carousel.play-pause.test.tsx +232 -0
  80. package/src/Carousel/__tests__/Carousel.prev-next.test.tsx +68 -0
  81. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +49 -0
  82. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +87 -0
  83. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +179 -0
  84. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +109 -0
  85. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +151 -0
  86. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +183 -0
  87. package/src/Carousel/__tests__/Carousel.touch-interaction.test.tsx +96 -0
  88. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +70 -0
  89. package/src/Carousel/__tests__/Carousel.translations.test.tsx +157 -0
  90. package/src/Carousel/__tests__/Carousel.uncontrolled-state.test.tsx +146 -0
  91. package/src/Carousel/hooks/index.ts +4 -0
  92. package/src/Carousel/hooks/useCarouselContext.ts +13 -0
  93. package/src/Carousel/hooks/useCarouselRoot.ts +450 -0
  94. package/src/Carousel/hooks/useCarouselSlide.ts +45 -0
  95. package/src/Carousel/hooks/useCarouselViewport.ts +290 -0
  96. package/src/Carousel/index.ts +3 -0
  97. package/src/Carousel/types.ts +400 -0
  98. package/src/Checkbox/Checkbox.tsx +228 -0
  99. package/src/Checkbox/CheckboxContext.ts +12 -0
  100. package/src/Checkbox/README.md +156 -0
  101. package/src/Checkbox/__tests__/Checkbox.asChild.test.tsx +69 -0
  102. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +41 -0
  103. package/src/Checkbox/__tests__/Checkbox.controlled-state.test.tsx +82 -0
  104. package/src/Checkbox/__tests__/Checkbox.disabled.test.tsx +15 -0
  105. package/src/Checkbox/__tests__/Checkbox.indeterminate.test.tsx +82 -0
  106. package/src/Checkbox/__tests__/Checkbox.indicator.test.tsx +117 -0
  107. package/src/Checkbox/__tests__/Checkbox.uncontrolled-state.test.tsx +89 -0
  108. package/src/Checkbox/hooks/index.ts +2 -0
  109. package/src/Checkbox/hooks/useCheckboxContext.ts +1 -0
  110. package/src/Checkbox/hooks/useCheckboxRoot.ts +32 -0
  111. package/src/Checkbox/index.ts +1 -0
  112. package/src/Checkbox/types.ts +33 -0
  113. package/src/CheckboxCard/CheckboxCard.tsx +208 -0
  114. package/src/CheckboxCard/CheckboxCardContext.ts +12 -0
  115. package/src/CheckboxCard/README.md +114 -0
  116. package/src/CheckboxCard/__tests__/CheckboxCard.asChild.test.tsx +54 -0
  117. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +58 -0
  118. package/src/CheckboxCard/__tests__/CheckboxCard.controlled-state.test.tsx +77 -0
  119. package/src/CheckboxCard/__tests__/CheckboxCard.disabled.test.tsx +55 -0
  120. package/src/CheckboxCard/__tests__/CheckboxCard.error-handling.test.tsx +20 -0
  121. package/src/CheckboxCard/__tests__/CheckboxCard.indeterminate.test.tsx +60 -0
  122. package/src/CheckboxCard/__tests__/CheckboxCard.indicator.test.tsx +136 -0
  123. package/src/CheckboxCard/__tests__/CheckboxCard.uncontrolled-state.test.tsx +73 -0
  124. package/src/CheckboxCard/hooks/index.ts +2 -0
  125. package/src/CheckboxCard/hooks/useCheckboxCardContext.ts +1 -0
  126. package/src/CheckboxCard/hooks/useCheckboxCardRoot.ts +30 -0
  127. package/src/CheckboxCard/index.ts +3 -0
  128. package/src/CheckboxCard/types.ts +33 -0
  129. package/src/Collapsible/Collapsible.tsx +316 -0
  130. package/src/Collapsible/CollapsibleContext.ts +7 -0
  131. package/src/Collapsible/README.md +174 -0
  132. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +240 -0
  133. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +118 -0
  134. package/src/Collapsible/__tests__/Collapsible.controlled-state.test.tsx +134 -0
  135. package/src/Collapsible/__tests__/Collapsible.disabled.test.tsx +132 -0
  136. package/src/Collapsible/__tests__/Collapsible.error-handling.test.tsx +40 -0
  137. package/src/Collapsible/__tests__/Collapsible.forceMount.test.tsx +111 -0
  138. package/src/Collapsible/__tests__/Collapsible.triggerIcon.test.tsx +93 -0
  139. package/src/Collapsible/__tests__/Collapsible.uncontrolled-state.test.tsx +125 -0
  140. package/src/Collapsible/hooks/index.ts +2 -0
  141. package/src/Collapsible/hooks/useCollapsibleRoot.ts +34 -0
  142. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +49 -0
  143. package/src/Collapsible/index.ts +1 -0
  144. package/src/Collapsible/types.ts +48 -0
  145. package/src/ContextMenu/ContextMenu.tsx +1004 -0
  146. package/src/ContextMenu/ContextMenuContentContext.ts +15 -0
  147. package/src/ContextMenu/ContextMenuContext.ts +21 -0
  148. package/src/ContextMenu/ContextMenuGroupContext.ts +8 -0
  149. package/src/ContextMenu/ContextMenuItemIndicatorContext.ts +8 -0
  150. package/src/ContextMenu/ContextMenuRadioGroupContext.ts +9 -0
  151. package/src/ContextMenu/ContextMenuSubContext.ts +15 -0
  152. package/src/ContextMenu/README.md +275 -0
  153. package/src/ContextMenu/__tests__/ContextMenu.asChild.test.tsx +186 -0
  154. package/src/ContextMenu/__tests__/ContextMenu.basic-rendering.test.tsx +39 -0
  155. package/src/ContextMenu/__tests__/ContextMenu.checkbox-item.test.tsx +145 -0
  156. package/src/ContextMenu/__tests__/ContextMenu.error-handling.test.tsx +113 -0
  157. package/src/ContextMenu/__tests__/ContextMenu.group-label.test.tsx +48 -0
  158. package/src/ContextMenu/__tests__/ContextMenu.item-indicator.test.tsx +88 -0
  159. package/src/ContextMenu/__tests__/ContextMenu.item.test.tsx +106 -0
  160. package/src/ContextMenu/__tests__/ContextMenu.keyboard-interaction.test.tsx +172 -0
  161. package/src/ContextMenu/__tests__/ContextMenu.mouse-interaction.test.tsx +227 -0
  162. package/src/ContextMenu/__tests__/ContextMenu.radio-item.test.tsx +127 -0
  163. package/src/ContextMenu/__tests__/ContextMenu.reading-direction.test.tsx +152 -0
  164. package/src/ContextMenu/__tests__/ContextMenu.separator.test.tsx +47 -0
  165. package/src/ContextMenu/__tests__/ContextMenu.state-modes.test.tsx +119 -0
  166. package/src/ContextMenu/__tests__/ContextMenu.sub.test.tsx +262 -0
  167. package/src/ContextMenu/__tests__/ContextMenu.typeahead.test.tsx +89 -0
  168. package/src/ContextMenu/constants.ts +4 -0
  169. package/src/ContextMenu/index.ts +1 -0
  170. package/src/ContextMenu/types.ts +199 -0
  171. package/src/DirectionProvider/DirectionContext.ts +21 -0
  172. package/src/DirectionProvider/DirectionProvider.tsx +31 -0
  173. package/src/DirectionProvider/README.md +62 -0
  174. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +29 -0
  175. package/src/DirectionProvider/index.ts +3 -0
  176. package/src/DirectionProvider/types.ts +10 -0
  177. package/src/Divider/Divider.tsx +57 -0
  178. package/src/Divider/README.md +57 -0
  179. package/src/Divider/__tests__/Divider.test.tsx +41 -0
  180. package/src/Divider/index.ts +1 -0
  181. package/src/Divider/types.ts +5 -0
  182. package/src/Dropdown/Dropdown.tsx +842 -0
  183. package/src/Dropdown/DropdownContentContext.ts +15 -0
  184. package/src/Dropdown/DropdownContext.ts +17 -0
  185. package/src/Dropdown/DropdownGroupContext.ts +8 -0
  186. package/src/Dropdown/DropdownItemIndicatorContext.ts +13 -0
  187. package/src/Dropdown/DropdownRadioGroupContext.ts +9 -0
  188. package/src/Dropdown/DropdownSubContext.ts +15 -0
  189. package/src/Dropdown/README.md +284 -0
  190. package/src/Dropdown/__tests__/Dropdown.asChild.test.tsx +286 -0
  191. package/src/Dropdown/__tests__/Dropdown.basic-rendering.test.tsx +43 -0
  192. package/src/Dropdown/__tests__/Dropdown.checkbox-item.test.tsx +121 -0
  193. package/src/Dropdown/__tests__/Dropdown.disabled.test.tsx +143 -0
  194. package/src/Dropdown/__tests__/Dropdown.error-handling.test.tsx +85 -0
  195. package/src/Dropdown/__tests__/Dropdown.group-label.test.tsx +68 -0
  196. package/src/Dropdown/__tests__/Dropdown.item-indicator.test.tsx +260 -0
  197. package/src/Dropdown/__tests__/Dropdown.item.test.tsx +72 -0
  198. package/src/Dropdown/__tests__/Dropdown.keyboard-edge-cases.test.tsx +77 -0
  199. package/src/Dropdown/__tests__/Dropdown.keyboard-interaction.test.tsx +310 -0
  200. package/src/Dropdown/__tests__/Dropdown.mouse-interaction.test.tsx +347 -0
  201. package/src/Dropdown/__tests__/Dropdown.radio-item.test.tsx +134 -0
  202. package/src/Dropdown/__tests__/Dropdown.reading-direction.test.tsx +153 -0
  203. package/src/Dropdown/__tests__/Dropdown.separator.test.tsx +46 -0
  204. package/src/Dropdown/__tests__/Dropdown.state-modes.test.tsx +100 -0
  205. package/src/Dropdown/__tests__/Dropdown.sub.test.tsx +185 -0
  206. package/src/Dropdown/__tests__/Dropdown.trigger.test.tsx +110 -0
  207. package/src/Dropdown/__tests__/Dropdown.typeahead.test.tsx +133 -0
  208. package/src/Dropdown/constants.ts +4 -0
  209. package/src/Dropdown/hooks/index.ts +9 -0
  210. package/src/Dropdown/hooks/useCloseSiblingSub.ts +13 -0
  211. package/src/Dropdown/hooks/useDropdownContent.ts +162 -0
  212. package/src/Dropdown/hooks/useDropdownContext.ts +1 -0
  213. package/src/Dropdown/hooks/useDropdownGroup.ts +18 -0
  214. package/src/Dropdown/hooks/useDropdownItem.ts +49 -0
  215. package/src/Dropdown/hooks/useDropdownLabel.ts +15 -0
  216. package/src/Dropdown/hooks/useDropdownRoot.ts +57 -0
  217. package/src/Dropdown/hooks/useDropdownSubContext.ts +1 -0
  218. package/src/Dropdown/hooks/useDropdownTrigger.ts +31 -0
  219. package/src/Dropdown/index.ts +1 -0
  220. package/src/Dropdown/types.ts +200 -0
  221. package/src/EmptyState/EmptyState.tsx +245 -0
  222. package/src/EmptyState/README.md +129 -0
  223. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +32 -0
  224. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +30 -0
  225. package/src/EmptyState/__tests__/EmptyState.Media.test.tsx +34 -0
  226. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +28 -0
  227. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +26 -0
  228. package/src/EmptyState/index.ts +2 -0
  229. package/src/EmptyState/types.ts +21 -0
  230. package/src/Field/Field.tsx +239 -0
  231. package/src/Field/FieldContext.ts +22 -0
  232. package/src/Field/README.md +167 -0
  233. package/src/Field/__tests__/Field.asChild.test.tsx +83 -0
  234. package/src/Field/__tests__/Field.basic-rendering.test.tsx +104 -0
  235. package/src/Field/__tests__/Field.state-cascade.test.tsx +75 -0
  236. package/src/Field/hooks/index.ts +2 -0
  237. package/src/Field/hooks/useFieldContext.ts +1 -0
  238. package/src/Field/hooks/useFieldProps.ts +57 -0
  239. package/src/Field/index.ts +2 -0
  240. package/src/Field/types.ts +33 -0
  241. package/src/Fieldset/Fieldset.tsx +104 -0
  242. package/src/Fieldset/README.md +74 -0
  243. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +81 -0
  244. package/src/Fieldset/__tests__/Fieldset.disabled.test.tsx +41 -0
  245. package/src/Fieldset/index.ts +2 -0
  246. package/src/Fieldset/types.ts +5 -0
  247. package/src/Input/Input.tsx +120 -0
  248. package/src/Input/README.md +180 -0
  249. package/src/Input/__tests__/Input.asChild.test.tsx +85 -0
  250. package/src/Input/__tests__/Input.basic-rendering.test.tsx +118 -0
  251. package/src/Input/__tests__/Input.disabled.test.tsx +49 -0
  252. package/src/Input/__tests__/Input.field-integration.test.tsx +148 -0
  253. package/src/Input/index.ts +2 -0
  254. package/src/Input/types.ts +7 -0
  255. package/src/InputGroup/InputGroup.tsx +228 -0
  256. package/src/InputGroup/README.md +178 -0
  257. package/src/InputGroup/__tests__/InputGroup.asChild.test.tsx +109 -0
  258. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +106 -0
  259. package/src/InputGroup/index.ts +2 -0
  260. package/src/InputGroup/types.ts +13 -0
  261. package/src/MillerColumns/MillerColumns.tsx +329 -0
  262. package/src/MillerColumns/MillerColumnsContext.ts +25 -0
  263. package/src/MillerColumns/README.md +278 -0
  264. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +82 -0
  265. package/src/MillerColumns/__tests__/MillerColumns.asChild.test.tsx +106 -0
  266. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +68 -0
  267. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +52 -0
  268. package/src/MillerColumns/__tests__/MillerColumns.column-projection.test.tsx +161 -0
  269. package/src/MillerColumns/__tests__/MillerColumns.controlled-state.test.tsx +90 -0
  270. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +77 -0
  271. package/src/MillerColumns/__tests__/MillerColumns.disabled-items.test.tsx +65 -0
  272. package/src/MillerColumns/__tests__/MillerColumns.error-handling.test.tsx +57 -0
  273. package/src/MillerColumns/__tests__/MillerColumns.fixtures.ts +15 -0
  274. package/src/MillerColumns/__tests__/MillerColumns.item-indicator.test.tsx +57 -0
  275. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +181 -0
  276. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +47 -0
  277. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +137 -0
  278. package/src/MillerColumns/__tests__/MillerColumns.roving-tabindex.test.tsx +91 -0
  279. package/src/MillerColumns/__tests__/MillerColumns.selection.test.tsx +54 -0
  280. package/src/MillerColumns/__tests__/MillerColumns.uncontrolled-state.test.tsx +70 -0
  281. package/src/MillerColumns/hooks/index.ts +7 -0
  282. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +23 -0
  283. package/src/MillerColumns/hooks/useMillerColumnsColumnContext.ts +1 -0
  284. package/src/MillerColumns/hooks/useMillerColumnsContext.ts +1 -0
  285. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +157 -0
  286. package/src/MillerColumns/hooks/useMillerColumnsItemContext.ts +1 -0
  287. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +76 -0
  288. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  289. package/src/MillerColumns/index.ts +3 -0
  290. package/src/MillerColumns/types.ts +93 -0
  291. package/src/MillerColumns/useMillerColumnsSelection.ts +31 -0
  292. package/src/MillerColumns/utils.ts +75 -0
  293. package/src/Modal/Modal.tsx +474 -0
  294. package/src/Modal/ModalContext.ts +13 -0
  295. package/src/Modal/README.md +207 -0
  296. package/src/Modal/__tests__/Modal.accessibility.test.tsx +167 -0
  297. package/src/Modal/__tests__/Modal.asChild.test.tsx +162 -0
  298. package/src/Modal/__tests__/Modal.click-outside.test.tsx +115 -0
  299. package/src/Modal/__tests__/Modal.content.test.tsx +193 -0
  300. package/src/Modal/__tests__/Modal.controlled-state.test.tsx +120 -0
  301. package/src/Modal/__tests__/Modal.error-handling.test.tsx +30 -0
  302. package/src/Modal/__tests__/Modal.escape-hatches.test.tsx +99 -0
  303. package/src/Modal/__tests__/Modal.imperative-api.test.tsx +119 -0
  304. package/src/Modal/__tests__/Modal.nested.test.tsx +106 -0
  305. package/src/Modal/__tests__/Modal.overlay.test.tsx +99 -0
  306. package/src/Modal/__tests__/Modal.portal.test.tsx +90 -0
  307. package/src/Modal/__tests__/Modal.presence.test.tsx +111 -0
  308. package/src/Modal/__tests__/Modal.trigger.test.tsx +70 -0
  309. package/src/Modal/__tests__/Modal.uncontrolled-state.test.tsx +72 -0
  310. package/src/Modal/__tests__/dialog-polyfill.ts +40 -0
  311. package/src/Modal/hooks/index.ts +4 -0
  312. package/src/Modal/hooks/useModalContent.ts +62 -0
  313. package/src/Modal/hooks/useModalContext.ts +1 -0
  314. package/src/Modal/hooks/useModalRoot.ts +81 -0
  315. package/src/Modal/hooks/useModalTrigger.ts +25 -0
  316. package/src/Modal/index.ts +3 -0
  317. package/src/Modal/types.ts +76 -0
  318. package/src/Portal/Portal.tsx +28 -0
  319. package/src/Portal/README.md +70 -0
  320. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +17 -0
  321. package/src/Portal/index.ts +2 -0
  322. package/src/Portal/types.ts +6 -0
  323. package/src/Progress/Progress.tsx +178 -0
  324. package/src/Progress/ProgressContext.ts +15 -0
  325. package/src/Progress/README.md +112 -0
  326. package/src/Progress/__tests__/Progress.asChild.test.tsx +37 -0
  327. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +65 -0
  328. package/src/Progress/__tests__/Progress.error-handling.test.tsx +40 -0
  329. package/src/Progress/__tests__/Progress.fixtures.ts +7 -0
  330. package/src/Progress/__tests__/Progress.value.test.tsx +83 -0
  331. package/src/Progress/hooks/index.ts +2 -0
  332. package/src/Progress/hooks/useProgressContext.ts +1 -0
  333. package/src/Progress/hooks/useProgressRoot.ts +45 -0
  334. package/src/Progress/index.ts +3 -0
  335. package/src/Progress/types.ts +43 -0
  336. package/src/RadioCard/README.md +133 -0
  337. package/src/RadioCard/RadioCard.tsx +334 -0
  338. package/src/RadioCard/RadioCardContext.ts +23 -0
  339. package/src/RadioCard/RadioCardItemContext.ts +10 -0
  340. package/src/RadioCard/__tests__/RadioCard.asChild.test.tsx +76 -0
  341. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +87 -0
  342. package/src/RadioCard/__tests__/RadioCard.controlled-state.test.tsx +107 -0
  343. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +61 -0
  344. package/src/RadioCard/__tests__/RadioCard.error-handling.test.tsx +35 -0
  345. package/src/RadioCard/__tests__/RadioCard.indicator.test.tsx +119 -0
  346. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +158 -0
  347. package/src/RadioCard/__tests__/RadioCard.orientation.test.tsx +90 -0
  348. package/src/RadioCard/__tests__/RadioCard.reading-direction.test.tsx +65 -0
  349. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +108 -0
  350. package/src/RadioCard/hooks/index.ts +3 -0
  351. package/src/RadioCard/hooks/useRadioCardContext.ts +1 -0
  352. package/src/RadioCard/hooks/useRadioCardItemContext.ts +1 -0
  353. package/src/RadioCard/hooks/useRadioCardRoot.ts +77 -0
  354. package/src/RadioCard/index.ts +4 -0
  355. package/src/RadioCard/types.ts +51 -0
  356. package/src/RadioGroup/README.md +185 -0
  357. package/src/RadioGroup/RadioGroup.tsx +353 -0
  358. package/src/RadioGroup/RadioGroupContext.ts +23 -0
  359. package/src/RadioGroup/RadioGroupItemContext.ts +10 -0
  360. package/src/RadioGroup/__tests__/RadioGroup.asChild.test.tsx +105 -0
  361. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +72 -0
  362. package/src/RadioGroup/__tests__/RadioGroup.controlled-state.test.tsx +109 -0
  363. package/src/RadioGroup/__tests__/RadioGroup.disabled-keydown-guards.test.tsx +68 -0
  364. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +79 -0
  365. package/src/RadioGroup/__tests__/RadioGroup.error-handling.test.tsx +33 -0
  366. package/src/RadioGroup/__tests__/RadioGroup.indicator.test.tsx +85 -0
  367. package/src/RadioGroup/__tests__/RadioGroup.keyboard-interaction.test.tsx +135 -0
  368. package/src/RadioGroup/__tests__/RadioGroup.orientation.test.tsx +90 -0
  369. package/src/RadioGroup/__tests__/RadioGroup.reading-direction.test.tsx +65 -0
  370. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +45 -0
  371. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +105 -0
  372. package/src/RadioGroup/__tests__/RadioGroup.uncontrolled-state.test.tsx +96 -0
  373. package/src/RadioGroup/hooks/index.ts +3 -0
  374. package/src/RadioGroup/hooks/useRadioGroupContext.ts +1 -0
  375. package/src/RadioGroup/hooks/useRadioGroupItemContext.ts +1 -0
  376. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +87 -0
  377. package/src/RadioGroup/index.ts +1 -0
  378. package/src/RadioGroup/types.ts +51 -0
  379. package/src/Select/README.md +203 -0
  380. package/src/Select/Select.tsx +204 -0
  381. package/src/Select/__tests__/Select.asChild.test.tsx +36 -0
  382. package/src/Select/__tests__/Select.basic-rendering.test.tsx +17 -0
  383. package/src/Select/__tests__/Select.controlled-state.test.tsx +69 -0
  384. package/src/Select/__tests__/Select.data-attributes.test.tsx +29 -0
  385. package/src/Select/__tests__/Select.field-integration.test.tsx +150 -0
  386. package/src/Select/__tests__/Select.group.test.tsx +42 -0
  387. package/src/Select/__tests__/Select.placeholder.test.tsx +32 -0
  388. package/src/Select/index.ts +2 -0
  389. package/src/Select/types.ts +89 -0
  390. package/src/SkipNav/README.md +87 -0
  391. package/src/SkipNav/SkipNav.tsx +116 -0
  392. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +23 -0
  393. package/src/SkipNav/__tests__/SkipNav.ids.test.tsx +19 -0
  394. package/src/SkipNav/index.ts +1 -0
  395. package/src/SkipNav/types.ts +26 -0
  396. package/src/Slider/README.md +215 -0
  397. package/src/Slider/Slider.tsx +308 -0
  398. package/src/Slider/SliderContext.ts +24 -0
  399. package/src/Slider/__tests__/Slider.asChild.test.tsx +119 -0
  400. package/src/Slider/__tests__/Slider.basic-rendering.test.tsx +157 -0
  401. package/src/Slider/__tests__/Slider.controlled-state.test.tsx +78 -0
  402. package/src/Slider/__tests__/Slider.disabled.test.tsx +82 -0
  403. package/src/Slider/__tests__/Slider.error-handling.test.tsx +45 -0
  404. package/src/Slider/__tests__/Slider.fixtures.ts +53 -0
  405. package/src/Slider/__tests__/Slider.form.test.tsx +67 -0
  406. package/src/Slider/__tests__/Slider.inverted.test.tsx +112 -0
  407. package/src/Slider/__tests__/Slider.keyboard-interaction.test.tsx +118 -0
  408. package/src/Slider/__tests__/Slider.multiple-thumbs.test.tsx +84 -0
  409. package/src/Slider/__tests__/Slider.orientation.test.tsx +101 -0
  410. package/src/Slider/__tests__/Slider.pointer-interaction.test.tsx +205 -0
  411. package/src/Slider/__tests__/Slider.reading-direction.test.tsx +99 -0
  412. package/src/Slider/__tests__/Slider.uncontrolled-state.test.tsx +69 -0
  413. package/src/Slider/__tests__/Slider.value-commit.test.tsx +103 -0
  414. package/src/Slider/hooks/index.ts +3 -0
  415. package/src/Slider/hooks/useSliderContext.ts +1 -0
  416. package/src/Slider/hooks/useSliderRoot.ts +197 -0
  417. package/src/Slider/hooks/useSliderThumb.ts +77 -0
  418. package/src/Slider/index.ts +3 -0
  419. package/src/Slider/types.ts +48 -0
  420. package/src/Slider/utils.ts +155 -0
  421. package/src/Slot/Slot.tsx +158 -0
  422. package/src/Slot/__tests__/Slot.test.tsx +163 -0
  423. package/src/Slot/__tests__/composeEventHandlers.test.ts +74 -0
  424. package/src/Slot/composeEventHandlers.ts +38 -0
  425. package/src/Slot/index.ts +3 -0
  426. package/src/Slot/types.ts +5 -0
  427. package/src/Status/README.md +50 -0
  428. package/src/Status/Status.tsx +44 -0
  429. package/src/Status/__tests__/Status.test.tsx +28 -0
  430. package/src/Status/index.ts +2 -0
  431. package/src/Status/types.ts +5 -0
  432. package/src/Switch/README.md +121 -0
  433. package/src/Switch/Switch.tsx +167 -0
  434. package/src/Switch/SwitchContext.ts +10 -0
  435. package/src/Switch/__tests__/Switch.asChild.test.tsx +56 -0
  436. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +76 -0
  437. package/src/Switch/__tests__/Switch.contract.test.tsx +109 -0
  438. package/src/Switch/__tests__/Switch.controlled-state.test.tsx +79 -0
  439. package/src/Switch/__tests__/Switch.disabled.test.tsx +60 -0
  440. package/src/Switch/__tests__/Switch.error-handling.test.tsx +20 -0
  441. package/src/Switch/__tests__/Switch.keyboard-interaction.test.tsx +56 -0
  442. package/src/Switch/__tests__/Switch.thumb.test.tsx +84 -0
  443. package/src/Switch/__tests__/Switch.uncontrolled-state.test.tsx +83 -0
  444. package/src/Switch/hooks/index.ts +2 -0
  445. package/src/Switch/hooks/useSwitchContext.ts +1 -0
  446. package/src/Switch/hooks/useSwitchRoot.ts +28 -0
  447. package/src/Switch/index.ts +3 -0
  448. package/src/Switch/types.ts +37 -0
  449. package/src/Table/README.md +205 -0
  450. package/src/Table/Table.tsx +380 -0
  451. package/src/Table/__tests__/Table.Body.test.tsx +61 -0
  452. package/src/Table/__tests__/Table.Caption.test.tsx +70 -0
  453. package/src/Table/__tests__/Table.Cell.test.tsx +73 -0
  454. package/src/Table/__tests__/Table.Footer.test.tsx +61 -0
  455. package/src/Table/__tests__/Table.Head.test.tsx +61 -0
  456. package/src/Table/__tests__/Table.Header.test.tsx +73 -0
  457. package/src/Table/__tests__/Table.Root.test.tsx +49 -0
  458. package/src/Table/__tests__/Table.Row.test.tsx +67 -0
  459. package/src/Table/__tests__/Table.ScrollArea.test.tsx +83 -0
  460. package/src/Table/index.ts +1 -0
  461. package/src/Table/types.ts +63 -0
  462. package/src/Tabs/README.md +110 -0
  463. package/src/Tabs/Tabs.tsx +434 -0
  464. package/src/Tabs/TabsContext.ts +13 -0
  465. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +114 -0
  466. package/src/Tabs/__tests__/Tabs.asChild.test.tsx +91 -0
  467. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +483 -0
  468. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +133 -0
  469. package/src/Tabs/__tests__/Tabs.controlled-state.test.tsx +152 -0
  470. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +203 -0
  471. package/src/Tabs/__tests__/Tabs.error-handling.test.tsx +82 -0
  472. package/src/Tabs/__tests__/Tabs.fixtures.ts +171 -0
  473. package/src/Tabs/__tests__/Tabs.imperative-api.test.tsx +118 -0
  474. package/src/Tabs/__tests__/Tabs.keyboard-interaction.test.tsx +192 -0
  475. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +61 -0
  476. package/src/Tabs/__tests__/Tabs.mouse-interaction.test.tsx +216 -0
  477. package/src/Tabs/__tests__/Tabs.reading-direction.test.tsx +58 -0
  478. package/src/Tabs/__tests__/Tabs.uncontrolled-state.test.tsx +197 -0
  479. package/src/Tabs/hooks/index.ts +4 -0
  480. package/src/Tabs/hooks/useTabsContent.ts +27 -0
  481. package/src/Tabs/hooks/useTabsContext.ts +1 -0
  482. package/src/Tabs/hooks/useTabsRoot.ts +148 -0
  483. package/src/Tabs/hooks/useTabsTrigger.ts +111 -0
  484. package/src/Tabs/index.ts +3 -0
  485. package/src/Tabs/types.ts +99 -0
  486. package/src/Tabs/utils.ts +8 -0
  487. package/src/Textarea/README.md +98 -0
  488. package/src/Textarea/Textarea.tsx +93 -0
  489. package/src/Textarea/__tests__/Textarea.asChild.test.tsx +85 -0
  490. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +107 -0
  491. package/src/Textarea/__tests__/Textarea.disabled.test.tsx +49 -0
  492. package/src/Textarea/__tests__/Textarea.field-integration.test.tsx +134 -0
  493. package/src/Textarea/index.ts +2 -0
  494. package/src/Textarea/types.ts +7 -0
  495. package/src/Toggle/README.md +97 -0
  496. package/src/Toggle/Toggle.tsx +81 -0
  497. package/src/Toggle/__tests__/Toggle.asChild.test.tsx +42 -0
  498. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +28 -0
  499. package/src/Toggle/__tests__/Toggle.controlled-state.test.tsx +60 -0
  500. package/src/Toggle/__tests__/Toggle.disabled.test.tsx +34 -0
  501. package/src/Toggle/__tests__/Toggle.keyboard-interaction.test.tsx +42 -0
  502. package/src/Toggle/__tests__/Toggle.uncontrolled-state.test.tsx +40 -0
  503. package/src/Toggle/index.ts +2 -0
  504. package/src/Toggle/types.ts +23 -0
  505. package/src/ToggleGroup/README.md +137 -0
  506. package/src/ToggleGroup/ToggleGroup.tsx +298 -0
  507. package/src/ToggleGroup/ToggleGroupContext.ts +9 -0
  508. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +65 -0
  509. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +50 -0
  510. package/src/ToggleGroup/__tests__/ToggleGroup.disabled.test.tsx +54 -0
  511. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +151 -0
  512. package/src/ToggleGroup/__tests__/ToggleGroup.multiple-mode.test.tsx +144 -0
  513. package/src/ToggleGroup/__tests__/ToggleGroup.reading-direction.test.tsx +28 -0
  514. package/src/ToggleGroup/__tests__/ToggleGroup.single-mode.test.tsx +139 -0
  515. package/src/ToggleGroup/hooks/index.ts +2 -0
  516. package/src/ToggleGroup/hooks/useToggleGroupContext.ts +1 -0
  517. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +110 -0
  518. package/src/ToggleGroup/index.ts +2 -0
  519. package/src/ToggleGroup/types.ts +72 -0
  520. package/src/Tooltip/README.md +214 -0
  521. package/src/Tooltip/Tooltip.tsx +260 -0
  522. package/src/Tooltip/TooltipContext.ts +20 -0
  523. package/src/Tooltip/__tests__/Tooltip.asChild.test.tsx +77 -0
  524. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +180 -0
  525. package/src/Tooltip/__tests__/Tooltip.controlled-state.test.tsx +128 -0
  526. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  527. package/src/Tooltip/__tests__/Tooltip.focus-interaction.test.tsx +88 -0
  528. package/src/Tooltip/__tests__/Tooltip.hover-interaction.test.tsx +179 -0
  529. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +85 -0
  530. package/src/Tooltip/__tests__/Tooltip.uncontrolled-state.test.tsx +67 -0
  531. package/src/Tooltip/hooks/index.ts +4 -0
  532. package/src/Tooltip/hooks/useTooltipContent.ts +53 -0
  533. package/src/Tooltip/hooks/useTooltipProvider.ts +41 -0
  534. package/src/Tooltip/hooks/useTooltipRoot.ts +106 -0
  535. package/src/Tooltip/hooks/useTooltipTrigger.ts +44 -0
  536. package/src/Tooltip/index.ts +1 -0
  537. package/src/Tooltip/types.ts +64 -0
  538. package/src/Tree/README.md +339 -0
  539. package/src/Tree/Tree.tsx +571 -0
  540. package/src/Tree/TreeContext.ts +24 -0
  541. package/src/Tree/__tests__/Tree.aria.test.tsx +53 -0
  542. package/src/Tree/__tests__/Tree.asChild.test.tsx +134 -0
  543. package/src/Tree/__tests__/Tree.basic-rendering.test.tsx +111 -0
  544. package/src/Tree/__tests__/Tree.branch-behaviour.test.tsx +87 -0
  545. package/src/Tree/__tests__/Tree.controlled-expansion.test.tsx +92 -0
  546. package/src/Tree/__tests__/Tree.data-attributes.test.tsx +88 -0
  547. package/src/Tree/__tests__/Tree.disabled-items.test.tsx +196 -0
  548. package/src/Tree/__tests__/Tree.error-handling.test.tsx +71 -0
  549. package/src/Tree/__tests__/Tree.forceMount.test.tsx +72 -0
  550. package/src/Tree/__tests__/Tree.keyboard-interaction.test.tsx +150 -0
  551. package/src/Tree/__tests__/Tree.multiple-selection.test.tsx +151 -0
  552. package/src/Tree/__tests__/Tree.range-selection.test.tsx +200 -0
  553. package/src/Tree/__tests__/Tree.recursion-depth.test.tsx +73 -0
  554. package/src/Tree/__tests__/Tree.roving-tabindex.test.tsx +117 -0
  555. package/src/Tree/__tests__/Tree.selection-path.test.tsx +404 -0
  556. package/src/Tree/__tests__/Tree.single-selection.test.tsx +108 -0
  557. package/src/Tree/__tests__/Tree.uncontrolled-expansion.test.tsx +69 -0
  558. package/src/Tree/hooks/index.ts +3 -0
  559. package/src/Tree/hooks/useTreeItemKeyboard.ts +86 -0
  560. package/src/Tree/hooks/useTreePath.ts +68 -0
  561. package/src/Tree/hooks/useTreeRoot.ts +279 -0
  562. package/src/Tree/index.ts +3 -0
  563. package/src/Tree/types.ts +224 -0
  564. package/src/Tree/utils.ts +59 -0
  565. package/src/VisuallyHidden/README.md +58 -0
  566. package/src/VisuallyHidden/VisuallyHidden.tsx +67 -0
  567. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +59 -0
  568. package/src/VisuallyHidden/index.ts +2 -0
  569. package/src/VisuallyHidden/types.ts +5 -0
  570. package/src/hooks/index.ts +3 -0
  571. package/src/hooks/useCollection.ts +74 -0
  572. package/src/hooks/useControllableState.ts +81 -0
  573. package/src/hooks/useRovingTabindex.ts +178 -0
  574. package/src/index.ts +38 -0
  575. package/src/test/intersectionObserverPolyfill.ts +83 -0
  576. package/src/test/popoverPolyfill.ts +86 -0
  577. package/src/test/scrollPolyfill.ts +23 -0
  578. package/src/types.ts +13 -0
  579. package/src/utils/__tests__/createStrictContext.test.tsx +69 -0
  580. package/src/utils/__tests__/deriveId.test.ts +28 -0
  581. package/src/utils/__tests__/getKeyToActionMap.test.ts +106 -0
  582. package/src/utils/createStrictContext.ts +49 -0
  583. package/src/utils/deriveId.ts +31 -0
  584. package/src/utils/getKeyToActionMap.ts +95 -0
  585. package/src/utils/index.ts +3 -0
@@ -0,0 +1,105 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { RadioGroup } from "../RadioGroup";
5
+
6
+ describe("RadioGroup roving tabindex", () => {
7
+ it("puts only the first item in the tab sequence when nothing is selected", () => {
8
+ // Arrange & Act
9
+ render(
10
+ <RadioGroup.Root aria-label="Colour">
11
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
12
+ <RadioGroup.Item value="green">Green</RadioGroup.Item>
13
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
14
+ </RadioGroup.Root>,
15
+ );
16
+
17
+ // Assert
18
+ expect(screen.getByRole("radio", { name: "Red" })).toHaveAttribute(
19
+ "tabindex",
20
+ "0",
21
+ );
22
+ expect(screen.getByRole("radio", { name: "Green" })).toHaveAttribute(
23
+ "tabindex",
24
+ "-1",
25
+ );
26
+ expect(screen.getByRole("radio", { name: "Blue" })).toHaveAttribute(
27
+ "tabindex",
28
+ "-1",
29
+ );
30
+ });
31
+
32
+ it("puts only the selected item in the tab sequence when one is selected", () => {
33
+ // Arrange & Act
34
+ render(
35
+ <RadioGroup.Root aria-label="Colour" defaultValue="green">
36
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
37
+ <RadioGroup.Item value="green">Green</RadioGroup.Item>
38
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
39
+ </RadioGroup.Root>,
40
+ );
41
+
42
+ // Assert
43
+ expect(screen.getByRole("radio", { name: "Red" })).toHaveAttribute(
44
+ "tabindex",
45
+ "-1",
46
+ );
47
+ expect(screen.getByRole("radio", { name: "Green" })).toHaveAttribute(
48
+ "tabindex",
49
+ "0",
50
+ );
51
+ expect(screen.getByRole("radio", { name: "Blue" })).toHaveAttribute(
52
+ "tabindex",
53
+ "-1",
54
+ );
55
+ });
56
+
57
+ it("moves the tab stop to a newly selected item", async () => {
58
+ // Arrange
59
+ const user = userEvent.setup();
60
+ render(
61
+ <RadioGroup.Root aria-label="Colour">
62
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
63
+ <RadioGroup.Item value="green">Green</RadioGroup.Item>
64
+ </RadioGroup.Root>,
65
+ );
66
+ const red = screen.getByRole("radio", { name: "Red" });
67
+ const green = screen.getByRole("radio", { name: "Green" });
68
+ expect(red).toHaveAttribute("tabindex", "0");
69
+
70
+ // Act
71
+ await user.click(green);
72
+
73
+ // Assert
74
+ expect(red).toHaveAttribute("tabindex", "-1");
75
+ expect(green).toHaveAttribute("tabindex", "0");
76
+ });
77
+
78
+ it("tabs into the single home-base item and not the others", async () => {
79
+ // Arrange
80
+ const user = userEvent.setup();
81
+ render(
82
+ <>
83
+ <button type="button">Before</button>
84
+ <RadioGroup.Root aria-label="Colour">
85
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
86
+ <RadioGroup.Item value="green">Green</RadioGroup.Item>
87
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
88
+ </RadioGroup.Root>
89
+ <button type="button">After</button>
90
+ </>,
91
+ );
92
+ screen.getByRole("button", { name: "Before" }).focus();
93
+
94
+ // Act
95
+ await user.tab();
96
+
97
+ // Assert: focus lands on the first radio (the home base)
98
+ expect(screen.getByRole("radio", { name: "Red" })).toHaveFocus();
99
+
100
+ // Tabbing again escapes the group entirely — the other radios are
101
+ // skipped because their tabindex is -1.
102
+ await user.tab();
103
+ expect(screen.getByRole("button", { name: "After" })).toHaveFocus();
104
+ });
105
+ });
@@ -0,0 +1,96 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { RadioGroup } from "../RadioGroup";
5
+
6
+ describe("RadioGroup uncontrolled state", () => {
7
+ it("reflects defaultValue on mount", () => {
8
+ // Arrange & Act
9
+ render(
10
+ <RadioGroup.Root aria-label="Colour" defaultValue="blue">
11
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
12
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
13
+ </RadioGroup.Root>,
14
+ );
15
+
16
+ // Assert
17
+ expect(screen.getByRole("radio", { name: "Red" })).toHaveAttribute(
18
+ "aria-checked",
19
+ "false",
20
+ );
21
+ expect(screen.getByRole("radio", { name: "Blue" })).toHaveAttribute(
22
+ "aria-checked",
23
+ "true",
24
+ );
25
+ expect(screen.getByRole("radio", { name: "Blue" })).toHaveAttribute(
26
+ "data-state",
27
+ "checked",
28
+ );
29
+ });
30
+
31
+ it("selects an item on click and un-selects the previous one", async () => {
32
+ // Arrange
33
+ const user = userEvent.setup();
34
+ render(
35
+ <RadioGroup.Root aria-label="Colour" defaultValue="red">
36
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
37
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
38
+ </RadioGroup.Root>,
39
+ );
40
+ const red = screen.getByRole("radio", { name: "Red" });
41
+ const blue = screen.getByRole("radio", { name: "Blue" });
42
+ expect(red).toHaveAttribute("aria-checked", "true");
43
+
44
+ // Act
45
+ await user.click(blue);
46
+
47
+ // Assert
48
+ expect(red).toHaveAttribute("aria-checked", "false");
49
+ expect(blue).toHaveAttribute("aria-checked", "true");
50
+ });
51
+
52
+ it("fires onValueChange with the new value on every distinct selection", async () => {
53
+ // Arrange
54
+ const user = userEvent.setup();
55
+ const onValueChange = vi.fn();
56
+ render(
57
+ <RadioGroup.Root aria-label="Colour" onValueChange={onValueChange}>
58
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
59
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
60
+ </RadioGroup.Root>,
61
+ );
62
+
63
+ // Act
64
+ await user.click(screen.getByRole("radio", { name: "Red" }));
65
+ await user.click(screen.getByRole("radio", { name: "Blue" }));
66
+
67
+ // Assert
68
+ expect(onValueChange).toHaveBeenNthCalledWith(1, "red");
69
+ expect(onValueChange).toHaveBeenNthCalledWith(2, "blue");
70
+ });
71
+
72
+ it("does not re-fire onValueChange when the already-selected item is clicked again", async () => {
73
+ // Arrange
74
+ const user = userEvent.setup();
75
+ const onValueChange = vi.fn();
76
+ render(
77
+ <RadioGroup.Root
78
+ aria-label="Colour"
79
+ defaultValue="red"
80
+ onValueChange={onValueChange}
81
+ >
82
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
83
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
84
+ </RadioGroup.Root>,
85
+ );
86
+
87
+ // Act
88
+ await user.click(screen.getByRole("radio", { name: "Red" }));
89
+ await user.click(screen.getByRole("radio", { name: "Red" }));
90
+
91
+ // Assert: the selection is already "red"; onValueChange should
92
+ // stay quiet because nothing actually changed.
93
+ expect(onValueChange).not.toHaveBeenCalled();
94
+ });
95
+
96
+ });
@@ -0,0 +1,3 @@
1
+ export { useRadioGroupContext } from "./useRadioGroupContext";
2
+ export { useRadioGroupItemContext } from "./useRadioGroupItemContext";
3
+ export { useRadioGroupRoot } from "./useRadioGroupRoot";
@@ -0,0 +1 @@
1
+ export { useRadioGroupContext } from "../RadioGroupContext";
@@ -0,0 +1 @@
1
+ export { useRadioGroupItemContext } from "../RadioGroupItemContext";
@@ -0,0 +1,87 @@
1
+ import { useCallback, useMemo } from "react";
2
+
3
+ import { useCollection, useControllableState } from "../../hooks";
4
+
5
+ type UseRadioGroupRootArgs = {
6
+ defaultValue?: string;
7
+ value?: string;
8
+ onValueChange?: (value: string) => void;
9
+ };
10
+
11
+ type ItemMeta = {
12
+ element: HTMLButtonElement;
13
+ disabled: boolean;
14
+ };
15
+
16
+ export function useRadioGroupRoot({
17
+ defaultValue,
18
+ value: controlledValue,
19
+ onValueChange,
20
+ }: UseRadioGroupRootArgs) {
21
+ const [value, setValue] = useControllableState<string>(
22
+ controlledValue,
23
+ defaultValue,
24
+ onValueChange,
25
+ );
26
+
27
+ const select = useCallback(
28
+ (next: string) => {
29
+ if (value === next) return;
30
+ setValue(next);
31
+ },
32
+ [value, setValue],
33
+ );
34
+
35
+ // Track registered item metadata in a ref (for focus handling) and
36
+ // their ordered values as state — required for the roving-tabindex
37
+ // home base. Disabled values are derived per render from itemsRef:
38
+ // any change that affects the set (mount, unmount, disabled toggle)
39
+ // already re-runs the registrar effect, which updates the keys state
40
+ // and forces the re-render that recomputes the memo.
41
+ const {
42
+ register: registerItemBase,
43
+ itemsRef,
44
+ keys: itemValues,
45
+ } = useCollection<string, ItemMeta>();
46
+
47
+ const registerItem = useCallback(
48
+ (
49
+ itemValue: string,
50
+ element: HTMLButtonElement | null,
51
+ disabled = false,
52
+ ) => {
53
+ registerItemBase(itemValue, element ? { element, disabled } : null);
54
+ },
55
+ [registerItemBase],
56
+ );
57
+
58
+ const disabledValues = useMemo(
59
+ () =>
60
+ new Set(
61
+ Array.from(itemsRef.current.entries())
62
+ .filter(([, meta]) => meta.disabled)
63
+ .map(([v]) => v),
64
+ ),
65
+ // itemValues is a fresh array on every register call (new identity even
66
+ // when the keys are the same), so the memo re-runs whenever any item
67
+ // mounts, unmounts, or toggles disabled — which is exactly the trigger
68
+ // we want for re-deriving disabledValues.
69
+ [itemValues, itemsRef],
70
+ );
71
+
72
+ const focusItem = useCallback(
73
+ (itemValue: string) => {
74
+ itemsRef.current.get(itemValue)?.element.focus();
75
+ },
76
+ [itemsRef],
77
+ );
78
+
79
+ return {
80
+ value,
81
+ select,
82
+ registerItem,
83
+ itemValues,
84
+ disabledValues,
85
+ focusItem,
86
+ };
87
+ }
@@ -0,0 +1 @@
1
+ export * from "./RadioGroup";
@@ -0,0 +1,51 @@
1
+ import { ComponentProps, ReactNode, Ref } from "react";
2
+
3
+ /**
4
+ * Which arrow keys navigate the group. `"both"` (default) accepts all
5
+ * four; `"horizontal"` only Arrow Left/Right; `"vertical"` only Arrow
6
+ * Up/Down.
7
+ */
8
+ export type RadioGroupOrientation = "horizontal" | "vertical" | "both";
9
+
10
+ /** Reading direction — swaps the horizontal arrow pair when `"rtl"`. */
11
+ export type RadioGroupReadingDirection = "ltr" | "rtl";
12
+
13
+ type RadioGroupRootBaseProps = Omit<ComponentProps<"div">, "role"> & {
14
+ children?: ReactNode;
15
+ ref?: Ref<HTMLDivElement>;
16
+ asChild?: boolean;
17
+ orientation?: RadioGroupOrientation;
18
+ dir?: RadioGroupReadingDirection;
19
+ };
20
+
21
+ type RadioGroupRootUncontrolledProps = RadioGroupRootBaseProps & {
22
+ defaultValue?: string;
23
+ value?: never;
24
+ onValueChange?: (value: string) => void;
25
+ };
26
+
27
+ type RadioGroupRootControlledProps = RadioGroupRootBaseProps & {
28
+ defaultValue?: never;
29
+ value: string;
30
+ onValueChange: (value: string) => void;
31
+ };
32
+
33
+ export type RadioGroupRootProps =
34
+ | RadioGroupRootUncontrolledProps
35
+ | RadioGroupRootControlledProps;
36
+
37
+ export type RadioGroupItemProps = Omit<
38
+ ComponentProps<"button">,
39
+ "type" | "role" | "aria-checked" | "value"
40
+ > & {
41
+ value: string;
42
+ children?: ReactNode;
43
+ ref?: Ref<HTMLButtonElement>;
44
+ asChild?: boolean;
45
+ };
46
+
47
+ export type RadioGroupIndicatorProps = ComponentProps<"span"> & {
48
+ children?: ReactNode;
49
+ forceMount?: boolean;
50
+ asChild?: boolean;
51
+ };
@@ -0,0 +1,203 @@
1
+ # Select
2
+
3
+ Headless **Select** — a compound component wrapping the native
4
+ `<select>` / `<option>` / `<optgroup>` elements. Zero styles ship.
5
+
6
+ ```tsx
7
+ import { Select } from "@primitiv-ui/react";
8
+
9
+ <Select.Root defaultValue="apple" aria-label="Pick a fruit">
10
+ <Select.Option value="apple">Apple</Select.Option>
11
+ <Select.Option value="banana">Banana</Select.Option>
12
+ <Select.Option value="cherry">Cherry</Select.Option>
13
+ </Select.Root>;
14
+ ```
15
+
16
+ Because the underlying element is the real `<select>`, the browser owns
17
+ the popup, the keyboard interaction (arrow keys, Home/End, typeahead),
18
+ the mobile UX (iOS/Android wheel pickers), and form submission. No
19
+ positioning JS, no Portal, no anchor positioning.
20
+
21
+ ## Sub-components
22
+
23
+ | Export | Element | ARIA / data hooks | `asChild` |
24
+ | --------------------- | ------------ | -------------------------------------------------- | --------- |
25
+ | `Select.Root` | `<select>` | implicit `role="combobox"`, `data-disabled` | yes |
26
+ | `Select.Option` | `<option>` | implicit `role="option"` | — |
27
+ | `Select.Group` | `<optgroup>` | implicit `role="group"`, `label` as accessible name | — |
28
+ | `Select.Placeholder` | `<option>` | always `value=""`, `disabled`, `hidden` | — |
29
+
30
+ ## State modes
31
+
32
+ ### Uncontrolled
33
+
34
+ Pass `defaultValue` (or omit it). The browser owns the selection.
35
+ `onValueChange` is optional.
36
+
37
+ ```tsx
38
+ <Select.Root defaultValue="banana" aria-label="Pick a fruit">
39
+ <Select.Option value="apple">Apple</Select.Option>
40
+ <Select.Option value="banana">Banana</Select.Option>
41
+ </Select.Root>
42
+ ```
43
+
44
+ ### Controlled
45
+
46
+ Pass `value` and `onValueChange` together. The parent owns the
47
+ selection; the component defers every transition back through the
48
+ callback.
49
+
50
+ ```tsx
51
+ const [fruit, setFruit] = useState("apple");
52
+
53
+ <Select.Root value={fruit} onValueChange={setFruit} aria-label="…">
54
+ <Select.Option value="apple">Apple</Select.Option>
55
+ <Select.Option value="banana">Banana</Select.Option>
56
+ </Select.Root>;
57
+ ```
58
+
59
+ `onValueChange` receives the new selection as a plain string. The
60
+ consumer's own `onChange` (the raw `ChangeEvent`) still fires alongside
61
+ it if provided.
62
+
63
+ ## Placeholder
64
+
65
+ `Select.Placeholder` renders a non-selectable hint that holds the
66
+ initial selection. The underlying option is rendered with `value=""`,
67
+ `disabled`, and `hidden`, so it shows in the closed Select's display
68
+ but is unreachable from the dropdown after the user picks something.
69
+
70
+ ```tsx
71
+ <Select.Root required aria-label="Pick a fruit">
72
+ <Select.Placeholder>Choose a fruit…</Select.Placeholder>
73
+ <Select.Option value="apple">Apple</Select.Option>
74
+ <Select.Option value="banana">Banana</Select.Option>
75
+ </Select.Root>
76
+ ```
77
+
78
+ When a `Select.Placeholder` is among Root's direct children and neither
79
+ `value` nor `defaultValue` is set, Root infers `defaultValue=""` so the
80
+ placeholder — not the first selectable option — is the initial
81
+ selection. Pair with `required` on Root to make the browser's native
82
+ form validation catch an unchosen value at submit time.
83
+
84
+ `asChild` on Root walks direct children only for this detection, so the
85
+ `asChild` + Placeholder combination requires the consumer to set
86
+ `defaultValue=""` explicitly.
87
+
88
+ ## Groups
89
+
90
+ `Select.Group` wraps options in a native `<optgroup>` with a labelled,
91
+ non-selectable heading.
92
+
93
+ ```tsx
94
+ <Select.Root aria-label="Pick a food">
95
+ <Select.Group label="Fruits">
96
+ <Select.Option value="apple">Apple</Select.Option>
97
+ <Select.Option value="banana">Banana</Select.Option>
98
+ </Select.Group>
99
+ <Select.Group label="Vegetables">
100
+ <Select.Option value="carrot">Carrot</Select.Option>
101
+ </Select.Group>
102
+ </Select.Root>
103
+ ```
104
+
105
+ The `label` is announced as the group's accessible name by assistive
106
+ technology.
107
+
108
+ ## Disabled
109
+
110
+ Pass `disabled` on Root to disable the whole control, or on an
111
+ individual `Select.Option` to disable that single choice. The native
112
+ `disabled` attribute does the work; `data-disabled=""` is mirrored on
113
+ the root `<select>` for CSS targeting.
114
+
115
+ ```tsx
116
+ <Select.Root disabled aria-label="Pick a fruit">
117
+ <Select.Option value="apple">Apple</Select.Option>
118
+ </Select.Root>
119
+
120
+ <Select.Root aria-label="Pick a fruit">
121
+ <Select.Option value="apple">Apple</Select.Option>
122
+ <Select.Option value="durian" disabled>Durian (sold out)</Select.Option>
123
+ </Select.Root>
124
+ ```
125
+
126
+ ## Field integration
127
+
128
+ When rendered inside a [`<Field.Root>`](../Field/README.md),
129
+ `Select.Root` reads `FieldContext` and inherits:
130
+
131
+ - `id` (from `field.id`)
132
+ - `aria-describedby` (composed: consumer ids first, then the field's
133
+ `descriptionId`, then `errorId` when invalid)
134
+ - `aria-invalid` (`"true"` when the field is invalid)
135
+ - `disabled`
136
+ - `required`
137
+
138
+ Consumer-supplied props always win — pass an explicit value on the
139
+ `Select.Root` to override any field-derived one. Outside a `<Field.Root>`,
140
+ behaviour is unchanged.
141
+
142
+ ```tsx
143
+ <Field.Root invalid={!!errors.fruit}>
144
+ <Field.Label>Fruit</Field.Label>
145
+ <Select.Root {...register("fruit")}>
146
+ <Select.Placeholder>Choose a fruit…</Select.Placeholder>
147
+ <Select.Option value="apple">Apple</Select.Option>
148
+ <Select.Option value="banana">Banana</Select.Option>
149
+ </Select.Root>
150
+ <Field.ErrorText>{errors.fruit?.message}</Field.ErrorText>
151
+ </Field.Root>
152
+ ```
153
+
154
+ ## Form integration
155
+
156
+ Native `<select>` is a form-associated element. Pass `name` and
157
+ `required`, place the Select inside a `<form>`, and submission carries
158
+ the selected value with no extra wiring.
159
+
160
+ ```tsx
161
+ <form onSubmit={…}>
162
+ <label>
163
+ Fruit
164
+ <Select.Root name="fruit" required>
165
+ <Select.Placeholder>Choose a fruit…</Select.Placeholder>
166
+ <Select.Option value="apple">Apple</Select.Option>
167
+ </Select.Root>
168
+ </label>
169
+ <button type="submit">Submit</button>
170
+ </form>
171
+ ```
172
+
173
+ ## `asChild` composition
174
+
175
+ Root accepts `asChild`. The consumer supplies a single element that
176
+ renders a `<select>` (typically a styled wrapper). Root's `onChange`,
177
+ `data-disabled`, `value` / `defaultValue`, and other native attributes
178
+ are merged onto it.
179
+
180
+ ```tsx
181
+ function StyledSelect(props: ComponentProps<"select">) {
182
+ return <select {...props} className="ds-select" />;
183
+ }
184
+
185
+ <Select.Root asChild value={fruit} onValueChange={setFruit}>
186
+ <StyledSelect>
187
+ <Select.Option value="apple">Apple</Select.Option>
188
+ </StyledSelect>
189
+ </Select.Root>
190
+ ```
191
+
192
+ ## Styling hooks
193
+
194
+ | Attribute | Values | Set on |
195
+ | --------------- | ---------------------------- | ------------- |
196
+ | `data-disabled` | `""` (present when disabled) | `Select.Root` |
197
+
198
+ ## Limitations
199
+
200
+ Native `<select>` only renders text inside `<option>`. Rich item content
201
+ (icons, descriptions, indicators) is not supported. A richer Select and
202
+ a Combobox with filtering are planned — see [Future
203
+ work](../../../../docs/select-future-work.md).