@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,110 @@
1
+ # Tabs
2
+
3
+ A compound component implementing the
4
+ [WAI-ARIA Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
5
+
6
+ ```tsx
7
+ import { Tabs } from "@primitiv-ui/react";
8
+
9
+ <Tabs.Root defaultValue="overview">
10
+ <Tabs.List label="Account sections">
11
+ <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
12
+ <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
13
+ </Tabs.List>
14
+ <Tabs.Content value="overview">Dashboard…</Tabs.Content>
15
+ <Tabs.Content value="settings">Preferences…</Tabs.Content>
16
+ </Tabs.Root>;
17
+ ```
18
+
19
+ ## Sub-components
20
+
21
+ | Export | Role | Notes |
22
+ | -------------- | ----------- | ---------------------------------------------------------------------------- |
23
+ | `Tabs.Root` | State owner | Uncontrolled (`defaultValue`) or controlled (`value` + `onValueChange`) |
24
+ | `Tabs.List` | `tablist` | Requires `label` or `ariaLabelledBy` for accessibility |
25
+ | `Tabs.Trigger` | `tab` | Supports `asChild` to render any element with tab semantics |
26
+ | `Tabs.Content` | `tabpanel` | Stays mounted when inactive; use `lazyMount` or conditional rendering for deferred/unmount semantics |
27
+
28
+ ## Keyboard interaction
29
+
30
+ | Key | Behaviour |
31
+ | -------------------------- | ------------------------------------------------- |
32
+ | `ArrowRight` / `ArrowLeft` | Move between triggers (horizontal tabs) |
33
+ | `ArrowDown` / `ArrowUp` | Move between triggers (vertical tabs) |
34
+ | `Home` / `End` | Jump to first / last trigger |
35
+ | `Enter` / `Space` | Activate focused trigger (manual activation mode) |
36
+ | `Tab` | Move from tablist into the active panel |
37
+
38
+ ## Reading direction
39
+
40
+ Pass `dir` (`"ltr"` / `"rtl"`) to set the arrow-key direction for horizontal
41
+ tabs and the container's `dir` attribute. When `dir` is omitted, it is
42
+ inherited from the nearest [`DirectionProvider`](../DirectionProvider/README.md),
43
+ falling back to `"ltr"` when there is no provider. An explicit `dir` prop
44
+ always wins over the inherited value.
45
+
46
+ ## State modes
47
+
48
+ - **Uncontrolled** — pass `defaultValue` (or omit for no initial selection).
49
+ - **Controlled** — pass `value` and `onValueChange` together.
50
+
51
+ ## Activation modes
52
+
53
+ - `activationMode="automatic"` (default) — arrow keys immediately activate the panel.
54
+ - `activationMode="manual"` — arrow keys move focus only; `Enter`/`Space` confirms.
55
+
56
+ ## Lazy mounting
57
+
58
+ Pass `lazyMount` on `Tabs.Root` to defer rendering a panel's children until
59
+ that tab is first activated. Once mounted, children remain in the DOM across
60
+ subsequent tab switches (lazy mount, not unmount-on-hide). The
61
+ `<div role="tabpanel">` wrapper always renders so the ARIA relationship between
62
+ trigger and panel is always present.
63
+
64
+ ```tsx
65
+ <Tabs.Root defaultValue="overview" lazyMount>
66
+
67
+ </Tabs.Root>
68
+ ```
69
+
70
+ This is useful when a panel owns expensive initialisation that depends on the
71
+ element being visible — for example, a scroll-snap carousel whose initial scroll
72
+ position must be computed while the panel has real dimensions.
73
+
74
+ ## Imperative API
75
+
76
+ ```tsx
77
+ const ref = useRef<TabsImperativeApi>(null);
78
+ <Tabs.Root ref={ref} defaultValue="a">
79
+
80
+ </Tabs.Root>;
81
+ ref.current?.setActiveTab("b");
82
+ ```
83
+
84
+ ## `asChild` composition
85
+
86
+ `Tabs.Trigger` accepts an `asChild` prop to render any child element with
87
+ full tab semantics. All ARIA attributes, event handlers, and the roving
88
+ `tabIndex` are merged onto the child following the asChild composition
89
+ pattern (child handler runs first, then the trigger's):
90
+
91
+ ```tsx
92
+ <Tabs.Trigger asChild value="settings">
93
+ <Link to="/settings">Settings</Link>
94
+ </Tabs.Trigger>
95
+ ```
96
+
97
+ ## Styling hooks
98
+
99
+ ```css
100
+ [role="tab"][data-state="active"] {
101
+ border-bottom: 2px solid currentColor;
102
+ }
103
+ [role="tabpanel"][data-state="active"] {
104
+ display: block;
105
+ }
106
+ ```
107
+
108
+ Both `data-state` (`"active"` | `"inactive"`) and
109
+ `data-orientation` (`"horizontal"` | `"vertical"`) are available on
110
+ every rendered element.
@@ -0,0 +1,434 @@
1
+ import { forwardRef, Ref } from "react";
2
+
3
+ import { useDirection } from "../DirectionProvider";
4
+ import { Slot, composeRefs } from "../Slot";
5
+
6
+ import {
7
+ useTabsRoot,
8
+ useTabsContext,
9
+ useTabsTrigger,
10
+ useTabsContent,
11
+ } from "./hooks";
12
+ import { TabsProvider } from "./TabsContext";
13
+ import type {
14
+ TabsRootProps,
15
+ TabsListProps,
16
+ TabsTriggerProps,
17
+ TabsContentProps,
18
+ TabsImperativeApi,
19
+ } from "./types";
20
+
21
+ /**
22
+ * The root of a Tabs widget — owns the active value, provides context to
23
+ * descendants, and renders a single container `<div>`.
24
+ *
25
+ * Supports two state modes, statically discriminated at the type level so
26
+ * only one of the two shapes is accepted by TypeScript:
27
+ *
28
+ * - **Uncontrolled** — pass {@link TabsRootProps.defaultValue | `defaultValue`}
29
+ * (or omit it and let the first click seed the state). The component
30
+ * owns and updates the active value internally.
31
+ * - **Controlled** — pass {@link TabsRootProps.value | `value`} *and*
32
+ * {@link TabsRootProps.onValueChange | `onValueChange`} together. The
33
+ * parent owns the active value; the component defers every state change
34
+ * back through the callback.
35
+ *
36
+ * An additional {@link TabsRootProps.onChange | `onChange({ index, name })`}
37
+ * callback fires on every user-driven activation (click or keyboard)
38
+ * independent of the control mode. Use it for analytics or side-effects
39
+ * that shouldn't re-enter the state update path.
40
+ *
41
+ * An imperative handle is exposed via `ref`, exposing
42
+ * {@link TabsImperativeApi.setActiveTab | `setActiveTab(value)`} for
43
+ * programmatic activation (e.g. restoring a remembered tab on mount, or
44
+ * reacting to a deep-linked hash):
45
+ *
46
+ * ```tsx
47
+ * const tabsRef = useRef<TabsImperativeApi>(null);
48
+ * tabsRef.current?.setActiveTab("settings");
49
+ * ```
50
+ *
51
+ * **Runtime validation.** If `value`/`defaultValue` doesn't match any
52
+ * registered `Tabs.Trigger`, a descriptive error is thrown during render
53
+ * and typos surface early in development.
54
+ *
55
+ * **Styling hooks.** `data-orientation="horizontal" | "vertical"` is set
56
+ * on the rendered container.
57
+ *
58
+ * **Reading direction.** `dir` (`"ltr"` / `"rtl"`) sets the arrow-key
59
+ * direction and the container's `dir` attribute. When omitted, it is
60
+ * inherited from the nearest {@link DirectionProvider}, falling back to
61
+ * `"ltr"` when there is no provider.
62
+ *
63
+ * @example Uncontrolled
64
+ * ```tsx
65
+ * <Tabs.Root defaultValue="overview">
66
+ * <Tabs.List label="Account sections">
67
+ * <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
68
+ * <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
69
+ * </Tabs.List>
70
+ * <Tabs.Content value="overview">Dashboard…</Tabs.Content>
71
+ * <Tabs.Content value="settings">Preferences…</Tabs.Content>
72
+ * </Tabs.Root>
73
+ * ```
74
+ *
75
+ * @example Controlled with an analytics hook
76
+ * ```tsx
77
+ * const [tab, setTab] = useState("overview");
78
+ *
79
+ * <Tabs.Root
80
+ * value={tab}
81
+ * onValueChange={setTab}
82
+ * onChange={({ name, index }) => track("tab_view", { name, index })}
83
+ * >
84
+ * …
85
+ * </Tabs.Root>
86
+ * ```
87
+ *
88
+ * @example Vertical orientation + imperative API
89
+ * ```tsx
90
+ * const ref = useRef<TabsImperativeApi>(null);
91
+ *
92
+ * <Tabs.Root ref={ref} orientation="vertical" defaultValue="one">
93
+ * …
94
+ * </Tabs.Root>
95
+ *
96
+ * <button onClick={() => ref.current?.setActiveTab("two")}>Go to two</button>
97
+ * ```
98
+ */
99
+ const TabsRoot = forwardRef<TabsImperativeApi, TabsRootProps>(function TabsRoot(
100
+ {
101
+ className = "",
102
+ orientation = "horizontal",
103
+ dir,
104
+ activationMode = "automatic",
105
+ defaultValue,
106
+ value,
107
+ onValueChange,
108
+ onChange,
109
+ lazyMount = false,
110
+ ...rest
111
+ },
112
+ ref,
113
+ ) {
114
+ const resolvedDir = dir ?? useDirection();
115
+ const { contextValue } = useTabsRoot(
116
+ {
117
+ orientation,
118
+ dir: resolvedDir,
119
+ activationMode,
120
+ defaultValue,
121
+ value,
122
+ onValueChange,
123
+ onChange,
124
+ lazyMount,
125
+ },
126
+ ref,
127
+ );
128
+
129
+ return (
130
+ <TabsProvider value={contextValue}>
131
+ <div
132
+ dir={resolvedDir}
133
+ className={className}
134
+ data-orientation={contextValue.orientation}
135
+ {...rest}
136
+ />
137
+ </TabsProvider>
138
+ );
139
+ });
140
+
141
+ TabsRoot.displayName = "TabsRoot";
142
+
143
+ /**
144
+ * The tablist — an accessible container for `Tabs.Trigger` elements.
145
+ *
146
+ * Renders a `<div role="tablist">` with `aria-orientation` inherited from
147
+ * the nearest {@link TabsRoot | `Tabs.Root`}. The
148
+ * {@link TabsListProps.label | `label`} prop is **required** and becomes
149
+ * the `aria-label` announced to assistive technology when the tablist
150
+ * receives focus — pick a short, human-readable description of the set of
151
+ * tabs (e.g. `"Account sections"`, not `"Tabs"`).
152
+ *
153
+ * **Styling hooks.** `data-orientation="horizontal" | "vertical"`.
154
+ *
155
+ * @example
156
+ * ```tsx
157
+ * <Tabs.List label="Document sections">
158
+ * <Tabs.Trigger value="title">Title</Tabs.Trigger>
159
+ * <Tabs.Trigger value="body">Body</Tabs.Trigger>
160
+ * </Tabs.List>
161
+ * ```
162
+ */
163
+ export function TabsList({
164
+ children,
165
+ className = "",
166
+ label,
167
+ ariaLabelledBy,
168
+ ...rest
169
+ }: TabsListProps) {
170
+ const { orientation } = useTabsContext();
171
+
172
+ return (
173
+ <div
174
+ role="tablist"
175
+ className={className}
176
+ aria-orientation={orientation}
177
+ {...(label && {
178
+ "aria-label": label,
179
+ })}
180
+ {...(ariaLabelledBy && {
181
+ "aria-labelledby": ariaLabelledBy,
182
+ })}
183
+ data-orientation={orientation}
184
+ {...rest}
185
+ >
186
+ {children}
187
+ </div>
188
+ );
189
+ }
190
+
191
+ TabsList.displayName = "TabsList";
192
+
193
+ /**
194
+ * An individual tab button. Renders `<button role="tab">` with full ARIA
195
+ * linkage, roving tabindex, and keyboard navigation handled automatically.
196
+ *
197
+ * **Value linkage.** Each trigger is identified by a unique
198
+ * {@link TabsTriggerProps.value | `value`} string; the matching
199
+ * `Tabs.Content` must share the same value. The IDs used for
200
+ * `aria-controls` and `aria-labelledby` are derived from the root's
201
+ * `useId()` plus the trigger's `value`, so they're stable and unique even
202
+ * when multiple `Tabs` instances coexist on a page.
203
+ *
204
+ * **Keyboard support** (WAI-ARIA Tabs pattern):
205
+ *
206
+ * | Key | Behaviour |
207
+ * | ---------------------------- | -------------------------------------------------- |
208
+ * | `ArrowRight` / `ArrowLeft` | Move between triggers (horizontal) |
209
+ * | `ArrowDown` / `ArrowUp` | Move between triggers (vertical) |
210
+ * | `Home` / `End` | Jump to first / last trigger |
211
+ * | `Enter` / `Space` | Activate focused trigger (manual mode only) |
212
+ *
213
+ * Movement **wraps** at the ends. In **automatic** activation mode (the default),
214
+ * focus movement immediately activates the panel. In **manual** mode, arrow keys
215
+ * move focus without switching the panel — `Enter` or `Space` confirms the
216
+ * selection. Use manual mode for panels that are expensive to render.
217
+ *
218
+ * **`asChild` prop.** Pass `asChild` to render an arbitrary child element
219
+ * instead of the default `<button>`. All tab ARIA attributes, event handlers,
220
+ * and the roving `tabIndex` are merged onto the child element following the
221
+ * Composition pattern:
222
+ * - Event handlers compose — the child's handler runs first, then the trigger's.
223
+ * - `style` is shallow-merged (child wins on collisions).
224
+ * - `className` strings are concatenated.
225
+ * - Refs from both sides are composed via `composeRefs`.
226
+ *
227
+ * The child **must** be a single React element that accepts a `ref`.
228
+ *
229
+ * **Styling hooks.**
230
+ * - `data-state="active" | "inactive"` on the rendered element.
231
+ * - `data-orientation="horizontal" | "vertical"`.
232
+ *
233
+ * @example Basic usage
234
+ * ```tsx
235
+ * <Tabs.Trigger value="account">Account</Tabs.Trigger>
236
+ * ```
237
+ *
238
+ * @example Icon + label
239
+ * ```tsx
240
+ * <Tabs.Trigger value="billing">
241
+ * <CreditCardIcon aria-hidden />
242
+ * <span>Billing</span>
243
+ * </Tabs.Trigger>
244
+ * ```
245
+ *
246
+ * @example asChild — render a router link with tab semantics
247
+ * ```tsx
248
+ * <Tabs.Trigger asChild value="settings">
249
+ * <Link to="/settings">Settings</Link>
250
+ * </Tabs.Trigger>
251
+ * ```
252
+ */
253
+ export function TabsTrigger<T extends HTMLElement = HTMLButtonElement>({
254
+ ref: externalRef,
255
+ children,
256
+ className = "",
257
+ value,
258
+ onClick,
259
+ disabled = false,
260
+ asChild = false,
261
+ ...rest
262
+ }: TabsTriggerProps<T>) {
263
+ const {
264
+ buttonRef,
265
+ triggerId,
266
+ panelId,
267
+ isActive,
268
+ orientation,
269
+ state,
270
+ tabIndex,
271
+ handleClick,
272
+ handleKeyDown,
273
+ } = useTabsTrigger({ value, onClick, disabled });
274
+
275
+ // Compose our internal ref with any external ref the consumer passes.
276
+ // The external ref is cast to match the internal ref's element type —
277
+ // RefObject<T> is invariant in React's types, but at runtime the callback
278
+ // receives whatever DOM element is actually rendered (button or asChild).
279
+ const composedRef = externalRef
280
+ ? composeRefs(buttonRef, externalRef as Ref<HTMLButtonElement>)
281
+ : buttonRef;
282
+
283
+ const triggerProps = {
284
+ ref: composedRef,
285
+ role: "tab" as const,
286
+ className,
287
+ id: triggerId,
288
+ "aria-controls": panelId,
289
+ "aria-selected": isActive,
290
+ "aria-disabled": disabled,
291
+ "data-disabled": disabled,
292
+ "data-orientation": orientation,
293
+ "data-state": state,
294
+ tabIndex,
295
+ onClick: handleClick,
296
+ onKeyDown: handleKeyDown,
297
+ ...rest,
298
+ };
299
+
300
+ if (asChild) {
301
+ return <Slot {...triggerProps}>{children}</Slot>;
302
+ }
303
+
304
+ return (
305
+ <button type="button" {...triggerProps}>
306
+ {children}
307
+ </button>
308
+ );
309
+ }
310
+
311
+ TabsTrigger.displayName = "TabsTrigger";
312
+
313
+ /**
314
+ * A panel associated with a `Tabs.Trigger` of the same
315
+ * {@link TabsContentProps.value | `value`}.
316
+ *
317
+ * Renders `<div role="tabpanel">` with `aria-labelledby` pointing at the
318
+ * matching trigger, and uses the native `hidden` attribute to toggle
319
+ * visibility. Inactive panels remain **mounted** — this preserves
320
+ * component state (scroll position, form input, animation state) across
321
+ * tab switches. If you need true unmount semantics (e.g. to tear down
322
+ * expensive subscriptions), render the `Tabs.Content` conditionally
323
+ * yourself based on the active value.
324
+ *
325
+ * When {@link TabsRootProps.lazyMount | `lazyMount`} is set on
326
+ * `Tabs.Root`, a panel's children are withheld until the tab is first
327
+ * activated. After that first activation the children remain mounted
328
+ * across subsequent tab switches (lazy mount, not unmount-on-hide).
329
+ * The `<div role="tabpanel">` wrapper always renders so the ARIA
330
+ * relationship between trigger and panel is always present in the DOM.
331
+ *
332
+ * **Styling hooks.**
333
+ * - `data-state="active" | "inactive"`.
334
+ * - `data-orientation="horizontal" | "vertical"`.
335
+ *
336
+ * @example
337
+ * ```tsx
338
+ * <Tabs.Content value="account">
339
+ * <AccountForm />
340
+ * </Tabs.Content>
341
+ * ```
342
+ */
343
+ export function TabsContent({
344
+ children,
345
+ className = "",
346
+ value,
347
+ ...rest
348
+ }: TabsContentProps) {
349
+ const { panelId, triggerId, orientation, isActive, state, tabIndex, shouldRender } =
350
+ useTabsContent({ value });
351
+
352
+ return (
353
+ <div
354
+ role="tabpanel"
355
+ className={className}
356
+ id={panelId}
357
+ aria-labelledby={triggerId}
358
+ data-orientation={orientation}
359
+ data-state={state}
360
+ hidden={!isActive}
361
+ tabIndex={tabIndex}
362
+ {...rest}
363
+ >
364
+ {shouldRender ? children : null}
365
+ </div>
366
+ );
367
+ }
368
+
369
+ TabsContent.displayName = "TabsContent";
370
+
371
+ type TabsCompound = typeof TabsRoot & {
372
+ Root: typeof TabsRoot;
373
+ List: typeof TabsList;
374
+ Trigger: typeof TabsTrigger;
375
+ Content: typeof TabsContent;
376
+ };
377
+
378
+ /**
379
+ * Headless, accessible **Tabs** — a compound component implementing the
380
+ * WAI-ARIA Tabs pattern with zero styles.
381
+ *
382
+ * `Tabs` is both callable (it's an alias of {@link TabsRoot | `Tabs.Root`})
383
+ * and carries its sub-components as static properties. Prefer the
384
+ * namespaced form in application code for readability and grep-ability:
385
+ *
386
+ * - {@link TabsRoot | `Tabs.Root`} — state owner, context provider, imperative API holder.
387
+ * - {@link TabsList | `Tabs.List`} — `role="tablist"` container for triggers.
388
+ * - {@link TabsTrigger | `Tabs.Trigger`} — individual `role="tab"` button.
389
+ * - {@link TabsContent | `Tabs.Content`} — `role="tabpanel"` panel, linked to a trigger by `value`.
390
+ *
391
+ * @example Minimal usage
392
+ * ```tsx
393
+ * import { Tabs } from "@primitiv-ui/react";
394
+ *
395
+ * export function Demo() {
396
+ * return (
397
+ * <Tabs.Root defaultValue="a">
398
+ * <Tabs.List label="Demo tabs">
399
+ * <Tabs.Trigger value="a">First</Tabs.Trigger>
400
+ * <Tabs.Trigger value="b">Second</Tabs.Trigger>
401
+ * </Tabs.List>
402
+ * <Tabs.Content value="a">First panel</Tabs.Content>
403
+ * <Tabs.Content value="b">Second panel</Tabs.Content>
404
+ * </Tabs.Root>
405
+ * );
406
+ * }
407
+ * ```
408
+ *
409
+ * @example Styling with any system
410
+ * Because no styles ship with the component, target the rendered elements
411
+ * or the `data-state` / `data-orientation` hooks with whatever system you
412
+ * use (CSS, CSS-in-JS, Tailwind, design-token stylesheets, etc.):
413
+ *
414
+ * ```css
415
+ * [role="tab"][data-state="active"] { border-bottom: 2px solid currentColor; }
416
+ * [role="tabpanel"][data-state="inactive"] { display: none; }
417
+ * ```
418
+ *
419
+ * @see {@link TabsRoot} for state modes, validation behaviour, and the
420
+ * imperative API.
421
+ * @see {@link TabsList} for the required `label` prop.
422
+ * @see {@link TabsTrigger} for the full keyboard-interaction table.
423
+ * @see {@link TabsContent} for panel lifecycle and unmount semantics.
424
+ */
425
+ const TabsCompound: TabsCompound = Object.assign(TabsRoot, {
426
+ Root: TabsRoot,
427
+ List: TabsList,
428
+ Trigger: TabsTrigger,
429
+ Content: TabsContent,
430
+ });
431
+
432
+ TabsCompound.displayName = "Tabs";
433
+
434
+ export { TabsCompound as Tabs };
@@ -0,0 +1,13 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ import { TabsContextValue } from "./types";
4
+
5
+ export const [TabsContext, useTabsContext] =
6
+ createStrictContext<TabsContextValue>(
7
+ "Component must be rendered as a child of Tabs.Root",
8
+ "TabsContext",
9
+ );
10
+
11
+ const TabsProvider = TabsContext.Provider;
12
+
13
+ export { TabsProvider };
@@ -0,0 +1,114 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { Tabs } from "../Tabs";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ describe("Activation mode tests", () => {
6
+ it("should be in automatic mode by default", async () => {
7
+ // Arrange
8
+ const user = userEvent.setup();
9
+ render(
10
+ <Tabs.Root defaultValue="tab1">
11
+ <Tabs.List label="Test tabs">
12
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
13
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
14
+ </Tabs.List>
15
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
16
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
17
+ </Tabs.Root>,
18
+ );
19
+
20
+ // Act
21
+ await user.tab();
22
+ await user.keyboard("{ArrowRight}");
23
+
24
+ // Assert
25
+ const secondTabPanel = screen.getByRole("tabpanel", {
26
+ name: "Tab 2",
27
+ });
28
+ expect(secondTabPanel).not.toHaveAttribute("hidden");
29
+ });
30
+
31
+ it("should not activate the second tab when focusing its tab in manual mode", async () => {
32
+ // Arrange
33
+ const user = userEvent.setup();
34
+ const { container } = render(
35
+ <Tabs.Root defaultValue="tab1" activationMode="manual">
36
+ <Tabs.List label="Test tabs">
37
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
38
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
39
+ </Tabs.List>
40
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
41
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
42
+ </Tabs.Root>,
43
+ );
44
+
45
+ // Act
46
+ await user.tab();
47
+ await user.keyboard("{ArrowRight}");
48
+
49
+ // Assert
50
+ const secondTabPanel = container.querySelectorAll('[role="tabpanel"]')[1];
51
+ expect(secondTabPanel).not.toBeVisible();
52
+ });
53
+
54
+ it.each([
55
+ {
56
+ defaultValue: "tab1",
57
+ direction: "next",
58
+ directionKeys: ["{ArrowRight}"],
59
+ key: " ",
60
+ expectedSelectedTab: "Tab 2",
61
+ },
62
+ {
63
+ defaultValue: "tab2",
64
+ direction: "previous",
65
+ directionKeys: ["{ArrowLeft}"],
66
+ key: " ",
67
+ expectedSelectedTab: "Tab 1",
68
+ },
69
+ {
70
+ defaultValue: "tab1",
71
+ direction: "next",
72
+ directionKeys: ["{ArrowRight}"],
73
+ key: "{Enter}",
74
+ expectedSelectedTab: "Tab 2",
75
+ },
76
+ {
77
+ defaultValue: "tab2",
78
+ direction: "previous",
79
+ directionKeys: ["{ArrowLeft}"],
80
+ key: "{Enter}",
81
+ expectedSelectedTab: "Tab 1",
82
+ },
83
+ ])(
84
+ "should activate the $direction tab when focusing it in manual mode and pressing the $key key",
85
+ async ({ defaultValue, directionKeys, key, expectedSelectedTab }) => {
86
+ // Arrange
87
+ const user = userEvent.setup();
88
+ render(
89
+ <Tabs.Root defaultValue={defaultValue} activationMode="manual">
90
+ <Tabs.List label="Test tabs">
91
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
92
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
93
+ </Tabs.List>
94
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
95
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
96
+ </Tabs.Root>,
97
+ );
98
+
99
+ // Act
100
+ await user.tab();
101
+ for (const directionKey of directionKeys) {
102
+ await user.keyboard(directionKey);
103
+ }
104
+ await user.keyboard(key);
105
+
106
+ // Assert
107
+ const tab = screen.getByRole("tab", {
108
+ name: expectedSelectedTab,
109
+ });
110
+ expect(tab).toHaveFocus();
111
+ expect(tab).toHaveAttribute("aria-selected", "true");
112
+ },
113
+ );
114
+ });