@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,111 @@
1
+ import {
2
+ useRef,
3
+ useEffect,
4
+ useCallback,
5
+ MouseEvent,
6
+ } from "react";
7
+
8
+ import { useRovingTabindex } from "../../hooks";
9
+
10
+ import { TabsTriggerProps } from "../types";
11
+ import { getTriggerAndPanelIds } from "../utils";
12
+
13
+ import { useTabsContext } from "./useTabsContext";
14
+
15
+ export function useTabsTrigger({
16
+ value,
17
+ onClick,
18
+ disabled,
19
+ }: Pick<TabsTriggerProps, "value" | "onClick" | "disabled">) {
20
+ const {
21
+ orientation,
22
+ dir,
23
+ activationMode,
24
+ activeValue,
25
+ isControlled,
26
+ setActiveValue,
27
+ onValueChange,
28
+ onChange,
29
+ tabsId,
30
+ registerTrigger,
31
+ triggerValues,
32
+ disabledTriggerValues,
33
+ focusTrigger,
34
+ } = useTabsContext();
35
+ const isActive = activeValue === value;
36
+ const { triggerId, panelId } = getTriggerAndPanelIds(tabsId, value);
37
+ const buttonRef = useRef<HTMLButtonElement>(null);
38
+ const state = isActive ? "active" : "inactive";
39
+ // When no tab is active (no defaultValue/value provided), the first registered
40
+ // trigger acts as the roving-tabindex home base so the tablist stays reachable
41
+ // via keyboard Tab navigation.
42
+ const isFirst = triggerValues[0] === value;
43
+ const tabIndex = isActive || (activeValue === undefined && isFirst) ? 0 : -1;
44
+
45
+ useEffect(() => {
46
+ registerTrigger(value, buttonRef.current, disabled);
47
+ return () => registerTrigger(value, null);
48
+ }, [value, disabled, registerTrigger]);
49
+
50
+ const activateTab = useCallback(
51
+ (newValue: string, index: number) => {
52
+ if (activeValue === newValue) return;
53
+ if (isControlled) {
54
+ onValueChange?.(newValue);
55
+ } else {
56
+ setActiveValue(newValue);
57
+ }
58
+ onChange?.({ index, name: newValue });
59
+ },
60
+ [activeValue, isControlled, onValueChange, setActiveValue, onChange],
61
+ );
62
+
63
+ // Note: this composition is deliberately bespoke and does NOT use the
64
+ // shared composeEventHandlers from ../../Slot. It runs the library's
65
+ // activation BEFORE the consumer's onClick, and offers no preventDefault
66
+ // veto path — both differ from the shared util's contract. Switching to
67
+ // composeEventHandlers here would be an observable behaviour change for
68
+ // consumers (handler ordering and preventDefault semantics).
69
+ function handleClick(e: MouseEvent<HTMLButtonElement>) {
70
+ const index = triggerValues.indexOf(value);
71
+ if (!disabled) {
72
+ activateTab(value, index);
73
+ }
74
+ onClick?.(e);
75
+ }
76
+
77
+ // Tabs deliberately leaves disabled values in `navigable` so arrow keys
78
+ // *land* on disabled tabs (focus moves there) without activating them —
79
+ // that's the Tabs-specific keyboard contract, distinct from RadioGroup
80
+ // and Accordion which both skip disabled siblings entirely.
81
+ const { handleKeyDown } = useRovingTabindex<string>({
82
+ orientation,
83
+ dir,
84
+ navigable: triggerValues,
85
+ currentKey: value,
86
+ includeHomeEnd: true,
87
+ includeActivate: true,
88
+ onNavigate: (targetValue, action) => {
89
+ const isDisabled = disabledTriggerValues.has(targetValue);
90
+ const shouldActivate =
91
+ !isDisabled &&
92
+ (activationMode === "automatic" ||
93
+ (activationMode === "manual" && action === "activate"));
94
+ if (shouldActivate) {
95
+ activateTab(targetValue, triggerValues.indexOf(targetValue));
96
+ }
97
+ focusTrigger(targetValue);
98
+ },
99
+ });
100
+ return {
101
+ buttonRef,
102
+ triggerId,
103
+ panelId,
104
+ isActive,
105
+ orientation,
106
+ state,
107
+ tabIndex,
108
+ handleClick,
109
+ handleKeyDown,
110
+ };
111
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Tabs';
2
+ export * from './TabsContext';
3
+ export * from './types';
@@ -0,0 +1,99 @@
1
+ import { ComponentProps, Ref } from "react";
2
+
3
+ export type TabsOrientation = "horizontal" | "vertical";
4
+
5
+ export type TabsReadingDirection = "ltr" | "rtl";
6
+
7
+ export type TabsActivationMode = "automatic" | "manual";
8
+
9
+ /**
10
+ * Payload delivered to the {@link BaseTabsRootProps.onChange | `onChange`} callback
11
+ * on every user-driven tab activation.
12
+ *
13
+ * - `index` — zero-based position of the activated trigger in registration order.
14
+ * - `name` — the programmatic **value** string passed to `Tabs.Trigger` (e.g.
15
+ * `"settings"`), **not** the human-readable label text rendered inside the
16
+ * trigger. Use the `children` of the trigger for the visible label.
17
+ */
18
+ export type TabMetadata = { index: number; name: string };
19
+
20
+ export type BaseTabsRootProps = {
21
+ onChange?: ({ index, name }: TabMetadata) => void;
22
+ };
23
+
24
+ export type UncontrolledTabsRootProps = {
25
+ defaultValue?: string;
26
+ value?: never;
27
+ onValueChange?: never;
28
+ };
29
+
30
+ type ControlledTabsRootProps = {
31
+ value: string;
32
+ onValueChange: (value: string) => void;
33
+ defaultValue?: never;
34
+ };
35
+
36
+ export type TabsRootProps = Omit<ComponentProps<"div">, "onChange"> & {
37
+ orientation?: TabsOrientation;
38
+ dir?: TabsReadingDirection;
39
+ activationMode?: TabsActivationMode;
40
+ /** When `true`, a panel's children are not rendered until that tab is first
41
+ * activated. Once mounted they remain in the DOM across subsequent tab
42
+ * switches (lazy mount, not unmount-on-hide). Useful for panels that own
43
+ * expensive initialisation — e.g. a scroll-snap carousel whose initial
44
+ * scroll position must be set while the panel is visible. */
45
+ lazyMount?: boolean;
46
+ } & BaseTabsRootProps &
47
+ (UncontrolledTabsRootProps | ControlledTabsRootProps);
48
+
49
+ export type TabsContextValue = {
50
+ orientation: TabsOrientation;
51
+ dir: TabsReadingDirection;
52
+ activationMode: TabsActivationMode;
53
+ activeValue: string | undefined;
54
+ isControlled: boolean;
55
+ setActiveValue: (next: string) => void;
56
+ onValueChange?: (value: string) => void;
57
+ onChange?: ({ index, name }: TabMetadata) => void;
58
+ tabsId: string;
59
+ lazyMount: boolean;
60
+ registerTrigger: (
61
+ value: string,
62
+ element: HTMLButtonElement | null,
63
+ disabled?: boolean,
64
+ ) => void;
65
+ triggerValues: string[];
66
+ disabledTriggerValues: Set<string>;
67
+ focusTrigger: (value: string) => void;
68
+ };
69
+
70
+ export type TabsListProps = Omit<
71
+ ComponentProps<"div">,
72
+ "label" | "aria-labelledby"
73
+ > &
74
+ (
75
+ | { label: string; ariaLabelledBy?: never }
76
+ | { label?: never; ariaLabelledBy: string }
77
+ );
78
+ export type TabsTriggerProps<T extends HTMLElement = HTMLButtonElement> = Omit<
79
+ ComponentProps<"button">,
80
+ "ref"
81
+ > & {
82
+ disabled?: boolean;
83
+ value: string;
84
+ /** Render the child element instead of the default `<button>`. All tab
85
+ * ARIA attributes and event handlers are merged onto the child. The child
86
+ * must accept a `ref`. Useful for routing links that need tab semantics. */
87
+ asChild?: boolean;
88
+ /** Ref to the rendered element. Defaults to `HTMLButtonElement`; when using
89
+ * `asChild`, specify the child's element type (e.g. `HTMLAnchorElement`). */
90
+ ref?: Ref<T>;
91
+ };
92
+
93
+ export type TabsContentProps = ComponentProps<"div"> & {
94
+ value: string;
95
+ };
96
+
97
+ export type TabsImperativeApi = {
98
+ setActiveTab: (value: string) => void;
99
+ };
@@ -0,0 +1,8 @@
1
+ import { deriveId } from "../utils";
2
+
3
+ export function getTriggerAndPanelIds(tabsId: string, value: string) {
4
+ return {
5
+ triggerId: deriveId(tabsId, "trigger", value),
6
+ panelId: deriveId(tabsId, "panel", value),
7
+ };
8
+ }
@@ -0,0 +1,98 @@
1
+ # Textarea
2
+
3
+ A headless, accessible multi-line text input. Zero styles ship — apply
4
+ whatever design system you use.
5
+
6
+ ```tsx
7
+ import { Textarea } from "@primitiv-ui/react";
8
+
9
+ <label htmlFor="bio">Bio</label>
10
+ <Textarea id="bio" rows={4} placeholder="Tell us about yourself" />
11
+ ```
12
+
13
+ ## Props
14
+
15
+ | Prop | Type | Default | Notes |
16
+ | ---------- | ---------------------------- | ------- | ---------------------------------------------------------------- |
17
+ | `disabled` | `boolean` | — | Native `disabled` + `data-disabled=""` styling hook |
18
+ | `asChild` | `boolean` | `false` | Delegate rendering to the child element via Slot |
19
+ | `ref` | `Ref<HTMLTextAreaElement>` | — | Forwarded to the underlying textarea element |
20
+ | `...rest` | `ComponentProps<"textarea">` | — | All other `<textarea>` props (`value`, `rows`, `aria-*`, etc.) |
21
+
22
+ ## Labelling
23
+
24
+ A `<textarea>` has no implicit accessible name. Give it one of:
25
+
26
+ - a `<label>` whose `htmlFor` matches the textarea's `id`,
27
+ - an `aria-label`, or
28
+ - an `aria-labelledby` pointing at visible label text.
29
+
30
+ ```tsx
31
+ <label htmlFor="notes">Notes</label>
32
+ <Textarea id="notes" />
33
+ ```
34
+
35
+ ## Disabled
36
+
37
+ Passing `disabled` forwards the native `disabled` attribute (removing the
38
+ field from the tab order and blocking input) **and** sets
39
+ `data-disabled=""` so CSS can target `[data-disabled]` without relying on
40
+ the `:disabled` pseudo-class:
41
+
42
+ ```tsx
43
+ <Textarea aria-label="Bio" disabled />
44
+ ```
45
+
46
+ ```css
47
+ textarea[data-disabled] {
48
+ opacity: 0.5;
49
+ cursor: not-allowed;
50
+ }
51
+ ```
52
+
53
+ ## `asChild` composition
54
+
55
+ Pass `asChild` to render any consumer-supplied element instead of
56
+ `<textarea>`. All props (`aria-*`, `data-*`, event handlers, `ref`) are
57
+ merged onto the child via the `Slot` utility — event handlers compose
58
+ (child runs first), `style` is shallow-merged, `className` is concatenated.
59
+
60
+ This is the escape hatch for wrapping a third-party autosizing textarea
61
+ while keeping the same prop contract:
62
+
63
+ ```tsx
64
+ <Textarea asChild aria-label="Bio">
65
+ <AutosizeTextarea />
66
+ </Textarea>
67
+ ```
68
+
69
+ ## Ref forwarding
70
+
71
+ ```tsx
72
+ const ref = useRef<HTMLTextAreaElement>(null);
73
+ <Textarea ref={ref} aria-label="Bio" />
74
+ ```
75
+
76
+ ## Field integration
77
+
78
+ When rendered inside a [`<Field.Root>`](../Field/README.md), `Textarea`
79
+ reads `FieldContext` and inherits:
80
+
81
+ - `id` (from `field.id`)
82
+ - `aria-describedby` (composed: consumer ids first, then the field's
83
+ `descriptionId`, then `errorId` when invalid)
84
+ - `aria-invalid` (`"true"` when the field is invalid)
85
+ - `disabled`
86
+ - `required`
87
+
88
+ Consumer-supplied props always win — pass an explicit value on the
89
+ `Textarea` to override any field-derived one. Outside a `<Field.Root>`,
90
+ behaviour is unchanged.
91
+
92
+ ```tsx
93
+ <Field.Root invalid={!!errors.bio}>
94
+ <Field.Label>Bio</Field.Label>
95
+ <Textarea rows={4} required {...register("bio")} />
96
+ <Field.ErrorText>{errors.bio?.message}</Field.ErrorText>
97
+ </Field.Root>
98
+ ```
@@ -0,0 +1,93 @@
1
+ import { useFieldProps } from "../Field/hooks";
2
+ import { Slot } from "../Slot";
3
+ import { TextareaProps } from "./types";
4
+
5
+ /**
6
+ * A headless, accessible multi-line text input.
7
+ *
8
+ * Renders a native `<textarea>` and passes every standard textarea
9
+ * attribute straight through to the DOM — `value` / `defaultValue`,
10
+ * `rows`, `cols`, `placeholder`, `readOnly`, `required`, `name`,
11
+ * `maxLength`, and so on. No styles ship with the component.
12
+ *
13
+ * **Labelling.** A `<textarea>` has no implicit accessible name. Pair it
14
+ * with a `<label>` (`htmlFor` → the textarea's `id`), or pass `aria-label`
15
+ * / `aria-labelledby` for the control to be announced correctly.
16
+ *
17
+ * **Field integration.** When rendered inside a `<Field.Root>`, Textarea
18
+ * opts into `FieldContext` and inherits `id`, `aria-describedby`,
19
+ * `aria-invalid`, `disabled`, and `required` from the field. Any prop
20
+ * the consumer passes wins; `aria-describedby` is composed (consumer
21
+ * ids first, then field-supplied description / error ids). Outside a
22
+ * `<Field.Root>`, behaviour is unchanged.
23
+ *
24
+ * **Ref forwarding.** Pass a `ref` prop to access the underlying
25
+ * `HTMLTextAreaElement` directly:
26
+ *
27
+ * ```tsx
28
+ * const ref = useRef<HTMLTextAreaElement>(null);
29
+ * <Textarea ref={ref} aria-label="Bio" />
30
+ * ```
31
+ *
32
+ * **Disabled.** Sets native `disabled` (removing the field from the tab
33
+ * order and blocking input) plus `data-disabled=""` so CSS can target
34
+ * `[data-disabled]` without relying on the `:disabled` pseudo-class:
35
+ *
36
+ * ```css
37
+ * textarea[data-disabled] { opacity: 0.5; cursor: not-allowed; }
38
+ * ```
39
+ *
40
+ * **`asChild` composition.** Renders the consumer's element instead of
41
+ * `<textarea>`, merging all props (aria-*, data-*, event handlers, ref)
42
+ * via the {@link Slot} utility. Use it to wrap an autosizing textarea
43
+ * implementation while keeping this component's prop contract.
44
+ *
45
+ * @example Basic usage
46
+ * ```tsx
47
+ * <label htmlFor="bio">Bio</label>
48
+ * <Textarea id="bio" rows={4} placeholder="Tell us about yourself" />
49
+ * ```
50
+ *
51
+ * @example Inside a Field — id and aria wired automatically
52
+ * ```tsx
53
+ * <Field.Root invalid={!!errors.bio}>
54
+ * <Field.Label>Bio</Field.Label>
55
+ * <Textarea rows={4} {...register("bio")} />
56
+ * <Field.ErrorText>{errors.bio?.message}</Field.ErrorText>
57
+ * </Field.Root>
58
+ * ```
59
+ *
60
+ * @example Disabled
61
+ * ```tsx
62
+ * <Textarea aria-label="Bio" disabled />
63
+ * ```
64
+ *
65
+ * @example asChild — wrap an autosizing textarea
66
+ * ```tsx
67
+ * <Textarea asChild aria-label="Bio">
68
+ * <AutosizeTextarea />
69
+ * </Textarea>
70
+ * ```
71
+ */
72
+ export function Textarea({
73
+ asChild = false,
74
+ children,
75
+ ref,
76
+ ...consumer
77
+ }: TextareaProps) {
78
+ const merged = useFieldProps(consumer);
79
+
80
+ const rootProps = {
81
+ ...merged,
82
+ ref,
83
+ "data-disabled": merged.disabled ? "" : undefined,
84
+ };
85
+
86
+ if (asChild) {
87
+ return <Slot {...rootProps}>{children}</Slot>;
88
+ }
89
+
90
+ return <textarea {...rootProps} />;
91
+ }
92
+
93
+ Textarea.displayName = "Textarea";
@@ -0,0 +1,85 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { Textarea } from "../Textarea";
6
+
7
+ describe("Textarea asChild composition", () => {
8
+ it("renders the consumer's element instead of its own <textarea>", () => {
9
+ // Arrange & Act
10
+ render(
11
+ <Textarea asChild aria-label="Bio">
12
+ <textarea data-testid="autosize" />
13
+ </Textarea>,
14
+ );
15
+
16
+ // Assert
17
+ expect(screen.getByRole("textbox", { name: "Bio" })).toHaveAttribute(
18
+ "data-testid",
19
+ "autosize",
20
+ );
21
+ });
22
+
23
+ it("renders exactly one textarea — the child, not an extra wrapper", () => {
24
+ // Arrange & Act
25
+ render(
26
+ <Textarea asChild aria-label="Bio">
27
+ <textarea />
28
+ </Textarea>,
29
+ );
30
+
31
+ // Assert
32
+ expect(screen.getAllByRole("textbox")).toHaveLength(1);
33
+ });
34
+
35
+ it("merges aria-* props onto the child element", () => {
36
+ // Arrange & Act
37
+ render(
38
+ <Textarea asChild aria-label="Bio" aria-invalid>
39
+ <textarea />
40
+ </Textarea>,
41
+ );
42
+
43
+ // Assert
44
+ expect(screen.getByRole("textbox", { name: "Bio" })).toHaveAttribute(
45
+ "aria-invalid",
46
+ "true",
47
+ );
48
+ });
49
+
50
+ it("composes onChange — child handler fires first, then Textarea's", async () => {
51
+ // Arrange
52
+ const user = userEvent.setup();
53
+ const order: string[] = [];
54
+ render(
55
+ <Textarea
56
+ asChild
57
+ aria-label="Bio"
58
+ onChange={() => order.push("textarea")}
59
+ >
60
+ <textarea onChange={() => order.push("child")} />
61
+ </Textarea>,
62
+ );
63
+
64
+ // Act
65
+ await user.type(screen.getByRole("textbox", { name: "Bio" }), "x");
66
+
67
+ // Assert
68
+ expect(order).toEqual(["child", "textarea"]);
69
+ });
70
+
71
+ it("forwards a ref to the child element", () => {
72
+ // Arrange
73
+ const ref = createRef<HTMLTextAreaElement>();
74
+
75
+ // Act
76
+ render(
77
+ <Textarea asChild aria-label="Bio" ref={ref}>
78
+ <textarea />
79
+ </Textarea>,
80
+ );
81
+
82
+ // Assert
83
+ expect(ref.current).toBe(screen.getByRole("textbox", { name: "Bio" }));
84
+ });
85
+ });
@@ -0,0 +1,107 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { Textarea } from "../Textarea";
6
+
7
+ describe("Textarea basic rendering", () => {
8
+ it("renders a <textarea> element", () => {
9
+ // Arrange & Act
10
+ render(<Textarea aria-label="Notes" />);
11
+
12
+ // Assert
13
+ expect(screen.getByRole("textbox", { name: "Notes" }).tagName).toBe(
14
+ "TEXTAREA",
15
+ );
16
+ });
17
+
18
+ it("forwards a ref to the underlying textarea element (object ref)", () => {
19
+ // Arrange
20
+ const ref = createRef<HTMLTextAreaElement>();
21
+
22
+ // Act
23
+ render(<Textarea aria-label="Notes" ref={ref} />);
24
+
25
+ // Assert
26
+ expect(ref.current).toBe(screen.getByRole("textbox", { name: "Notes" }));
27
+ });
28
+
29
+ it("forwards a ref to the underlying textarea element (function ref)", () => {
30
+ // Arrange
31
+ const received: (HTMLTextAreaElement | null)[] = [];
32
+ const functionRef = (node: HTMLTextAreaElement | null) => {
33
+ received.push(node);
34
+ };
35
+
36
+ // Act
37
+ render(<Textarea aria-label="Notes" ref={functionRef} />);
38
+
39
+ // Assert
40
+ expect(received).toContain(
41
+ screen.getByRole("textbox", { name: "Notes" }),
42
+ );
43
+ });
44
+
45
+ it("reflects a defaultValue", () => {
46
+ // Arrange & Act
47
+ render(<Textarea aria-label="Notes" defaultValue="hello" />);
48
+
49
+ // Assert
50
+ expect(screen.getByRole("textbox", { name: "Notes" })).toHaveValue(
51
+ "hello",
52
+ );
53
+ });
54
+
55
+ it("passes through textarea attributes (rows, placeholder)", () => {
56
+ // Arrange & Act
57
+ render(<Textarea aria-label="Notes" rows={6} placeholder="Type here" />);
58
+ const textarea = screen.getByRole("textbox", { name: "Notes" });
59
+
60
+ // Assert
61
+ expect(textarea).toHaveAttribute("rows", "6");
62
+ expect(textarea).toHaveAttribute("placeholder", "Type here");
63
+ });
64
+
65
+ it("passes through aria-* attributes", () => {
66
+ // Arrange & Act
67
+ render(<Textarea aria-label="Notes" aria-invalid />);
68
+
69
+ // Assert
70
+ expect(screen.getByRole("textbox", { name: "Notes" })).toHaveAttribute(
71
+ "aria-invalid",
72
+ "true",
73
+ );
74
+ });
75
+
76
+ it("passes through data-* attributes", () => {
77
+ // Arrange & Act
78
+ render(<Textarea aria-label="Notes" data-testid="bio" />);
79
+
80
+ // Assert
81
+ expect(screen.getByTestId("bio")).toBeInTheDocument();
82
+ });
83
+
84
+ it("passes through className", () => {
85
+ // Arrange & Act
86
+ render(<Textarea aria-label="Notes" className="field" />);
87
+
88
+ // Assert
89
+ expect(screen.getByRole("textbox", { name: "Notes" })).toHaveAttribute(
90
+ "class",
91
+ "field",
92
+ );
93
+ });
94
+
95
+ it("passes through event handlers (onChange)", async () => {
96
+ // Arrange
97
+ const user = userEvent.setup();
98
+ const onChange = vi.fn();
99
+ render(<Textarea aria-label="Notes" onChange={onChange} />);
100
+
101
+ // Act
102
+ await user.type(screen.getByRole("textbox", { name: "Notes" }), "hi");
103
+
104
+ // Assert
105
+ expect(onChange).toHaveBeenCalledTimes(2);
106
+ });
107
+ });
@@ -0,0 +1,49 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Textarea } from "../Textarea";
4
+
5
+ describe("Textarea disabled state", () => {
6
+ it("sets the native disabled attribute", () => {
7
+ // Arrange & Act
8
+ render(<Textarea aria-label="Notes" disabled />);
9
+
10
+ // Assert
11
+ expect(screen.getByRole("textbox", { name: "Notes" })).toBeDisabled();
12
+ });
13
+
14
+ it('sets data-disabled="" as a CSS styling hook', () => {
15
+ // Arrange & Act
16
+ render(<Textarea aria-label="Notes" disabled />);
17
+
18
+ // Assert
19
+ expect(screen.getByRole("textbox", { name: "Notes" })).toHaveAttribute(
20
+ "data-disabled",
21
+ "",
22
+ );
23
+ });
24
+
25
+ it("does not set data-disabled when not disabled", () => {
26
+ // Arrange & Act
27
+ render(<Textarea aria-label="Notes" />);
28
+
29
+ // Assert
30
+ expect(
31
+ screen.getByRole("textbox", { name: "Notes" }),
32
+ ).not.toHaveAttribute("data-disabled");
33
+ });
34
+
35
+ it("sets data-disabled on the child element when asChild and disabled", () => {
36
+ // Arrange & Act
37
+ render(
38
+ <Textarea asChild aria-label="Notes" disabled>
39
+ <textarea />
40
+ </Textarea>,
41
+ );
42
+
43
+ // Assert
44
+ expect(screen.getByRole("textbox", { name: "Notes" })).toHaveAttribute(
45
+ "data-disabled",
46
+ "",
47
+ );
48
+ });
49
+ });