@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,137 @@
1
+ # ToggleGroup
2
+
3
+ A headless, accessible compound component that manages a group of pressable
4
+ toggle buttons with roving-tabindex keyboard navigation.
5
+
6
+ ```tsx
7
+ import { ToggleGroup } from "@primitiv-ui/react";
8
+
9
+ <ToggleGroup.Root type="single" defaultValue="left" aria-label="Alignment">
10
+ <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
11
+ <ToggleGroup.Item value="center">Center</ToggleGroup.Item>
12
+ <ToggleGroup.Item value="right">Right</ToggleGroup.Item>
13
+ </ToggleGroup.Root>;
14
+ ```
15
+
16
+ ## Sub-components
17
+
18
+ | Export | Element | Notes |
19
+ | ------------------- | ---------- | ------------------------------------------------------------ |
20
+ | `ToggleGroup.Root` | `<div>` | `role="group"`, context provider, `asChild` |
21
+ | `ToggleGroup.Item` | `<button>` | `aria-pressed`, `data-state`, roving tabindex, `asChild` |
22
+
23
+ ## Type modes
24
+
25
+ ### `type="single"`
26
+
27
+ At most one item is pressed at a time. Pressing the active item again
28
+ **deselects** it — unlike a radio group, the value can return to `undefined`.
29
+
30
+ ```tsx
31
+ // Uncontrolled
32
+ <ToggleGroup.Root type="single" defaultValue="center" aria-label="Alignment">
33
+
34
+ </ToggleGroup.Root>
35
+
36
+ // Controlled — onValueChange receives string | undefined
37
+ const [align, setAlign] = useState<string | undefined>("center");
38
+ <ToggleGroup.Root type="single" value={align} onValueChange={setAlign} aria-label="Alignment">
39
+
40
+ </ToggleGroup.Root>
41
+ ```
42
+
43
+ ### `type="multiple"`
44
+
45
+ Any number of items can be pressed simultaneously. Each item toggles
46
+ independently.
47
+
48
+ ```tsx
49
+ // Uncontrolled
50
+ <ToggleGroup.Root type="multiple" defaultValue={["bold"]} aria-label="Formatting">
51
+
52
+ </ToggleGroup.Root>
53
+
54
+ // Controlled — onValueChange receives string[]
55
+ const [formats, setFormats] = useState<string[]>([]);
56
+ <ToggleGroup.Root type="multiple" value={formats} onValueChange={setFormats} aria-label="Formatting">
57
+
58
+ </ToggleGroup.Root>
59
+ ```
60
+
61
+ ## State modes
62
+
63
+ Both type modes support **uncontrolled** (`defaultValue`) and **controlled**
64
+ (`value` + `onValueChange`). The shapes are statically discriminated at the
65
+ type level; TypeScript rejects mixing them.
66
+
67
+ ## Pressed state
68
+
69
+ | `pressed` | `aria-pressed` | `data-state` |
70
+ | --------- | -------------- | ------------ |
71
+ | `true` | `"true"` | `"on"` |
72
+ | `false` | `"false"` | `"off"` |
73
+
74
+ ## Keyboard interaction
75
+
76
+ | Key | Behaviour |
77
+ | --------------- | -------------------------------------------------- |
78
+ | `ArrowRight` / `ArrowDown` | Focus next item (wraps) |
79
+ | `ArrowLeft` / `ArrowUp` | Focus previous item (wraps) |
80
+ | `Home` | Focus first item |
81
+ | `End` | Focus last item |
82
+ | `Space` | Toggle the focused item |
83
+ | `Enter` | Toggle the focused item |
84
+
85
+ Navigation is **decoupled from selection**: arrow keys move focus without
86
+ toggling. Only `Space` / `Enter` (or a click) toggle the focused item.
87
+ Only one item is in the document tab sequence at a time; `Tab` escapes
88
+ the group in a single keystroke.
89
+
90
+ ## Reading direction
91
+
92
+ Pass `dir` (`"ltr"` / `"rtl"`) to set the arrow-key direction for horizontal
93
+ groups. When `dir` is omitted, it is inherited from the nearest
94
+ [`DirectionProvider`](../DirectionProvider/README.md), falling back to `"ltr"`
95
+ when there is no provider. An explicit `dir` prop always wins over the
96
+ inherited value.
97
+
98
+ ## Disabled
99
+
100
+ Passing `disabled` on an `Item` forwards the native `disabled` attribute
101
+ (removing it from the tab order and suppressing clicks), sets
102
+ `data-disabled=""` for CSS targeting, and excludes it from arrow-key
103
+ navigation.
104
+
105
+ ```tsx
106
+ <ToggleGroup.Item value="strikethrough" disabled>
107
+ Strikethrough
108
+ </ToggleGroup.Item>
109
+ ```
110
+
111
+ ## `asChild` composition
112
+
113
+ Both `Root` and `Item` accept `asChild`. When set, the component delegates
114
+ rendering to its single child element and merges its own ARIA attributes,
115
+ data-state, composed event handlers, and ref onto the child.
116
+
117
+ ## Styling hooks
118
+
119
+ ```css
120
+ /* pressed item */
121
+ [data-state="on"] {
122
+ background: oklch(65% 0.18 250);
123
+ color: white;
124
+ }
125
+
126
+ /* group orientation */
127
+ [role="group"][data-orientation="horizontal"] {
128
+ display: flex;
129
+ flex-direction: row;
130
+ }
131
+
132
+ /* disabled */
133
+ [data-disabled] {
134
+ opacity: 0.4;
135
+ cursor: not-allowed;
136
+ }
137
+ ```
@@ -0,0 +1,298 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+
3
+ import { useDirection } from "../DirectionProvider";
4
+ import { useRovingTabindex } from "../hooks";
5
+ import { Slot, composeEventHandlers, composeRefs } from "../Slot";
6
+
7
+ import { ToggleGroupContext } from "./ToggleGroupContext";
8
+ import { useToggleGroupRoot, useToggleGroupContext } from "./hooks";
9
+ import { ToggleGroupItemProps, ToggleGroupRootProps } from "./types";
10
+
11
+ /**
12
+ * The root of a ToggleGroup — a `<div role="group">` that owns the
13
+ * pressed values and provides {@link ToggleGroupContext} to descendant
14
+ * {@link ToggleGroupItem | `ToggleGroup.Item`}s.
15
+ *
16
+ * Two type modes, statically discriminated at the type level:
17
+ *
18
+ * - **`type="single"`** — at most one item can be pressed at a time.
19
+ * Pressing the active item again deselects it (value returns to
20
+ * `undefined`). Controlled via `value: string | undefined` +
21
+ * `onValueChange: (value: string | undefined) => void`.
22
+ * - **`type="multiple"`** — any number of items can be pressed
23
+ * simultaneously. Controlled via `value: string[]` +
24
+ * `onValueChange: (value: string[]) => void`.
25
+ *
26
+ * Each type supports **uncontrolled** (pass `defaultValue`, or omit for
27
+ * nothing pressed on mount) and **controlled** (pass `value` +
28
+ * `onValueChange`) modes. The two shapes are statically discriminated at
29
+ * the type level.
30
+ *
31
+ * **Keyboard.** Arrow keys move focus between items (roving tabindex)
32
+ * independently of selection. `Space` / `Enter` toggle the focused item.
33
+ *
34
+ * **ARIA.** `role="group"` on the root. Each item carries `aria-pressed`.
35
+ *
36
+ * **Styling hooks.** `data-orientation="horizontal" | "vertical"` on the
37
+ * root. Each item: `data-state="on" | "off"`, `data-disabled=""`.
38
+ *
39
+ * **Reading direction.** `dir` (`"ltr"` / `"rtl"`) sets the arrow-key
40
+ * direction for horizontal groups. When omitted, it is inherited from the
41
+ * nearest {@link DirectionProvider}, falling back to `"ltr"` when there is
42
+ * no provider.
43
+ *
44
+ * **`asChild` prop.** Pass `asChild` to render any consumer-supplied
45
+ * element in place of the native `<div>`, with the group's `role`,
46
+ * `data-orientation`, and ref merged in.
47
+ *
48
+ * @example Single mode — text alignment
49
+ * ```tsx
50
+ * <ToggleGroup.Root type="single" defaultValue="left" aria-label="Alignment">
51
+ * <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
52
+ * <ToggleGroup.Item value="center">Center</ToggleGroup.Item>
53
+ * <ToggleGroup.Item value="right">Right</ToggleGroup.Item>
54
+ * </ToggleGroup.Root>
55
+ * ```
56
+ *
57
+ * @example Multiple mode — text formatting
58
+ * ```tsx
59
+ * <ToggleGroup.Root type="multiple" aria-label="Formatting">
60
+ * <ToggleGroup.Item value="bold">Bold</ToggleGroup.Item>
61
+ * <ToggleGroup.Item value="italic">Italic</ToggleGroup.Item>
62
+ * </ToggleGroup.Root>
63
+ * ```
64
+ */
65
+ function ToggleGroupRoot({
66
+ type,
67
+ defaultValue,
68
+ value: controlledValue,
69
+ onValueChange,
70
+ orientation = "horizontal",
71
+ dir,
72
+ asChild = false,
73
+ children,
74
+ ref,
75
+ ...rest
76
+ }: ToggleGroupRootProps) {
77
+ const resolvedDir = dir ?? useDirection();
78
+ const {
79
+ value,
80
+ toggle,
81
+ registerItem,
82
+ itemValues,
83
+ disabledValues,
84
+ focusItem,
85
+ focusedValue,
86
+ setFocusedValue,
87
+ } = useToggleGroupRoot({
88
+ type,
89
+ defaultValue,
90
+ value: controlledValue,
91
+ onValueChange,
92
+ orientation,
93
+ dir: resolvedDir,
94
+ });
95
+
96
+ const contextValue = useMemo(
97
+ () => ({
98
+ value,
99
+ toggle,
100
+ registerItem,
101
+ itemValues,
102
+ disabledValues,
103
+ focusItem,
104
+ focusedValue,
105
+ setFocusedValue,
106
+ orientation,
107
+ dir: resolvedDir,
108
+ }),
109
+ [
110
+ value,
111
+ toggle,
112
+ registerItem,
113
+ itemValues,
114
+ disabledValues,
115
+ focusItem,
116
+ focusedValue,
117
+ setFocusedValue,
118
+ orientation,
119
+ resolvedDir,
120
+ ],
121
+ );
122
+
123
+ const rootProps = {
124
+ ...rest,
125
+ ref,
126
+ role: "group" as const,
127
+ "data-orientation": orientation,
128
+ };
129
+
130
+ return (
131
+ <ToggleGroupContext.Provider value={contextValue}>
132
+ {asChild ? (
133
+ <Slot {...rootProps}>{children}</Slot>
134
+ ) : (
135
+ <div {...rootProps}>{children}</div>
136
+ )}
137
+ </ToggleGroupContext.Provider>
138
+ );
139
+ }
140
+
141
+ ToggleGroupRoot.displayName = "ToggleGroupRoot";
142
+
143
+ /**
144
+ * An individual toggle button inside a ToggleGroup — a native
145
+ * `<button type="button" aria-pressed>` that participates in the
146
+ * group's roving tabindex and reports its pressed state.
147
+ *
148
+ * **Selection.** Clicking an Item (or pressing `Space` / `Enter` on
149
+ * the focused Item) toggles it. In `type="single"` mode, pressing an
150
+ * active item deselects it; in `type="multiple"` mode, items toggle
151
+ * independently.
152
+ *
153
+ * **Roving tabindex.** Arrow keys move focus between enabled items
154
+ * without changing selection. Only one Item is in the document tab
155
+ * sequence at a time; `Tab` escapes the group in a single keystroke.
156
+ *
157
+ * **Disabled.** Passing `disabled` forwards the native attribute and
158
+ * excludes the Item from arrow-key navigation. Use `data-disabled` for
159
+ * CSS targeting without `:disabled` specificity complications.
160
+ *
161
+ * **Styling hooks.** `data-state="on" | "off"` mirrors the pressed state.
162
+ * `data-disabled=""` when disabled.
163
+ *
164
+ * **`asChild` prop.** Pass `asChild` to render any consumer element in
165
+ * place of the native `<button>`, with the Item's `aria-pressed`,
166
+ * `data-state`, `tabIndex`, and event handlers merged onto it.
167
+ *
168
+ * @throws if rendered outside a `ToggleGroup.Root`.
169
+ */
170
+ function ToggleGroupItem({
171
+ value,
172
+ disabled,
173
+ asChild = false,
174
+ onClick,
175
+ onKeyDown,
176
+ onFocus,
177
+ children,
178
+ ref,
179
+ ...rest
180
+ }: ToggleGroupItemProps) {
181
+ const {
182
+ value: groupValue,
183
+ toggle,
184
+ registerItem,
185
+ itemValues,
186
+ disabledValues,
187
+ focusItem,
188
+ focusedValue,
189
+ setFocusedValue,
190
+ orientation,
191
+ dir,
192
+ } = useToggleGroupContext();
193
+
194
+ const isPressed = groupValue.includes(value);
195
+
196
+ const enabledValues = useMemo(
197
+ () => itemValues.filter((v) => !disabledValues.has(v)),
198
+ [itemValues, disabledValues],
199
+ );
200
+
201
+ // Tabstop: the focused item if known; otherwise fall back to the first
202
+ // enabled item so the group is reachable via Tab before any interaction.
203
+ const isTabstop =
204
+ focusedValue !== undefined
205
+ ? focusedValue === value
206
+ : enabledValues[0] === value;
207
+
208
+ const localRef = useRef<HTMLButtonElement | null>(null);
209
+ const setRef = useMemo(() => composeRefs(localRef, ref), [ref]);
210
+
211
+ useEffect(() => {
212
+ registerItem(value, localRef.current, disabled);
213
+ return () => registerItem(value, null);
214
+ }, [value, disabled, registerItem]);
215
+
216
+ const { handleKeyDown } = useRovingTabindex<string>({
217
+ orientation,
218
+ dir,
219
+ navigable: enabledValues,
220
+ currentKey: value,
221
+ includeHomeEnd: true,
222
+ includeActivate: true,
223
+ onNavigate: (target, action) => {
224
+ if (action === "activate") {
225
+ toggle(target);
226
+ }
227
+ focusItem(target);
228
+ },
229
+ });
230
+
231
+ const itemProps = {
232
+ ...rest,
233
+ ref: setRef,
234
+ "aria-pressed": isPressed,
235
+ "data-state": isPressed ? ("on" as const) : ("off" as const),
236
+ "data-disabled": disabled ? "" : undefined,
237
+ tabIndex: isTabstop ? 0 : -1,
238
+ disabled,
239
+ onClick: composeEventHandlers(onClick, () => toggle(value)),
240
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
241
+ onFocus: composeEventHandlers(onFocus, () => setFocusedValue(value)),
242
+ };
243
+
244
+ return asChild ? (
245
+ <Slot {...itemProps}>{children}</Slot>
246
+ ) : (
247
+ <button type="button" {...itemProps}>
248
+ {children}
249
+ </button>
250
+ );
251
+ }
252
+
253
+ ToggleGroupItem.displayName = "ToggleGroupItem";
254
+
255
+ type TToggleGroupCompound = typeof ToggleGroupRoot & {
256
+ Root: typeof ToggleGroupRoot;
257
+ Item: typeof ToggleGroupItem;
258
+ };
259
+
260
+ /**
261
+ * Headless, accessible **ToggleGroup** — a compound component that
262
+ * manages a group of pressable toggle buttons with roving tabindex
263
+ * keyboard navigation.
264
+ *
265
+ * Supports `type="single"` (at most one item pressed, pressing again
266
+ * deselects) and `type="multiple"` (items toggle independently).
267
+ *
268
+ * `ToggleGroup` is both callable (an alias of
269
+ * {@link ToggleGroupRoot | `ToggleGroup.Root`}) and carries its
270
+ * sub-components as static properties.
271
+ *
272
+ * - {@link ToggleGroupRoot | `ToggleGroup.Root`} — state owner, context
273
+ * provider, `<div role="group">` wrapper.
274
+ * - {@link ToggleGroupItem | `ToggleGroup.Item`} — a pressable toggle
275
+ * button participating in the roving tabindex.
276
+ *
277
+ * @example Single mode — text alignment
278
+ * ```tsx
279
+ * import { ToggleGroup } from "@primitiv-ui/react";
280
+ *
281
+ * <ToggleGroup.Root type="single" defaultValue="left" aria-label="Alignment">
282
+ * <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
283
+ * <ToggleGroup.Item value="center">Center</ToggleGroup.Item>
284
+ * <ToggleGroup.Item value="right">Right</ToggleGroup.Item>
285
+ * </ToggleGroup.Root>;
286
+ * ```
287
+ *
288
+ * @see {@link ToggleGroupRoot} for type modes, state, and ARIA.
289
+ * @see {@link ToggleGroupItem} for selection, roving tabindex, and keyboard navigation.
290
+ */
291
+ const ToggleGroupCompound: TToggleGroupCompound = Object.assign(ToggleGroupRoot, {
292
+ Root: ToggleGroupRoot,
293
+ Item: ToggleGroupItem,
294
+ });
295
+
296
+ ToggleGroupCompound.displayName = "ToggleGroup";
297
+
298
+ export { ToggleGroupCompound as ToggleGroup };
@@ -0,0 +1,9 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ import { ToggleGroupContextValue } from "./types";
4
+
5
+ export const [ToggleGroupContext, useToggleGroupContext] =
6
+ createStrictContext<ToggleGroupContextValue>(
7
+ "ToggleGroup.Item must be rendered inside a ToggleGroup.Root",
8
+ "ToggleGroupContext",
9
+ );
@@ -0,0 +1,65 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { ToggleGroup } from "../ToggleGroup";
5
+
6
+ describe("ToggleGroup asChild composition", () => {
7
+ it("Root asChild renders the consumer's element instead of <div>", () => {
8
+ render(
9
+ <ToggleGroup.Root type="single" asChild aria-label="Alignment">
10
+ <section>
11
+ <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
12
+ </section>
13
+ </ToggleGroup.Root>,
14
+ );
15
+ const group = screen.getByRole("group", { name: "Alignment" });
16
+ expect(group.tagName).toBe("SECTION");
17
+ });
18
+
19
+ it("Root asChild merges role and data-orientation onto the child element", () => {
20
+ render(
21
+ <ToggleGroup.Root
22
+ type="single"
23
+ orientation="vertical"
24
+ asChild
25
+ aria-label="Alignment"
26
+ >
27
+ <section>
28
+ <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
29
+ </section>
30
+ </ToggleGroup.Root>,
31
+ );
32
+ const group = screen.getByRole("group", { name: "Alignment" });
33
+ expect(group).toHaveAttribute("data-orientation", "vertical");
34
+ });
35
+
36
+ it("Item asChild renders the consumer's element instead of <button>", () => {
37
+ render(
38
+ <ToggleGroup.Root type="single" aria-label="Alignment">
39
+ <ToggleGroup.Item value="left" asChild>
40
+ <div>Left</div>
41
+ </ToggleGroup.Item>
42
+ </ToggleGroup.Root>,
43
+ );
44
+ const item = screen.getByLabelText("Alignment").querySelector("[data-state]");
45
+ expect(item?.tagName).toBe("DIV");
46
+ expect(item).toHaveAttribute("aria-pressed", "false");
47
+ expect(item).toHaveAttribute("data-state", "off");
48
+ });
49
+
50
+ it("Item asChild composes onClick with the toggle handler", async () => {
51
+ const user = userEvent.setup();
52
+ const consumerClick = vi.fn();
53
+ render(
54
+ <ToggleGroup.Root type="single" aria-label="Alignment">
55
+ <ToggleGroup.Item value="left" asChild onClick={consumerClick}>
56
+ <div>Left</div>
57
+ </ToggleGroup.Item>
58
+ </ToggleGroup.Root>,
59
+ );
60
+ const item = screen.getByLabelText("Alignment").querySelector("[data-state]")!;
61
+ await user.click(item);
62
+ expect(consumerClick).toHaveBeenCalledOnce();
63
+ expect(item).toHaveAttribute("aria-pressed", "true");
64
+ });
65
+ });
@@ -0,0 +1,50 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { ToggleGroup } from "../ToggleGroup";
4
+
5
+ describe("ToggleGroup basic rendering", () => {
6
+ it('Root renders a <div role="group">', () => {
7
+ render(
8
+ <ToggleGroup.Root type="single" aria-label="Alignment">
9
+ <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
10
+ </ToggleGroup.Root>,
11
+ );
12
+ expect(screen.getByRole("group", { name: "Alignment" })).toBeInTheDocument();
13
+ });
14
+
15
+ it('Item renders a <button type="button" aria-pressed>', () => {
16
+ render(
17
+ <ToggleGroup.Root type="single" aria-label="Alignment">
18
+ <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
19
+ </ToggleGroup.Root>,
20
+ );
21
+ const item = screen.getByRole("button", { name: "Left" });
22
+ expect(item.tagName).toBe("BUTTON");
23
+ expect(item).toHaveAttribute("type", "button");
24
+ expect(item).toHaveAttribute("aria-pressed", "false");
25
+ });
26
+
27
+ it('Item defaults data-state to "off" when unpressed', () => {
28
+ render(
29
+ <ToggleGroup.Root type="single" aria-label="Alignment">
30
+ <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
31
+ </ToggleGroup.Root>,
32
+ );
33
+ expect(screen.getByRole("button", { name: "Left" })).toHaveAttribute(
34
+ "data-state",
35
+ "off",
36
+ );
37
+ });
38
+
39
+ it('Root sets data-orientation="horizontal" by default', () => {
40
+ render(
41
+ <ToggleGroup.Root type="single" aria-label="Alignment">
42
+ <ToggleGroup.Item value="left">Left</ToggleGroup.Item>
43
+ </ToggleGroup.Root>,
44
+ );
45
+ expect(screen.getByRole("group", { name: "Alignment" })).toHaveAttribute(
46
+ "data-orientation",
47
+ "horizontal",
48
+ );
49
+ });
50
+ });
@@ -0,0 +1,54 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { ToggleGroup } from "../ToggleGroup";
5
+
6
+ function Fixture({ disabledItem }: { disabledItem?: string }) {
7
+ return (
8
+ <ToggleGroup.Root type="single" aria-label="Alignment">
9
+ <ToggleGroup.Item value="left" disabled={disabledItem === "left"}>
10
+ Left
11
+ </ToggleGroup.Item>
12
+ <ToggleGroup.Item value="center" disabled={disabledItem === "center"}>
13
+ Center
14
+ </ToggleGroup.Item>
15
+ <ToggleGroup.Item value="right" disabled={disabledItem === "right"}>
16
+ Right
17
+ </ToggleGroup.Item>
18
+ </ToggleGroup.Root>
19
+ );
20
+ }
21
+
22
+ describe("ToggleGroup disabled item", () => {
23
+ it("sets the native disabled attribute on the item", () => {
24
+ render(<Fixture disabledItem="center" />);
25
+ expect(screen.getByRole("button", { name: "Center" })).toBeDisabled();
26
+ });
27
+
28
+ it('sets data-disabled="" on the disabled item', () => {
29
+ render(<Fixture disabledItem="center" />);
30
+ expect(screen.getByRole("button", { name: "Center" })).toHaveAttribute(
31
+ "data-disabled",
32
+ "",
33
+ );
34
+ });
35
+
36
+ it("does not toggle the disabled item when clicked", async () => {
37
+ const user = userEvent.setup();
38
+ render(<Fixture disabledItem="center" />);
39
+ await user.click(screen.getByRole("button", { name: "Center" }));
40
+ expect(screen.getByRole("button", { name: "Center" })).toHaveAttribute(
41
+ "aria-pressed",
42
+ "false",
43
+ );
44
+ });
45
+
46
+ it("skips disabled items during arrow key navigation", async () => {
47
+ const user = userEvent.setup();
48
+ render(<Fixture disabledItem="center" />);
49
+ screen.getByRole("button", { name: "Left" }).focus();
50
+ await user.keyboard("{ArrowRight}");
51
+ // Center is skipped — focus lands on Right
52
+ expect(screen.getByRole("button", { name: "Right" })).toHaveFocus();
53
+ });
54
+ });