@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,38 @@
1
+ /**
2
+ * Returns an event handler that runs the consumer-supplied handler first,
3
+ * then the library's own handler — unless the consumer called
4
+ * `event.preventDefault()`, in which case the library handler is skipped.
5
+ *
6
+ * This is the standard composition pattern used throughout headless
7
+ * components to let consumers attach their own listeners onto a
8
+ * sub-component without clobbering the component's own behaviour, while
9
+ * still giving them an opt-out via `preventDefault()`.
10
+ *
11
+ * @example Consumer logs every click; component still closes the modal
12
+ * afterwards:
13
+ * ```tsx
14
+ * <Modal.Overlay onClick={(e) => console.log("overlay clicked")} />
15
+ * ```
16
+ *
17
+ * @example Consumer vetoes the component's behaviour:
18
+ * ```tsx
19
+ * <Modal.Overlay onClick={(e) => {
20
+ * if (formIsDirty) e.preventDefault(); // don't close
21
+ * }} />
22
+ * ```
23
+ */
24
+ export function composeEventHandlers<E>(
25
+ theirHandler: ((event: E) => void) | undefined,
26
+ ourHandler: (event: E) => void,
27
+ { checkForDefaultPrevented = true }: { checkForDefaultPrevented?: boolean } = {},
28
+ ) {
29
+ return function handleEvent(event: E) {
30
+ theirHandler?.(event);
31
+ if (
32
+ !checkForDefaultPrevented ||
33
+ !(event as unknown as Event).defaultPrevented
34
+ ) {
35
+ ourHandler(event);
36
+ }
37
+ };
38
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./Slot";
2
+ export * from "./composeEventHandlers";
3
+ export * from "./types";
@@ -0,0 +1,5 @@
1
+ import { HTMLAttributes, ReactNode } from "react";
2
+
3
+ export type SlotProps = HTMLAttributes<HTMLElement> & {
4
+ children?: ReactNode;
5
+ };
@@ -0,0 +1,50 @@
1
+ # Status
2
+
3
+ A polite live region for low-priority, non-urgent status messages,
4
+ implementing the
5
+ [WAI-ARIA `status` role](https://www.w3.org/TR/wai-aria-1.2/#status).
6
+
7
+ ```tsx
8
+ import { Status } from "@primitiv-ui/react";
9
+
10
+ {
11
+ saved && <Status>All changes saved.</Status>;
12
+ }
13
+ ```
14
+
15
+ ## Props
16
+
17
+ | Prop | Type | Default | Notes |
18
+ | ----------- | ----------------------- | ------- | -------------------------------------------------- |
19
+ | `asChild` | `boolean` | `false` | Render the consumer's element instead of a `<div>` |
20
+ | `className` | `string` | — | Applied directly to the rendered element |
21
+ | `...rest` | `ComponentProps<"div">` | — | All other `<div>` props, including `aria-*` |
22
+
23
+ ## Behaviour
24
+
25
+ `Status` renders a `<div role="status">`. The `status` role carries an
26
+ implicit `aria-live="polite"` and `aria-atomic="true"`, so assistive
27
+ technology announces the message once the user is idle, without
28
+ interrupting them. Use it for confirmations, counts, and background
29
+ progress — for errors the user must see immediately, reach for
30
+ [`Alert`](../Alert/README.md).
31
+
32
+ ## Announce on change
33
+
34
+ A live region announces content that changes *after* the region is in
35
+ the DOM. Either render the `Status` conditionally, or keep it mounted
36
+ and update its text content:
37
+
38
+ ```tsx
39
+ <Status>{count} items in your cart</Status>
40
+ ```
41
+
42
+ ## asChild
43
+
44
+ Pass `asChild` to apply `role="status"` to the consumer's own element:
45
+
46
+ ```tsx
47
+ <Status asChild>
48
+ <output>{count} items in your cart</output>
49
+ </Status>
50
+ ```
@@ -0,0 +1,44 @@
1
+ import { Slot } from "../Slot";
2
+ import { StatusProps } from "./types";
3
+
4
+ /**
5
+ * A polite live region for low-priority, non-urgent status messages.
6
+ *
7
+ * Renders a `<div role="status">`. The `status` role carries an
8
+ * implicit `aria-live="polite"` and `aria-atomic="true"`, so assistive
9
+ * technology announces the message once the user is idle, without
10
+ * interrupting them — use it for confirmations, counts, and background
11
+ * progress. For errors the user must see immediately, reach for
12
+ * `Alert` instead.
13
+ *
14
+ * Render the `Status` conditionally, or update its text content while
15
+ * it stays mounted: the live region announces content that changes
16
+ * *after* it is already in the DOM.
17
+ *
18
+ * **`asChild` composition.** Renders the consumer's element instead of
19
+ * a `<div>`, merging `role="status"` and all other props in via the
20
+ * {@link Slot} utility.
21
+ *
22
+ * @example Confirmation message
23
+ * ```tsx
24
+ * {saved && <Status>All changes saved.</Status>}
25
+ * ```
26
+ *
27
+ * @example asChild — keep semantic markup
28
+ * ```tsx
29
+ * <Status asChild>
30
+ * <output>{count} items in your cart</output>
31
+ * </Status>
32
+ * ```
33
+ */
34
+ export function Status({ asChild = false, children, ...rest }: StatusProps) {
35
+ const rootProps = { role: "status", ...rest };
36
+
37
+ if (asChild) {
38
+ return <Slot {...rootProps}>{children}</Slot>;
39
+ }
40
+
41
+ return <div {...rootProps}>{children}</div>;
42
+ }
43
+
44
+ Status.displayName = "Status";
@@ -0,0 +1,28 @@
1
+ import { Status } from "..";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ describe("Status component", () => {
5
+ it("should render a div with role status containing its children", () => {
6
+ // Arrange
7
+ render(<Status>3 items added to your cart</Status>);
8
+
9
+ // Assert
10
+ const status = screen.getByRole("status");
11
+ expect(status.tagName).toBe("DIV");
12
+ expect(status).toHaveTextContent("3 items added to your cart");
13
+ });
14
+
15
+ it("should render the consumer element with asChild, keeping role status", () => {
16
+ // Arrange
17
+ render(
18
+ <Status asChild>
19
+ <section>Saved</section>
20
+ </Status>,
21
+ );
22
+
23
+ // Assert
24
+ const status = screen.getByRole("status");
25
+ expect(status.tagName).toBe("SECTION");
26
+ expect(status).toHaveTextContent("Saved");
27
+ });
28
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./Status";
2
+ export * from "./types";
@@ -0,0 +1,5 @@
1
+ import { ComponentProps } from "react";
2
+
3
+ export type StatusProps = ComponentProps<"div"> & {
4
+ asChild?: boolean;
5
+ };
@@ -0,0 +1,121 @@
1
+ # Switch
2
+
3
+ Headless, accessible **Switch** — a compound component implementing the
4
+ [WAI-ARIA Switch pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/)
5
+ on a native `<button role="switch">`. Semantically represents an immediate
6
+ on/off action (as opposed to a selection choice). Zero styles ship.
7
+
8
+ ```tsx
9
+ import { Switch } from "@primitiv-ui/react";
10
+
11
+ <Switch.Root defaultChecked aria-label="Enable notifications">
12
+ <Switch.Thumb />
13
+ </Switch.Root>
14
+ ```
15
+
16
+ ## Sub-components
17
+
18
+ | Export | Element | ARIA / data hooks | `asChild` |
19
+ |--------|---------|------------------|-----------|
20
+ | `Switch.Root` | `<button>` | `role="switch"`, `aria-checked`, `data-state`, `data-disabled` | yes |
21
+ | `Switch.Thumb` | `<span>` | `aria-hidden="true"`, `data-state` | yes |
22
+
23
+ ## State modes
24
+
25
+ ### Uncontrolled
26
+
27
+ Pass `defaultChecked` (or omit for unchecked on mount). The component owns
28
+ the value internally.
29
+
30
+ ```tsx
31
+ <Switch.Root defaultChecked aria-label="Enable dark mode">
32
+ <Switch.Thumb />
33
+ </Switch.Root>
34
+ ```
35
+
36
+ ### Controlled
37
+
38
+ Pass `checked` and `onCheckedChange` together. The parent owns the value.
39
+
40
+ ```tsx
41
+ const [enabled, setEnabled] = useState(false);
42
+
43
+ <Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="…">
44
+ <Switch.Thumb />
45
+ </Switch.Root>
46
+ ```
47
+
48
+ ## Keyboard interaction
49
+
50
+ | Key | Behaviour |
51
+ |-----|-----------|
52
+ | `Space` | Toggle the switch (native `<button>` behaviour) |
53
+ | `Enter` | Toggle the switch (native `<button>` behaviour) |
54
+ | `Tab` | Move focus to or from the switch |
55
+
56
+ ## Disabled
57
+
58
+ Pass `disabled` on the Root. The native attribute suppresses clicks and removes
59
+ the switch from the focus ring. `data-disabled=""` is set for CSS targeting.
60
+
61
+ ```tsx
62
+ <Switch.Root aria-label="Enable feature" disabled>
63
+ <Switch.Thumb />
64
+ </Switch.Root>
65
+ ```
66
+
67
+ ## The Thumb
68
+
69
+ `Switch.Thumb` is **always mounted** — unlike `Checkbox.Indicator`, it never
70
+ conditionally unmounts. Its visual position (left for off, right for on) is
71
+ driven entirely by CSS targeting `data-state`. This gives consumers a real DOM
72
+ node to animate with `transition` or Web Animations, rather than being
73
+ constrained to pseudo-elements.
74
+
75
+ ```scss
76
+ .switch-root {
77
+ position: relative;
78
+ width: 2.75rem;
79
+ height: 1.5rem;
80
+ border-radius: 9999px;
81
+ background: #d1d5db;
82
+ transition: background 120ms ease;
83
+
84
+ &[data-state="checked"] {
85
+ background: #6366f1;
86
+ }
87
+ }
88
+
89
+ .switch-thumb {
90
+ position: absolute;
91
+ top: 0.125rem;
92
+ left: 0.125rem;
93
+ width: 1.25rem;
94
+ height: 1.25rem;
95
+ border-radius: 50%;
96
+ background: white;
97
+ transition: translate 120ms ease;
98
+
99
+ &[data-state="checked"] {
100
+ translate: 1.25rem 0;
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## `asChild` composition
106
+
107
+ Both sub-components accept `asChild`. The library's ARIA attributes,
108
+ `data-state`, event handlers, and ref are merged onto the consumer's element.
109
+
110
+ ```tsx
111
+ <Switch.Root asChild aria-label="Enable notifications">
112
+ <div role="switch">…</div>
113
+ </Switch.Root>
114
+ ```
115
+
116
+ ## Styling hooks
117
+
118
+ | Attribute | Values | Set on |
119
+ |-----------|--------|--------|
120
+ | `data-state` | `"checked"` \| `"unchecked"` | `Switch.Root`, `Switch.Thumb` |
121
+ | `data-disabled` | `""` (present when disabled) | `Switch.Root` |
@@ -0,0 +1,167 @@
1
+ import { useMemo } from "react";
2
+
3
+ import { Slot, composeEventHandlers } from "../Slot";
4
+
5
+ import { SwitchContext } from "./SwitchContext";
6
+ import { useSwitchContext, useSwitchRoot } from "./hooks";
7
+ import { SwitchRootProps, SwitchThumbProps } from "./types";
8
+
9
+ /**
10
+ * The root of a Switch — a native `<button type="button" role="switch">` that
11
+ * owns the binary checked state and provides {@link SwitchContext} to
12
+ * descendant {@link SwitchThumb | `Switch.Thumb`}s.
13
+ *
14
+ * Supports two state modes, statically discriminated at the type level:
15
+ *
16
+ * - **Uncontrolled** — pass
17
+ * {@link SwitchRootProps.defaultChecked | `defaultChecked`} (or omit for
18
+ * unchecked-on-mount). The component owns the value internally.
19
+ * - **Controlled** — pass
20
+ * {@link SwitchRootProps.checked | `checked`} *and*
21
+ * {@link SwitchRootProps.onCheckedChange | `onCheckedChange`} together.
22
+ * The parent owns the value; every click defers back through the callback.
23
+ *
24
+ * **ARIA.** `role="switch"` and `aria-checked` are set automatically.
25
+ * Provide an accessible name via `aria-label`, `aria-labelledby`, or
26
+ * a visible label element.
27
+ *
28
+ * **Styling hooks.** `data-state="checked" | "unchecked"` on the root, plus
29
+ * `data-disabled=""` when disabled.
30
+ *
31
+ * **`asChild` prop.** Pass `asChild` to render any consumer-supplied element
32
+ * with the switch's ARIA attributes, `data-state`, composed `onClick`, and
33
+ * `ref` merged in. The native `<button>` is dropped.
34
+ *
35
+ * @example Uncontrolled
36
+ * ```tsx
37
+ * <Switch.Root defaultChecked aria-label="Enable notifications">
38
+ * <Switch.Thumb />
39
+ * </Switch.Root>
40
+ * ```
41
+ *
42
+ * @example Controlled
43
+ * ```tsx
44
+ * const [enabled, setEnabled] = useState(false);
45
+ *
46
+ * <Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="…">
47
+ * <Switch.Thumb />
48
+ * </Switch.Root>
49
+ * ```
50
+ */
51
+ function SwitchRoot({
52
+ defaultChecked,
53
+ checked,
54
+ onCheckedChange,
55
+ disabled,
56
+ asChild = false,
57
+ onClick,
58
+ children,
59
+ ref,
60
+ ...rest
61
+ }: SwitchRootProps) {
62
+ const { checked: isChecked, toggle } = useSwitchRoot({
63
+ defaultChecked,
64
+ checked,
65
+ onCheckedChange,
66
+ });
67
+ const contextValue = useMemo(() => ({ checked: isChecked }), [isChecked]);
68
+ const rootProps = {
69
+ ...rest,
70
+ ref,
71
+ role: "switch" as const,
72
+ "aria-checked": isChecked,
73
+ "data-state": isChecked ? ("checked" as const) : ("unchecked" as const),
74
+ "data-disabled": disabled ? "" : undefined,
75
+ disabled,
76
+ onClick: composeEventHandlers(onClick, toggle),
77
+ };
78
+ return (
79
+ <SwitchContext.Provider value={contextValue}>
80
+ {asChild ? (
81
+ <Slot {...rootProps}>{children}</Slot>
82
+ ) : (
83
+ <button type="button" {...rootProps}>
84
+ {children}
85
+ </button>
86
+ )}
87
+ </SwitchContext.Provider>
88
+ );
89
+ }
90
+
91
+ SwitchRoot.displayName = "SwitchRoot";
92
+
93
+ /**
94
+ * A decorative `<span aria-hidden="true">` that represents the sliding thumb
95
+ * of the switch. Always mounted — its position is driven entirely by CSS
96
+ * targeting `data-state="checked" | "unchecked"`. Unlike
97
+ * {@link Checkbox.Indicator}, it never conditionally unmounts.
98
+ *
99
+ * **Styling hook.** Mirrors the parent Root's
100
+ * `data-state="checked" | "unchecked"` for CSS transitions.
101
+ *
102
+ * **`asChild` prop.** Pass `asChild` to render the consumer's own element
103
+ * as the thumb, with `aria-hidden` and `data-state` merged in.
104
+ *
105
+ * @example
106
+ * ```tsx
107
+ * <Switch.Root aria-label="Enable notifications">
108
+ * <Switch.Thumb />
109
+ * </Switch.Root>
110
+ * ```
111
+ *
112
+ * @throws if rendered outside a `Switch.Root`.
113
+ */
114
+ function SwitchThumb({ children, asChild = false, ...rest }: SwitchThumbProps) {
115
+ const { checked } = useSwitchContext();
116
+ const thumbProps = {
117
+ ...rest,
118
+ "aria-hidden": "true" as const,
119
+ "data-state": checked ? ("checked" as const) : ("unchecked" as const),
120
+ };
121
+ if (asChild) {
122
+ return <Slot {...thumbProps}>{children}</Slot>;
123
+ }
124
+ return <span {...thumbProps}>{children}</span>;
125
+ }
126
+
127
+ SwitchThumb.displayName = "SwitchThumb";
128
+
129
+ type TSwitchCompound = typeof SwitchRoot & {
130
+ Root: typeof SwitchRoot;
131
+ Thumb: typeof SwitchThumb;
132
+ };
133
+
134
+ /**
135
+ * Headless, accessible **Switch** — a compound component implementing the
136
+ * [WAI-ARIA Switch pattern](https://www.w3.org/WAI/ARIA/apg/patterns/switch/)
137
+ * on a native `<button role="switch">`. Semantically represents an immediate
138
+ * on/off action (as opposed to a selection choice). Zero styles ship.
139
+ *
140
+ * `Switch` is both callable (an alias of
141
+ * {@link SwitchRoot | `Switch.Root`}) and carries its sub-components as
142
+ * static properties.
143
+ *
144
+ * - {@link SwitchRoot | `Switch.Root`} — state owner, context provider, toggle button.
145
+ * - {@link SwitchThumb | `Switch.Thumb`} — always-mounted decorative thumb;
146
+ * position driven by `data-state` via CSS.
147
+ *
148
+ * @example Minimal usage
149
+ * ```tsx
150
+ * import { Switch } from "@primitiv-ui/react";
151
+ *
152
+ * <Switch.Root aria-label="Enable notifications">
153
+ * <Switch.Thumb />
154
+ * </Switch.Root>
155
+ * ```
156
+ *
157
+ * @see {@link SwitchRoot} for state modes and ARIA details.
158
+ * @see {@link SwitchThumb} for styling the sliding thumb.
159
+ */
160
+ const SwitchCompound: TSwitchCompound = Object.assign(SwitchRoot, {
161
+ Root: SwitchRoot,
162
+ Thumb: SwitchThumb,
163
+ });
164
+
165
+ SwitchCompound.displayName = "Switch";
166
+
167
+ export { SwitchCompound as Switch };
@@ -0,0 +1,10 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ export type SwitchContextValue = {
4
+ checked: boolean;
5
+ };
6
+
7
+ export const [SwitchContext, useSwitchContext] =
8
+ createStrictContext<SwitchContextValue>(
9
+ "Switch.Thumb must be rendered inside a <Switch.Root>.",
10
+ );
@@ -0,0 +1,56 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Switch } from "../Switch";
5
+
6
+ describe("Switch asChild", () => {
7
+ it("renders the Root as the consumer element when asChild is set", () => {
8
+ // Arrange & Act
9
+ render(
10
+ <Switch.Root asChild aria-label="Enable notifications">
11
+ <div>Toggle</div>
12
+ </Switch.Root>,
13
+ );
14
+
15
+ // Assert — div rendered, not button; role and aria-checked merged
16
+ const sw = screen.getByRole("switch", { name: "Enable notifications" });
17
+ expect(sw.tagName).toBe("DIV");
18
+ expect(sw).toHaveAttribute("aria-checked", "false");
19
+ });
20
+
21
+ it("merges onClick and toggles state on the asChild Root element", async () => {
22
+ // Arrange
23
+ const user = userEvent.setup();
24
+ const onClick = vi.fn();
25
+ render(
26
+ <Switch.Root asChild aria-label="Enable notifications" onClick={onClick}>
27
+ <div>Toggle</div>
28
+ </Switch.Root>,
29
+ );
30
+
31
+ // Act
32
+ await user.click(screen.getByRole("switch", { name: "Enable notifications" }));
33
+
34
+ // Assert — consumer onClick ran and state toggled
35
+ expect(onClick).toHaveBeenCalledOnce();
36
+ expect(
37
+ screen.getByRole("switch", { name: "Enable notifications" }),
38
+ ).toHaveAttribute("aria-checked", "true");
39
+ });
40
+
41
+ it("renders the Thumb as the consumer element when asChild is set", () => {
42
+ // Arrange & Act
43
+ render(
44
+ <Switch.Root aria-label="Enable notifications">
45
+ <Switch.Thumb asChild>
46
+ <span data-testid="custom-thumb" />
47
+ </Switch.Thumb>
48
+ </Switch.Root>,
49
+ );
50
+
51
+ // Assert — our span rendered; aria-hidden and data-state merged onto it
52
+ const thumb = screen.getByTestId("custom-thumb");
53
+ expect(thumb).toHaveAttribute("aria-hidden", "true");
54
+ expect(thumb).toHaveAttribute("data-state", "unchecked");
55
+ });
56
+ });
@@ -0,0 +1,76 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Switch } from "../Switch";
4
+
5
+ describe("Switch basic rendering", () => {
6
+ it('renders a <button type="button" role="switch">', () => {
7
+ // Arrange & Act
8
+ render(<Switch.Root aria-label="Enable notifications" />);
9
+ const sw = screen.getByRole("switch", { name: "Enable notifications" });
10
+
11
+ // Assert
12
+ expect(sw.tagName).toBe("BUTTON");
13
+ expect(sw).toHaveAttribute("type", "button");
14
+ });
15
+
16
+ it('defaults aria-checked="false" when unchecked', () => {
17
+ // Arrange & Act
18
+ render(<Switch.Root aria-label="Enable notifications" />);
19
+
20
+ // Assert
21
+ expect(
22
+ screen.getByRole("switch", { name: "Enable notifications" }),
23
+ ).toHaveAttribute("aria-checked", "false");
24
+ });
25
+
26
+ it('defaults data-state="unchecked" on mount', () => {
27
+ // Arrange & Act
28
+ render(<Switch.Root aria-label="Enable notifications" />);
29
+
30
+ // Assert
31
+ expect(
32
+ screen.getByRole("switch", { name: "Enable notifications" }),
33
+ ).toHaveAttribute("data-state", "unchecked");
34
+ });
35
+
36
+ it("renders Switch.Thumb as a <span> inside the root", () => {
37
+ // Arrange & Act
38
+ render(
39
+ <Switch.Root aria-label="Enable notifications">
40
+ <Switch.Thumb data-testid="thumb" />
41
+ </Switch.Root>,
42
+ );
43
+
44
+ // Assert
45
+ const thumb = screen.getByTestId("thumb");
46
+ expect(thumb.tagName).toBe("SPAN");
47
+ expect(thumb).toBeInTheDocument();
48
+ });
49
+
50
+ it('sets aria-hidden="true" on the Thumb', () => {
51
+ // Arrange & Act
52
+ render(
53
+ <Switch.Root aria-label="Enable notifications">
54
+ <Switch.Thumb data-testid="thumb" />
55
+ </Switch.Root>,
56
+ );
57
+
58
+ // Assert
59
+ expect(screen.getByTestId("thumb")).toHaveAttribute("aria-hidden", "true");
60
+ });
61
+
62
+ it('sets data-state="unchecked" on Thumb by default', () => {
63
+ // Arrange & Act
64
+ render(
65
+ <Switch.Root aria-label="Enable notifications">
66
+ <Switch.Thumb data-testid="thumb" />
67
+ </Switch.Root>,
68
+ );
69
+
70
+ // Assert
71
+ expect(screen.getByTestId("thumb")).toHaveAttribute(
72
+ "data-state",
73
+ "unchecked",
74
+ );
75
+ });
76
+ });