@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,353 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+
3
+ import { useDirection } from "../DirectionProvider";
4
+ import { useRovingTabindex } from "../hooks";
5
+ import { Slot, composeEventHandlers, composeRefs } from "../Slot";
6
+
7
+ import { RadioGroupContext } from "./RadioGroupContext";
8
+ import { RadioGroupItemContext } from "./RadioGroupItemContext";
9
+ import {
10
+ useRadioGroupContext,
11
+ useRadioGroupItemContext,
12
+ useRadioGroupRoot,
13
+ } from "./hooks";
14
+ import {
15
+ RadioGroupIndicatorProps,
16
+ RadioGroupItemProps,
17
+ RadioGroupRootProps,
18
+ } from "./types";
19
+
20
+ /**
21
+ * The root of a RadioGroup — a `<div role="radiogroup">` that owns the
22
+ * selected value and provides {@link RadioGroupContext} to descendant
23
+ * {@link RadioGroupItem | `RadioGroup.Item`}s.
24
+ *
25
+ * Supports two state modes, statically discriminated at the type level:
26
+ *
27
+ * - **Uncontrolled** — pass
28
+ * {@link RadioGroupRootProps.defaultValue | `defaultValue`} (or omit
29
+ * for nothing selected on mount). The component owns the value.
30
+ * - **Controlled** — pass
31
+ * {@link RadioGroupRootProps.value | `value`} *and*
32
+ * {@link RadioGroupRootProps.onValueChange | `onValueChange`}
33
+ * together. The parent owns the value; the component defers every
34
+ * change back through the callback.
35
+ *
36
+ * **ARIA.** `role="radiogroup"` is set automatically. Provide an
37
+ * accessible name via `aria-label` or `aria-labelledby`.
38
+ *
39
+ * **Orientation.** By default (`orientation="both"`) all four arrow keys
40
+ * navigate. Pass `orientation="horizontal"` or `"vertical"` to restrict
41
+ * navigation to a single axis.
42
+ *
43
+ * **Reading direction.** `dir` (`"ltr"` / `"rtl"`) swaps the horizontal
44
+ * arrow pair so Arrow Left moves forward in RTL. When omitted, it is
45
+ * inherited from the nearest {@link DirectionProvider}, falling back to
46
+ * `"ltr"`.
47
+ *
48
+ * **`asChild` prop.** Pass `asChild` to render any consumer-supplied
49
+ * element (e.g. `<menu role="menu">` for dropdown composition) with
50
+ * the RadioGroup's props merged in. The native `<div>` is dropped.
51
+ *
52
+ * @example Uncontrolled
53
+ * ```tsx
54
+ * <RadioGroup.Root defaultValue="comfortable" aria-label="Density">
55
+ * <RadioGroup.Item value="compact">
56
+ * <RadioGroup.Indicator />
57
+ * Compact
58
+ * </RadioGroup.Item>
59
+ * <RadioGroup.Item value="comfortable">
60
+ * <RadioGroup.Indicator />
61
+ * Comfortable
62
+ * </RadioGroup.Item>
63
+ * </RadioGroup.Root>
64
+ * ```
65
+ *
66
+ * @example Controlled
67
+ * ```tsx
68
+ * const [value, setValue] = useState("comfortable");
69
+ *
70
+ * <RadioGroup.Root value={value} onValueChange={setValue} aria-label="…">
71
+ * <RadioGroup.Item value="compact">Compact</RadioGroup.Item>
72
+ * <RadioGroup.Item value="comfortable">Comfortable</RadioGroup.Item>
73
+ * </RadioGroup.Root>
74
+ * ```
75
+ */
76
+ function RadioGroupRoot({
77
+ defaultValue,
78
+ value: controlledValue,
79
+ onValueChange,
80
+ orientation = "both",
81
+ dir,
82
+ asChild = false,
83
+ children,
84
+ ...rest
85
+ }: RadioGroupRootProps) {
86
+ const resolvedDir = dir ?? useDirection();
87
+ const { value, select, registerItem, itemValues, disabledValues, focusItem } =
88
+ useRadioGroupRoot({
89
+ defaultValue,
90
+ value: controlledValue,
91
+ onValueChange,
92
+ });
93
+ const contextValue = useMemo(
94
+ () => ({
95
+ value,
96
+ select,
97
+ registerItem,
98
+ itemValues,
99
+ disabledValues,
100
+ focusItem,
101
+ orientation,
102
+ dir: resolvedDir,
103
+ }),
104
+ [
105
+ value,
106
+ select,
107
+ registerItem,
108
+ itemValues,
109
+ disabledValues,
110
+ focusItem,
111
+ orientation,
112
+ resolvedDir,
113
+ ],
114
+ );
115
+ const rootProps = {
116
+ role: "radiogroup" as const,
117
+ "aria-orientation": orientation === "both" ? undefined : orientation,
118
+ dir: resolvedDir,
119
+ ...rest,
120
+ };
121
+ return (
122
+ <RadioGroupContext.Provider value={contextValue}>
123
+ {asChild ? (
124
+ <Slot {...rootProps}>{children}</Slot>
125
+ ) : (
126
+ <div {...rootProps}>{children}</div>
127
+ )}
128
+ </RadioGroupContext.Provider>
129
+ );
130
+ }
131
+
132
+ RadioGroupRoot.displayName = "RadioGroupRoot";
133
+
134
+ /**
135
+ * An individual radio option inside a RadioGroup — a native
136
+ * `<button role="radio">` that reports its state, participates in the
137
+ * roving tabindex, and handles arrow-key navigation within the group.
138
+ *
139
+ * **Selection.** Clicking an Item (or pressing Space / Enter on the
140
+ * focused Item via native `<button>` behaviour) selects it. The arrow
141
+ * keys enabled by the group's `orientation` move focus and selection to
142
+ * the next or previous non-disabled Item, wrapping at the ends.
143
+ *
144
+ * **Roving tabindex.** Only one Item per group is in the document tab
145
+ * sequence at a time: the selected one if any, otherwise the first
146
+ * non-disabled Item. All others have `tabIndex=-1` so `Tab` escapes
147
+ * the group in a single keystroke.
148
+ *
149
+ * **Disabled.** Passing `disabled` forwards the native attribute (the
150
+ * browser suppresses clicks and removes it from the focus ring) and
151
+ * excludes the Item from arrow-key navigation and the roving-tabindex
152
+ * home base. Styling via `:disabled` works natively.
153
+ *
154
+ * **Styling hook.** `data-state="checked" | "unchecked"` mirrors the
155
+ * selection state for CSS targeting.
156
+ *
157
+ * **`asChild` prop.** Pass `asChild` to render any consumer element
158
+ * (e.g. `<li role="menuitemradio">` for dropdown menu composition)
159
+ * with the Item's ARIA, data-state, tabIndex, onClick, onKeyDown,
160
+ * disabled, and ref merged onto it. The native `<button>` is dropped;
161
+ * consumers who render a non-focusable element are responsible for
162
+ * making it focusable.
163
+ *
164
+ * @throws if rendered outside a `RadioGroup.Root`.
165
+ */
166
+ function RadioGroupItem({
167
+ value,
168
+ children,
169
+ onClick,
170
+ onKeyDown,
171
+ disabled,
172
+ asChild = false,
173
+ ref,
174
+ ...rest
175
+ }: RadioGroupItemProps) {
176
+ const {
177
+ value: selectedValue,
178
+ select,
179
+ registerItem,
180
+ itemValues,
181
+ disabledValues,
182
+ focusItem,
183
+ orientation,
184
+ dir,
185
+ } = useRadioGroupContext();
186
+ const isChecked = selectedValue === value;
187
+ const enabledValues = useMemo(
188
+ () => itemValues.filter((v) => !disabledValues.has(v)),
189
+ [itemValues, disabledValues],
190
+ );
191
+ const isTabStop =
192
+ selectedValue !== undefined ? isChecked : enabledValues[0] === value;
193
+
194
+ const localRef = useRef<HTMLButtonElement | null>(null);
195
+ const setRef = useMemo(() => composeRefs(localRef, ref), [ref]);
196
+
197
+ useEffect(() => {
198
+ registerItem(value, localRef.current, disabled);
199
+ return () => registerItem(value, null);
200
+ }, [value, disabled, registerItem]);
201
+
202
+ const { handleKeyDown } = useRovingTabindex<string>({
203
+ orientation,
204
+ dir,
205
+ navigable: enabledValues,
206
+ currentKey: value,
207
+ onNavigate: (target) => {
208
+ select(target);
209
+ focusItem(target);
210
+ },
211
+ });
212
+
213
+ const itemContextValue = useMemo(() => ({ checked: isChecked }), [isChecked]);
214
+
215
+ const itemProps = {
216
+ ...rest,
217
+ ref: setRef,
218
+ role: "radio" as const,
219
+ "aria-checked": isChecked,
220
+ "data-state": isChecked ? ("checked" as const) : ("unchecked" as const),
221
+ tabIndex: isTabStop ? 0 : -1,
222
+ disabled,
223
+ onClick: composeEventHandlers(onClick, () => select(value)),
224
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
225
+ };
226
+
227
+ return (
228
+ <RadioGroupItemContext.Provider value={itemContextValue}>
229
+ {asChild ? (
230
+ <Slot {...itemProps}>{children}</Slot>
231
+ ) : (
232
+ <button type="button" {...itemProps}>
233
+ {children}
234
+ </button>
235
+ )}
236
+ </RadioGroupItemContext.Provider>
237
+ );
238
+ }
239
+
240
+ RadioGroupItem.displayName = "RadioGroupItem";
241
+
242
+ /**
243
+ * A decorative `<span aria-hidden="true">` that renders its children
244
+ * only while the enclosing {@link RadioGroupItem | `RadioGroup.Item`}
245
+ * is the selected one — typically the filled dot inside a radio
246
+ * control. The radio's accessible state is already conveyed by
247
+ * `aria-checked` on the Item, so the indicator is purely visual.
248
+ *
249
+ * **Styling hook.** Mirrors the parent Item's
250
+ * `data-state="checked" | "unchecked"` so the same CSS rule can target
251
+ * both.
252
+ *
253
+ * **`asChild` prop.** Pass `asChild` to render the consumer's own
254
+ * element (typically an `<svg>` dot) as the indicator itself, with
255
+ * `aria-hidden` and `data-state` merged onto that element rather than
256
+ * a wrapper.
257
+ *
258
+ * **`forceMount` prop.** Pass `forceMount` to keep the indicator in
259
+ * the DOM while unchecked so a CSS exit animation can play against
260
+ * `data-state="unchecked"`. Consumers who use `forceMount` own the
261
+ * exit lifecycle themselves.
262
+ *
263
+ * @example Default span wrapper
264
+ * ```tsx
265
+ * <RadioGroup.Item value="red">
266
+ * <RadioGroup.Indicator />
267
+ * Red
268
+ * </RadioGroup.Item>
269
+ * ```
270
+ *
271
+ * @example Icon as the indicator via `asChild`
272
+ * ```tsx
273
+ * <RadioGroup.Indicator asChild>
274
+ * <svg viewBox="0 0 10 10"><circle cx="5" cy="5" r="3" /></svg>
275
+ * </RadioGroup.Indicator>
276
+ * ```
277
+ *
278
+ * @throws if rendered outside a `RadioGroup.Item`.
279
+ */
280
+ function RadioGroupIndicator({
281
+ children,
282
+ forceMount,
283
+ asChild = false,
284
+ ...rest
285
+ }: RadioGroupIndicatorProps) {
286
+ const { checked } = useRadioGroupItemContext();
287
+ if (!checked && !forceMount) return null;
288
+ const indicatorProps = {
289
+ ...rest,
290
+ "aria-hidden": "true" as const,
291
+ "data-state": checked ? ("checked" as const) : ("unchecked" as const),
292
+ };
293
+ if (asChild) {
294
+ return <Slot {...indicatorProps}>{children}</Slot>;
295
+ }
296
+ return <span {...indicatorProps}>{children}</span>;
297
+ }
298
+
299
+ RadioGroupIndicator.displayName = "RadioGroupIndicator";
300
+
301
+ type TRadioGroupCompound = typeof RadioGroupRoot & {
302
+ Root: typeof RadioGroupRoot;
303
+ Item: typeof RadioGroupItem;
304
+ Indicator: typeof RadioGroupIndicator;
305
+ };
306
+
307
+ /**
308
+ * Headless, accessible **RadioGroup** — a compound component
309
+ * implementing the
310
+ * [WAI-ARIA Radio Group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/)
311
+ * built on native `<button role="radio">` elements. Zero styles ship.
312
+ *
313
+ * `RadioGroup` is both callable (an alias of
314
+ * {@link RadioGroupRoot | `RadioGroup.Root`}) and carries its
315
+ * sub-components as static properties. Prefer the namespaced form in
316
+ * application code for readability and grep-ability.
317
+ *
318
+ * - {@link RadioGroupRoot | `RadioGroup.Root`} — state owner, context
319
+ * provider, `<div role="radiogroup">` wrapper.
320
+ * - {@link RadioGroupItem | `RadioGroup.Item`} — a selectable radio
321
+ * button participating in the roving tabindex.
322
+ * - {@link RadioGroupIndicator | `RadioGroup.Indicator`} — decorative
323
+ * dot, mounted only while the parent Item is selected.
324
+ *
325
+ * @example Minimal usage
326
+ * ```tsx
327
+ * import { RadioGroup } from "@primitiv-ui/react";
328
+ *
329
+ * <RadioGroup.Root defaultValue="compact" aria-label="Density">
330
+ * <RadioGroup.Item value="compact">
331
+ * <RadioGroup.Indicator />
332
+ * Compact
333
+ * </RadioGroup.Item>
334
+ * <RadioGroup.Item value="comfortable">
335
+ * <RadioGroup.Indicator />
336
+ * Comfortable
337
+ * </RadioGroup.Item>
338
+ * </RadioGroup.Root>;
339
+ * ```
340
+ *
341
+ * @see {@link RadioGroupRoot} for state modes.
342
+ * @see {@link RadioGroupItem} for selection, roving tabindex, and keyboard navigation.
343
+ * @see {@link RadioGroupIndicator} for the mount gate and animation hooks.
344
+ */
345
+ const RadioGroupCompound: TRadioGroupCompound = Object.assign(RadioGroupRoot, {
346
+ Root: RadioGroupRoot,
347
+ Item: RadioGroupItem,
348
+ Indicator: RadioGroupIndicator,
349
+ });
350
+
351
+ RadioGroupCompound.displayName = "RadioGroup";
352
+
353
+ export { RadioGroupCompound as RadioGroup };
@@ -0,0 +1,23 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ import { RadioGroupOrientation, RadioGroupReadingDirection } from "./types";
4
+
5
+ export type RadioGroupContextValue = {
6
+ value: string | undefined;
7
+ select: (value: string) => void;
8
+ registerItem: (
9
+ value: string,
10
+ element: HTMLButtonElement | null,
11
+ disabled?: boolean,
12
+ ) => void;
13
+ itemValues: string[];
14
+ disabledValues: Set<string>;
15
+ focusItem: (value: string) => void;
16
+ orientation: RadioGroupOrientation;
17
+ dir: RadioGroupReadingDirection;
18
+ };
19
+
20
+ export const [RadioGroupContext, useRadioGroupContext] =
21
+ createStrictContext<RadioGroupContextValue>(
22
+ "RadioGroup sub-components must be rendered inside a <RadioGroup.Root>.",
23
+ );
@@ -0,0 +1,10 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ export type RadioGroupItemContextValue = {
4
+ checked: boolean;
5
+ };
6
+
7
+ export const [RadioGroupItemContext, useRadioGroupItemContext] =
8
+ createStrictContext<RadioGroupItemContextValue>(
9
+ "RadioGroup.Indicator must be rendered inside a <RadioGroup.Item>.",
10
+ );
@@ -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 asChild composition", () => {
7
+ it("Root asChild renders the consumer element with radiogroup role preserved", () => {
8
+ // Arrange & Act
9
+ render(
10
+ <RadioGroup.Root asChild aria-label="Colour">
11
+ <menu data-testid="menu">
12
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
13
+ </menu>
14
+ </RadioGroup.Root>,
15
+ );
16
+
17
+ // Assert
18
+ const root = screen.getByTestId("menu");
19
+ expect(root.tagName).toBe("MENU");
20
+ expect(root).toHaveAttribute("role", "radiogroup");
21
+ expect(root).toHaveAttribute("aria-label", "Colour");
22
+ });
23
+
24
+ it("Root asChild allows the consumer to override the role for menu contexts", () => {
25
+ // Arrange & Act
26
+ render(
27
+ <RadioGroup.Root asChild aria-label="Colour">
28
+ <menu role="menu" data-testid="menu">
29
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
30
+ </menu>
31
+ </RadioGroup.Root>,
32
+ );
33
+
34
+ // Assert: the consumer's <menu> is the rendered root. Its own role
35
+ // wins (Slot's "child overrides" rule), and Slot must have merged
36
+ // the Root's aria-label onto it — without asChild the aria-label
37
+ // would sit on a wrapping <div> and not on the <menu>.
38
+ const menuEl = screen.getByTestId("menu");
39
+ expect(menuEl).toHaveAttribute("role", "menu");
40
+ expect(menuEl).toHaveAttribute("aria-label", "Colour");
41
+ });
42
+
43
+ it("Item asChild delegates to the child element while keeping ARIA + selection wiring", async () => {
44
+ // Arrange
45
+ const user = userEvent.setup();
46
+ const onValueChange = vi.fn();
47
+ render(
48
+ <RadioGroup.Root aria-label="Colour" onValueChange={onValueChange}>
49
+ <RadioGroup.Item value="red" asChild>
50
+ <li>Red</li>
51
+ </RadioGroup.Item>
52
+ </RadioGroup.Root>,
53
+ );
54
+ const item = screen.getByRole("radio", { name: "Red" });
55
+
56
+ // Assert element is the consumer's <li>
57
+ expect(item.tagName).toBe("LI");
58
+ expect(item).toHaveAttribute("aria-checked", "false");
59
+ expect(item).toHaveAttribute("data-state", "unchecked");
60
+
61
+ // Act
62
+ await user.click(item);
63
+
64
+ // Assert selection wiring still fires via composed onClick
65
+ expect(onValueChange).toHaveBeenCalledWith("red");
66
+ expect(item).toHaveAttribute("aria-checked", "true");
67
+ });
68
+
69
+ it("Item asChild lets the consumer override the role for menu-item contexts", () => {
70
+ // Arrange & Act
71
+ render(
72
+ <RadioGroup.Root aria-label="Colour">
73
+ <RadioGroup.Item value="red" asChild>
74
+ <li role="menuitemradio">Red</li>
75
+ </RadioGroup.Item>
76
+ </RadioGroup.Root>,
77
+ );
78
+
79
+ // Assert
80
+ const item = screen.getByRole("menuitemradio", { name: "Red" });
81
+ expect(item).toHaveAttribute("aria-checked", "false");
82
+ });
83
+
84
+ it("Indicator asChild delegates rendering to the consumer's element", () => {
85
+ // Arrange & Act
86
+ render(
87
+ <RadioGroup.Root aria-label="Colour" defaultValue="red">
88
+ <RadioGroup.Item value="red">
89
+ Red
90
+ <RadioGroup.Indicator asChild>
91
+ <svg data-testid="dot" viewBox="0 0 10 10">
92
+ <circle cx="5" cy="5" r="3" />
93
+ </svg>
94
+ </RadioGroup.Indicator>
95
+ </RadioGroup.Item>
96
+ </RadioGroup.Root>,
97
+ );
98
+
99
+ // Assert
100
+ const dot = screen.getByTestId("dot");
101
+ expect(dot.tagName.toLowerCase()).toBe("svg");
102
+ expect(dot).toHaveAttribute("aria-hidden", "true");
103
+ expect(dot).toHaveAttribute("data-state", "checked");
104
+ });
105
+ });
@@ -0,0 +1,72 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { RadioGroup } from "../RadioGroup";
4
+
5
+ describe("RadioGroup basic rendering", () => {
6
+ it('renders a container with role="radiogroup"', () => {
7
+ // Arrange & Act
8
+ render(
9
+ <RadioGroup.Root aria-label="Colour">
10
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
11
+ </RadioGroup.Root>,
12
+ );
13
+
14
+ // Assert
15
+ expect(
16
+ screen.getByRole("radiogroup", { name: "Colour" }),
17
+ ).toBeInTheDocument();
18
+ });
19
+
20
+ it('renders each item as a <button role="radio"> with aria-checked="false" by default', () => {
21
+ // Arrange & Act
22
+ render(
23
+ <RadioGroup.Root aria-label="Colour">
24
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
25
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
26
+ </RadioGroup.Root>,
27
+ );
28
+ const red = screen.getByRole("radio", { name: "Red" });
29
+ const blue = screen.getByRole("radio", { name: "Blue" });
30
+
31
+ // Assert
32
+ expect(red.tagName).toBe("BUTTON");
33
+ expect(red).toHaveAttribute("aria-checked", "false");
34
+ expect(blue.tagName).toBe("BUTTON");
35
+ expect(blue).toHaveAttribute("aria-checked", "false");
36
+ });
37
+
38
+ it('defaults type="button" on items so they never submit an enclosing form', () => {
39
+ // Arrange & Act
40
+ render(
41
+ <RadioGroup.Root aria-label="Colour">
42
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
43
+ </RadioGroup.Root>,
44
+ );
45
+
46
+ // Assert
47
+ expect(screen.getByRole("radio", { name: "Red" })).toHaveAttribute(
48
+ "type",
49
+ "button",
50
+ );
51
+ });
52
+
53
+ it('sets data-state="unchecked" on each item when nothing is selected', () => {
54
+ // Arrange & Act
55
+ render(
56
+ <RadioGroup.Root aria-label="Colour">
57
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
58
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
59
+ </RadioGroup.Root>,
60
+ );
61
+
62
+ // Assert
63
+ expect(screen.getByRole("radio", { name: "Red" })).toHaveAttribute(
64
+ "data-state",
65
+ "unchecked",
66
+ );
67
+ expect(screen.getByRole("radio", { name: "Blue" })).toHaveAttribute(
68
+ "data-state",
69
+ "unchecked",
70
+ );
71
+ });
72
+ });
@@ -0,0 +1,109 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { useState } from "react";
4
+
5
+ import { RadioGroup } from "../RadioGroup";
6
+
7
+ describe("RadioGroup controlled state", () => {
8
+ it("reflects the controlled `value` prop", () => {
9
+ // Arrange & Act
10
+ const { rerender } = render(
11
+ <RadioGroup.Root
12
+ aria-label="Colour"
13
+ value="red"
14
+ onValueChange={() => {}}
15
+ >
16
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
17
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
18
+ </RadioGroup.Root>,
19
+ );
20
+ expect(screen.getByRole("radio", { name: "Red" })).toHaveAttribute(
21
+ "aria-checked",
22
+ "true",
23
+ );
24
+
25
+ rerender(
26
+ <RadioGroup.Root
27
+ aria-label="Colour"
28
+ value="blue"
29
+ onValueChange={() => {}}
30
+ >
31
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
32
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
33
+ </RadioGroup.Root>,
34
+ );
35
+
36
+ // Assert
37
+ expect(screen.getByRole("radio", { name: "Red" })).toHaveAttribute(
38
+ "aria-checked",
39
+ "false",
40
+ );
41
+ expect(screen.getByRole("radio", { name: "Blue" })).toHaveAttribute(
42
+ "aria-checked",
43
+ "true",
44
+ );
45
+ });
46
+
47
+ it("does not update rendered state when the parent refuses to update `value`", async () => {
48
+ // Arrange
49
+ const user = userEvent.setup();
50
+ const onValueChange = vi.fn();
51
+ render(
52
+ <RadioGroup.Root
53
+ aria-label="Colour"
54
+ value="red"
55
+ onValueChange={onValueChange}
56
+ >
57
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
58
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
59
+ </RadioGroup.Root>,
60
+ );
61
+ const blue = screen.getByRole("radio", { name: "Blue" });
62
+
63
+ // Act
64
+ await user.click(blue);
65
+
66
+ // Assert: callback fires but the pinned `value` prop keeps the
67
+ // rendered state on "red".
68
+ expect(onValueChange).toHaveBeenCalledWith("blue");
69
+ expect(screen.getByRole("radio", { name: "Red" })).toHaveAttribute(
70
+ "aria-checked",
71
+ "true",
72
+ );
73
+ expect(blue).toHaveAttribute("aria-checked", "false");
74
+ });
75
+
76
+ it("lets a parent drive the value end to end", async () => {
77
+ // Arrange
78
+ const user = userEvent.setup();
79
+ function Harness() {
80
+ // Start on "blue" so the pre-click state can only be correct if
81
+ // the controlled prop is honoured — a broken impl would fall back
82
+ // to defaultValue and render nothing selected.
83
+ const [value, setValue] = useState("blue");
84
+ return (
85
+ <RadioGroup.Root
86
+ aria-label="Colour"
87
+ value={value}
88
+ onValueChange={setValue}
89
+ >
90
+ <RadioGroup.Item value="red">Red</RadioGroup.Item>
91
+ <RadioGroup.Item value="blue">Blue</RadioGroup.Item>
92
+ </RadioGroup.Root>
93
+ );
94
+ }
95
+ render(<Harness />);
96
+ const red = screen.getByRole("radio", { name: "Red" });
97
+ const blue = screen.getByRole("radio", { name: "Blue" });
98
+ expect(blue).toHaveAttribute("aria-checked", "true");
99
+
100
+ // Act & Assert
101
+ await user.click(red);
102
+ expect(red).toHaveAttribute("aria-checked", "true");
103
+ expect(blue).toHaveAttribute("aria-checked", "false");
104
+
105
+ await user.click(blue);
106
+ expect(blue).toHaveAttribute("aria-checked", "true");
107
+ expect(red).toHaveAttribute("aria-checked", "false");
108
+ });
109
+ });