@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,133 @@
1
+ # RadioCard
2
+
3
+ Headless, accessible **RadioCard** — a card/tile-shaped radio group variant
4
+ implementing the
5
+ [WAI-ARIA Radio Group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/).
6
+ The entire card surface is the interactive element. Zero styles ship.
7
+
8
+ ```tsx
9
+ import { RadioCard } from "@primitiv-ui/react";
10
+
11
+ <RadioCard.Root defaultValue="pro" aria-label="Plan">
12
+ <RadioCard.Item value="starter">
13
+ <RadioCard.Indicator />
14
+ <h3>Starter</h3>
15
+ <p>Free forever</p>
16
+ </RadioCard.Item>
17
+ <RadioCard.Item value="pro">
18
+ <RadioCard.Indicator />
19
+ <h3>Pro</h3>
20
+ <p>$9/month</p>
21
+ </RadioCard.Item>
22
+ </RadioCard.Root>
23
+ ```
24
+
25
+ ## Sub-components
26
+
27
+ | Export | Element | ARIA / data hooks | `asChild` |
28
+ |--------|---------|------------------|-----------|
29
+ | `RadioCard.Root` | `<div>` | `role="radiogroup"` | yes |
30
+ | `RadioCard.Item` | `<button>` | `role="radio"`, `aria-checked`, `data-state`, `tabIndex` | yes |
31
+ | `RadioCard.Indicator` | `<span>` | `aria-hidden="true"`, `data-state` | yes |
32
+
33
+ ## State modes
34
+
35
+ ### Uncontrolled
36
+
37
+ Pass `defaultValue` (or omit for nothing selected on mount). The component owns
38
+ the value internally.
39
+
40
+ ```tsx
41
+ <RadioCard.Root defaultValue="pro" aria-label="Plan">
42
+ <RadioCard.Item value="starter">Starter</RadioCard.Item>
43
+ <RadioCard.Item value="pro">Pro</RadioCard.Item>
44
+ </RadioCard.Root>
45
+ ```
46
+
47
+ ### Controlled
48
+
49
+ Pass `value` and `onValueChange` together. The parent owns the value.
50
+
51
+ ```tsx
52
+ const [plan, setPlan] = useState("pro");
53
+
54
+ <RadioCard.Root value={plan} onValueChange={setPlan} aria-label="Plan">
55
+ <RadioCard.Item value="starter">Starter</RadioCard.Item>
56
+ <RadioCard.Item value="pro">Pro</RadioCard.Item>
57
+ </RadioCard.Root>
58
+ ```
59
+
60
+ ## Keyboard interaction
61
+
62
+ | Key | Behaviour |
63
+ |-----|-----------|
64
+ | `ArrowDown` / `ArrowRight` | Select and focus the next enabled Item, wrapping at the end |
65
+ | `ArrowUp` / `ArrowLeft` | Select and focus the previous enabled Item, wrapping at the start |
66
+ | `Space` / `Enter` | Select the focused Item (native `<button>` behaviour) |
67
+ | `Tab` | Move focus out of the group (only one Item is in the tab sequence at a time) |
68
+
69
+ ## Orientation
70
+
71
+ By default (`orientation="both"`) all four arrow keys navigate the group.
72
+ Pass `orientation` to restrict navigation to a single axis:
73
+
74
+ - `orientation="horizontal"` — only `ArrowLeft` / `ArrowRight` navigate.
75
+ - `orientation="vertical"` — only `ArrowUp` / `ArrowDown` navigate.
76
+
77
+ A `horizontal` or `vertical` group also reflects the axis to assistive
78
+ technology via `aria-orientation` on the `radiogroup` element.
79
+
80
+ ## Reading direction
81
+
82
+ Pass `dir` (`"ltr"` / `"rtl"`) to swap the horizontal arrow pair, so
83
+ `ArrowLeft` moves forward and `ArrowRight` moves backward in RTL. The
84
+ vertical pair is never swapped. When `dir` is omitted, it is inherited
85
+ from the nearest [`DirectionProvider`](../DirectionProvider/README.md),
86
+ falling back to `"ltr"` when there is no provider. An explicit `dir` prop
87
+ always wins over the inherited value.
88
+
89
+ ## Disabled items
90
+
91
+ Pass `disabled` on any `RadioCard.Item`. The native attribute suppresses clicks,
92
+ removes the item from the focus ring, and excludes it from arrow-key navigation
93
+ and the roving-tabindex home base.
94
+
95
+ ```tsx
96
+ <RadioCard.Root aria-label="Plan">
97
+ <RadioCard.Item value="starter" disabled>Starter</RadioCard.Item>
98
+ <RadioCard.Item value="pro">Pro</RadioCard.Item>
99
+ </RadioCard.Root>
100
+ ```
101
+
102
+ ## `asChild` composition
103
+
104
+ All three sub-components accept `asChild`. The library's ARIA attributes,
105
+ `data-state`, event handlers, and ref are merged onto the consumer's element;
106
+ the default element is dropped.
107
+
108
+ ```tsx
109
+ <RadioCard.Root asChild aria-label="Plan">
110
+ <ul>
111
+ <RadioCard.Item value="pro" asChild>
112
+ <li>Pro</li>
113
+ </RadioCard.Item>
114
+ </ul>
115
+ </RadioCard.Root>
116
+ ```
117
+
118
+ ## Indicator animation hooks
119
+
120
+ `RadioCard.Indicator` mounts only while its Item is selected. Pass `forceMount`
121
+ to keep it in the DOM — `data-state="unchecked"` lets a CSS exit animation play.
122
+
123
+ ```tsx
124
+ <RadioCard.Indicator forceMount>
125
+ <svg viewBox="0 0 10 10"><circle cx="5" cy="5" r="3" /></svg>
126
+ </RadioCard.Indicator>
127
+ ```
128
+
129
+ ## Styling hooks
130
+
131
+ | Attribute | Values | Set on |
132
+ |-----------|--------|--------|
133
+ | `data-state` | `"checked"` \| `"unchecked"` | `RadioCard.Item`, `RadioCard.Indicator` |
@@ -0,0 +1,334 @@
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 { RadioCardContext } from "./RadioCardContext";
8
+ import { RadioCardItemContext } from "./RadioCardItemContext";
9
+ import {
10
+ useRadioCardContext,
11
+ useRadioCardItemContext,
12
+ useRadioCardRoot,
13
+ } from "./hooks";
14
+ import {
15
+ RadioCardIndicatorProps,
16
+ RadioCardItemProps,
17
+ RadioCardRootProps,
18
+ } from "./types";
19
+
20
+ /**
21
+ * The root of a RadioCard group — a `<div role="radiogroup">` that owns the
22
+ * selected value and provides {@link RadioCardContext} to descendant
23
+ * {@link RadioCardItem | `RadioCard.Item`}s.
24
+ *
25
+ * Supports two state modes, statically discriminated at the type level:
26
+ *
27
+ * - **Uncontrolled** — pass
28
+ * {@link RadioCardRootProps.defaultValue | `defaultValue`} (or omit
29
+ * for nothing selected on mount). The component owns the value.
30
+ * - **Controlled** — pass
31
+ * {@link RadioCardRootProps.value | `value`} *and*
32
+ * {@link RadioCardRootProps.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 with the RadioCard's props merged in. The native `<div>` is dropped.
50
+ *
51
+ * @example Uncontrolled
52
+ * ```tsx
53
+ * <RadioCard.Root defaultValue="pro" aria-label="Plan">
54
+ * <RadioCard.Item value="starter">
55
+ * <h3>Starter</h3>
56
+ * <RadioCard.Indicator />
57
+ * </RadioCard.Item>
58
+ * <RadioCard.Item value="pro">
59
+ * <h3>Pro</h3>
60
+ * <RadioCard.Indicator />
61
+ * </RadioCard.Item>
62
+ * </RadioCard.Root>
63
+ * ```
64
+ *
65
+ * @example Controlled
66
+ * ```tsx
67
+ * const [plan, setPlan] = useState("pro");
68
+ *
69
+ * <RadioCard.Root value={plan} onValueChange={setPlan} aria-label="Plan">
70
+ * <RadioCard.Item value="starter">Starter</RadioCard.Item>
71
+ * <RadioCard.Item value="pro">Pro</RadioCard.Item>
72
+ * </RadioCard.Root>
73
+ * ```
74
+ */
75
+ function RadioCardRoot({
76
+ defaultValue,
77
+ value: controlledValue,
78
+ onValueChange,
79
+ orientation = "both",
80
+ dir,
81
+ asChild = false,
82
+ children,
83
+ ...rest
84
+ }: RadioCardRootProps) {
85
+ const resolvedDir = dir ?? useDirection();
86
+ const { value, select, registerItem, itemValues, disabledValues, focusItem } =
87
+ useRadioCardRoot({
88
+ defaultValue,
89
+ value: controlledValue,
90
+ onValueChange,
91
+ });
92
+ const contextValue = useMemo(
93
+ () => ({
94
+ value,
95
+ select,
96
+ registerItem,
97
+ itemValues,
98
+ disabledValues,
99
+ focusItem,
100
+ orientation,
101
+ dir: resolvedDir,
102
+ }),
103
+ [
104
+ value,
105
+ select,
106
+ registerItem,
107
+ itemValues,
108
+ disabledValues,
109
+ focusItem,
110
+ orientation,
111
+ resolvedDir,
112
+ ],
113
+ );
114
+ const rootProps = {
115
+ role: "radiogroup" as const,
116
+ "aria-orientation": orientation === "both" ? undefined : orientation,
117
+ dir: resolvedDir,
118
+ ...rest,
119
+ };
120
+ return (
121
+ <RadioCardContext.Provider value={contextValue}>
122
+ {asChild ? (
123
+ <Slot {...rootProps}>{children}</Slot>
124
+ ) : (
125
+ <div {...rootProps}>{children}</div>
126
+ )}
127
+ </RadioCardContext.Provider>
128
+ );
129
+ }
130
+
131
+ RadioCardRoot.displayName = "RadioCardRoot";
132
+
133
+ /**
134
+ * An individual card option inside a RadioCard group — a native
135
+ * `<button role="radio">` whose entire surface is the interactive area.
136
+ * Participates in the roving tabindex and handles arrow-key navigation.
137
+ *
138
+ * **Selection.** Clicking an Item (or pressing Space / Enter via native
139
+ * `<button>` behaviour) selects it. The arrow keys enabled by the group's
140
+ * `orientation` move focus and selection to the next or previous
141
+ * non-disabled Item, wrapping at the ends.
142
+ *
143
+ * **Roving tabindex.** Only one Item per group is in the document tab
144
+ * sequence at a time: the selected one if any, otherwise the first
145
+ * non-disabled Item.
146
+ *
147
+ * **Disabled.** Passing `disabled` forwards the native attribute and
148
+ * excludes the Item from arrow-key navigation and the roving-tabindex
149
+ * home base.
150
+ *
151
+ * **Styling hook.** `data-state="checked" | "unchecked"` mirrors the
152
+ * selection state for CSS targeting.
153
+ *
154
+ * **`asChild` prop.** Pass `asChild` to render any consumer element with
155
+ * the Item's ARIA, data-state, tabIndex, onClick, onKeyDown, disabled, and
156
+ * ref merged onto it.
157
+ *
158
+ * @throws if rendered outside a `RadioCard.Root`.
159
+ */
160
+ function RadioCardItem({
161
+ value,
162
+ children,
163
+ onClick,
164
+ onKeyDown,
165
+ disabled,
166
+ asChild = false,
167
+ ref,
168
+ ...rest
169
+ }: RadioCardItemProps) {
170
+ const {
171
+ value: selectedValue,
172
+ select,
173
+ registerItem,
174
+ itemValues,
175
+ disabledValues,
176
+ focusItem,
177
+ orientation,
178
+ dir,
179
+ } = useRadioCardContext();
180
+ const isChecked = selectedValue === value;
181
+ const enabledValues = useMemo(
182
+ () => itemValues.filter((v) => !disabledValues.has(v)),
183
+ [itemValues, disabledValues],
184
+ );
185
+ const isTabStop =
186
+ selectedValue !== undefined ? isChecked : enabledValues[0] === value;
187
+
188
+ const localRef = useRef<HTMLButtonElement | null>(null);
189
+ const setRef = useMemo(() => composeRefs(localRef, ref), [ref]);
190
+
191
+ useEffect(() => {
192
+ registerItem(value, localRef.current, disabled);
193
+ return () => registerItem(value, null);
194
+ }, [value, disabled, registerItem]);
195
+
196
+ const { handleKeyDown } = useRovingTabindex<string>({
197
+ orientation,
198
+ dir,
199
+ navigable: enabledValues,
200
+ currentKey: value,
201
+ onNavigate: (target) => {
202
+ select(target);
203
+ focusItem(target);
204
+ },
205
+ });
206
+
207
+ const itemContextValue = useMemo(() => ({ checked: isChecked }), [isChecked]);
208
+
209
+ const itemProps = {
210
+ ...rest,
211
+ ref: setRef,
212
+ role: "radio" as const,
213
+ "aria-checked": isChecked,
214
+ "data-state": isChecked ? ("checked" as const) : ("unchecked" as const),
215
+ tabIndex: isTabStop ? 0 : -1,
216
+ disabled,
217
+ onClick: composeEventHandlers(onClick, () => select(value)),
218
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
219
+ };
220
+
221
+ return (
222
+ <RadioCardItemContext.Provider value={itemContextValue}>
223
+ {asChild ? (
224
+ <Slot {...itemProps}>{children}</Slot>
225
+ ) : (
226
+ <button type="button" {...itemProps}>
227
+ {children}
228
+ </button>
229
+ )}
230
+ </RadioCardItemContext.Provider>
231
+ );
232
+ }
233
+
234
+ RadioCardItem.displayName = "RadioCardItem";
235
+
236
+ /**
237
+ * A decorative `<span aria-hidden="true">` that renders its children
238
+ * only while the enclosing {@link RadioCardItem | `RadioCard.Item`}
239
+ * is the selected one. Purely visual — accessible state is conveyed by
240
+ * `aria-checked` on the Item.
241
+ *
242
+ * **Styling hook.** Mirrors the parent Item's
243
+ * `data-state="checked" | "unchecked"` so the same CSS rule can target both.
244
+ *
245
+ * **`asChild` prop.** Pass `asChild` to render the consumer's own element
246
+ * as the indicator itself, with `aria-hidden` and `data-state` merged in.
247
+ *
248
+ * **`forceMount` prop.** Pass `forceMount` to keep the indicator in the DOM
249
+ * while unchecked so a CSS exit animation can play against
250
+ * `data-state="unchecked"`.
251
+ *
252
+ * @example
253
+ * ```tsx
254
+ * <RadioCard.Item value="pro">
255
+ * <RadioCard.Indicator />
256
+ * Pro
257
+ * </RadioCard.Item>
258
+ * ```
259
+ *
260
+ * @throws if rendered outside a `RadioCard.Item`.
261
+ */
262
+ function RadioCardIndicator({
263
+ children,
264
+ forceMount,
265
+ asChild = false,
266
+ ...rest
267
+ }: RadioCardIndicatorProps) {
268
+ const { checked } = useRadioCardItemContext();
269
+ if (!checked && !forceMount) return null;
270
+ const indicatorProps = {
271
+ ...rest,
272
+ "aria-hidden": "true" as const,
273
+ "data-state": checked ? ("checked" as const) : ("unchecked" as const),
274
+ };
275
+ if (asChild) {
276
+ return <Slot {...indicatorProps}>{children}</Slot>;
277
+ }
278
+ return <span {...indicatorProps}>{children}</span>;
279
+ }
280
+
281
+ RadioCardIndicator.displayName = "RadioCardIndicator";
282
+
283
+ type TRadioCardCompound = typeof RadioCardRoot & {
284
+ Root: typeof RadioCardRoot;
285
+ Item: typeof RadioCardItem;
286
+ Indicator: typeof RadioCardIndicator;
287
+ };
288
+
289
+ /**
290
+ * Headless, accessible **RadioCard** — a card/tile-shaped radio group
291
+ * variant implementing the
292
+ * [WAI-ARIA Radio Group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/).
293
+ * The entire card surface is the interactive element. Zero styles ship.
294
+ *
295
+ * `RadioCard` is both callable (an alias of
296
+ * {@link RadioCardRoot | `RadioCard.Root`}) and carries its sub-components
297
+ * as static properties.
298
+ *
299
+ * - {@link RadioCardRoot | `RadioCard.Root`} — state owner, context provider,
300
+ * `<div role="radiogroup">` wrapper.
301
+ * - {@link RadioCardItem | `RadioCard.Item`} — a selectable card participating
302
+ * in the roving tabindex.
303
+ * - {@link RadioCardIndicator | `RadioCard.Indicator`} — decorative indicator,
304
+ * mounted only while the parent Item is selected.
305
+ *
306
+ * @example Minimal usage
307
+ * ```tsx
308
+ * import { RadioCard } from "@primitiv-ui/react";
309
+ *
310
+ * <RadioCard.Root defaultValue="pro" aria-label="Plan">
311
+ * <RadioCard.Item value="starter">
312
+ * <RadioCard.Indicator />
313
+ * Starter
314
+ * </RadioCard.Item>
315
+ * <RadioCard.Item value="pro">
316
+ * <RadioCard.Indicator />
317
+ * Pro
318
+ * </RadioCard.Item>
319
+ * </RadioCard.Root>
320
+ * ```
321
+ *
322
+ * @see {@link RadioCardRoot} for state modes.
323
+ * @see {@link RadioCardItem} for selection, roving tabindex, and keyboard navigation.
324
+ * @see {@link RadioCardIndicator} for the mount gate and animation hooks.
325
+ */
326
+ const RadioCardCompound: TRadioCardCompound = Object.assign(RadioCardRoot, {
327
+ Root: RadioCardRoot,
328
+ Item: RadioCardItem,
329
+ Indicator: RadioCardIndicator,
330
+ });
331
+
332
+ RadioCardCompound.displayName = "RadioCard";
333
+
334
+ export { RadioCardCompound as RadioCard };
@@ -0,0 +1,23 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ import { RadioCardOrientation, RadioCardReadingDirection } from "./types";
4
+
5
+ export type RadioCardContextValue = {
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: RadioCardOrientation;
17
+ dir: RadioCardReadingDirection;
18
+ };
19
+
20
+ export const [RadioCardContext, useRadioCardContext] =
21
+ createStrictContext<RadioCardContextValue>(
22
+ "RadioCard sub-components must be rendered inside a <RadioCard.Root>.",
23
+ );
@@ -0,0 +1,10 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ export type RadioCardItemContextValue = {
4
+ checked: boolean;
5
+ };
6
+
7
+ export const [RadioCardItemContext, useRadioCardItemContext] =
8
+ createStrictContext<RadioCardItemContextValue>(
9
+ "RadioCard.Indicator must be rendered inside a <RadioCard.Item>.",
10
+ );
@@ -0,0 +1,76 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { RadioCard } from "../RadioCard";
5
+
6
+ describe("RadioCard asChild", () => {
7
+ it("renders the Root as the consumer element when asChild is set", () => {
8
+ // Arrange & Act
9
+ render(
10
+ <RadioCard.Root asChild aria-label="Plan">
11
+ <section>
12
+ <RadioCard.Item value="pro">Pro</RadioCard.Item>
13
+ </section>
14
+ </RadioCard.Root>,
15
+ );
16
+
17
+ // Assert — section rendered, not div; role still present via merged prop
18
+ const group = screen.getByRole("radiogroup", { name: "Plan" });
19
+ expect(group.tagName).toBe("SECTION");
20
+ });
21
+
22
+ it("renders the Item as the consumer element when asChild is set", () => {
23
+ // Arrange & Act
24
+ render(
25
+ <RadioCard.Root aria-label="Plan">
26
+ <RadioCard.Item value="pro" asChild>
27
+ <div>Pro card</div>
28
+ </RadioCard.Item>
29
+ </RadioCard.Root>,
30
+ );
31
+
32
+ // Assert — div rendered, not button; role and aria-checked merged
33
+ const item = screen.getByRole("radio", { name: "Pro card" });
34
+ expect(item.tagName).toBe("DIV");
35
+ expect(item).toHaveAttribute("aria-checked", "false");
36
+ });
37
+
38
+ it("merges onClick onto the asChild Item element", async () => {
39
+ // Arrange
40
+ const user = userEvent.setup();
41
+ const onClick = vi.fn();
42
+ render(
43
+ <RadioCard.Root aria-label="Plan">
44
+ <RadioCard.Item value="pro" asChild onClick={onClick}>
45
+ <div>Pro</div>
46
+ </RadioCard.Item>
47
+ </RadioCard.Root>,
48
+ );
49
+
50
+ // Act
51
+ await user.click(screen.getByRole("radio", { name: "Pro" }));
52
+
53
+ // Assert
54
+ expect(onClick).toHaveBeenCalledOnce();
55
+ });
56
+
57
+ it("renders the Indicator as the consumer element when asChild is set", () => {
58
+ // Arrange & Act
59
+ render(
60
+ <RadioCard.Root aria-label="Plan" defaultValue="pro">
61
+ <RadioCard.Item value="pro">
62
+ <RadioCard.Indicator asChild>
63
+ <svg data-testid="icon" viewBox="0 0 10 10" />
64
+ </RadioCard.Indicator>
65
+ Pro
66
+ </RadioCard.Item>
67
+ </RadioCard.Root>,
68
+ );
69
+
70
+ // Assert — svg rendered, not span; aria-hidden and data-state merged
71
+ const icon = screen.getByTestId("icon");
72
+ expect(icon.tagName).toBe("svg");
73
+ expect(icon).toHaveAttribute("aria-hidden", "true");
74
+ expect(icon).toHaveAttribute("data-state", "checked");
75
+ });
76
+ });
@@ -0,0 +1,87 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { RadioCard } from "../RadioCard";
4
+
5
+ describe("RadioCard basic rendering", () => {
6
+ it('renders a container with role="radiogroup"', () => {
7
+ // Arrange & Act
8
+ render(
9
+ <RadioCard.Root aria-label="Plan">
10
+ <RadioCard.Item value="starter">Starter</RadioCard.Item>
11
+ </RadioCard.Root>,
12
+ );
13
+
14
+ // Assert
15
+ expect(
16
+ screen.getByRole("radiogroup", { name: "Plan" }),
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
+ <RadioCard.Root aria-label="Plan">
24
+ <RadioCard.Item value="starter">Starter</RadioCard.Item>
25
+ <RadioCard.Item value="pro">Pro</RadioCard.Item>
26
+ </RadioCard.Root>,
27
+ );
28
+ const starter = screen.getByRole("radio", { name: "Starter" });
29
+ const pro = screen.getByRole("radio", { name: "Pro" });
30
+
31
+ // Assert
32
+ expect(starter.tagName).toBe("BUTTON");
33
+ expect(starter).toHaveAttribute("aria-checked", "false");
34
+ expect(pro.tagName).toBe("BUTTON");
35
+ expect(pro).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
+ <RadioCard.Root aria-label="Plan">
42
+ <RadioCard.Item value="starter">Starter</RadioCard.Item>
43
+ </RadioCard.Root>,
44
+ );
45
+
46
+ // Assert
47
+ expect(screen.getByRole("radio", { name: "Starter" })).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
+ <RadioCard.Root aria-label="Plan">
57
+ <RadioCard.Item value="starter">Starter</RadioCard.Item>
58
+ <RadioCard.Item value="pro">Pro</RadioCard.Item>
59
+ </RadioCard.Root>,
60
+ );
61
+
62
+ // Assert
63
+ expect(screen.getByRole("radio", { name: "Starter" })).toHaveAttribute(
64
+ "data-state",
65
+ "unchecked",
66
+ );
67
+ expect(screen.getByRole("radio", { name: "Pro" })).toHaveAttribute(
68
+ "data-state",
69
+ "unchecked",
70
+ );
71
+ });
72
+
73
+ it("renders children inside the card item", () => {
74
+ // Arrange & Act
75
+ render(
76
+ <RadioCard.Root aria-label="Plan">
77
+ <RadioCard.Item value="pro">
78
+ <span data-testid="icon">icon</span>
79
+ Pro plan
80
+ </RadioCard.Item>
81
+ </RadioCard.Root>,
82
+ );
83
+
84
+ // Assert
85
+ expect(screen.getByTestId("icon")).toBeInTheDocument();
86
+ });
87
+ });