@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,151 @@
1
+ import { useRef, useMemo, useCallback, useId, useEffect } from "react";
2
+
3
+ import { useCollection, useControllableState } from "../../hooks";
4
+
5
+ import type { AccordionReadingDirection } from "../types";
6
+
7
+ type TriggerMeta = {
8
+ element: HTMLButtonElement;
9
+ disabled: boolean;
10
+ };
11
+
12
+ export function useAccordionRoot(
13
+ controlledValue: string[] | undefined,
14
+ defaultValue: string | (readonly string[] & string) | undefined,
15
+ multiple: boolean,
16
+ onValueChange: ((values: string[]) => void) | undefined,
17
+ orientation: "vertical" | "horizontal",
18
+ dir: AccordionReadingDirection,
19
+ ) {
20
+ const accordionId = useId();
21
+ // updateKeysOnCleanup is false so trigger unmounts don't fire setState
22
+ // outside act() after a render-time validation throw.
23
+ const {
24
+ register: registerTriggerBase,
25
+ itemsRef: triggersRef,
26
+ keys: registeredTriggerItemIds,
27
+ } = useCollection<string, TriggerMeta>({ updateKeysOnCleanup: false });
28
+ const panelsRef = useRef<Set<string>>(new Set());
29
+
30
+ const registerTrigger = useCallback(
31
+ (
32
+ itemId: string,
33
+ element: HTMLButtonElement | null,
34
+ disabled = false,
35
+ ) => {
36
+ registerTriggerBase(itemId, element ? { element, disabled } : null);
37
+ },
38
+ [registerTriggerBase],
39
+ );
40
+
41
+ const disabledItemIds = useMemo(
42
+ () =>
43
+ new Set(
44
+ Array.from(triggersRef.current.entries())
45
+ .filter(([, meta]) => meta.disabled)
46
+ .map(([id]) => id),
47
+ ),
48
+ // registeredTriggerItemIds is a fresh array on every register call (new
49
+ // identity even when the keys are the same), so the memo re-runs whenever
50
+ // any trigger mounts, unmounts, or toggles disabled — which is exactly
51
+ // the trigger we want for re-deriving disabledItemIds.
52
+ [registeredTriggerItemIds, triggersRef],
53
+ );
54
+
55
+ const focusTrigger = useCallback(
56
+ (itemId: string) => {
57
+ triggersRef.current.get(itemId)?.element.focus();
58
+ },
59
+ [triggersRef],
60
+ );
61
+
62
+ // Like Tabs, Accordion's existing public contract is that uncontrolled
63
+ // mode does NOT call onValueChange — only the controlled path notifies the
64
+ // consumer. So we don't pass onValueChange to useControllableState; the
65
+ // toggleItem branch below fires it directly in controlled mode.
66
+ const [expandedItems, setExpandedItems, isControlled] = useControllableState<
67
+ Set<string>
68
+ >(
69
+ controlledValue !== undefined ? new Set(controlledValue) : undefined,
70
+ defaultValue !== undefined ? new Set([defaultValue]) : new Set(),
71
+ );
72
+
73
+ const computeNext = (prev: Set<string>, itemId: string): Set<string> => {
74
+ const next = new Set(prev);
75
+ if (next.has(itemId)) {
76
+ next.delete(itemId);
77
+ } else if (multiple) {
78
+ next.add(itemId);
79
+ } else {
80
+ next.clear();
81
+ next.add(itemId);
82
+ }
83
+ return next;
84
+ };
85
+
86
+ const toggleItem = useCallback(
87
+ (itemId: string) => {
88
+ const next = computeNext(expandedItems, itemId);
89
+ if (isControlled) {
90
+ onValueChange?.(Array.from(next));
91
+ } else {
92
+ setExpandedItems(next);
93
+ }
94
+ },
95
+ [expandedItems, isControlled, multiple, setExpandedItems, onValueChange],
96
+ );
97
+
98
+ // Panels are tracked in a ref only. registerPanel does NOT call setState —
99
+ // the validation effect reads panelsRef.current directly. Because React 18
100
+ // runs all effects in a commit before flushing batched state updates, the
101
+ // panel effect always executes (and updates panelsRef) before the re-render
102
+ // triggered by trigger registration reaches the validation effect.
103
+ const registerPanel = useCallback((itemId: string) => {
104
+ panelsRef.current.add(itemId);
105
+ }, []);
106
+
107
+ const unregisterPanel = useCallback((itemId: string) => {
108
+ panelsRef.current.delete(itemId);
109
+ }, []);
110
+
111
+ useEffect(() => {
112
+ for (const triggerId of registeredTriggerItemIds) {
113
+ if (!panelsRef.current.has(triggerId)) {
114
+ throw new Error(
115
+ `AccordionTrigger (item "${triggerId}") has no corresponding AccordionContent. Every AccordionItem with a Trigger must also contain an AccordionContent.`,
116
+ );
117
+ }
118
+ }
119
+ }, [registeredTriggerItemIds]);
120
+
121
+ const contextValue = useMemo(
122
+ () => ({
123
+ accordionId,
124
+ expandedItems,
125
+ orientation,
126
+ dir,
127
+ toggleItem,
128
+ registerTrigger,
129
+ registeredTriggerItemIds,
130
+ disabledItemIds,
131
+ focusTrigger,
132
+ registerPanel,
133
+ unregisterPanel,
134
+ }),
135
+ [
136
+ accordionId,
137
+ expandedItems,
138
+ orientation,
139
+ dir,
140
+ toggleItem,
141
+ registerTrigger,
142
+ registeredTriggerItemIds,
143
+ disabledItemIds,
144
+ focusTrigger,
145
+ registerPanel,
146
+ unregisterPanel,
147
+ ],
148
+ );
149
+
150
+ return { contextValue };
151
+ }
@@ -0,0 +1,90 @@
1
+ import { useMemo, useRef, useEffect, MouseEvent, KeyboardEvent } from "react";
2
+
3
+ import { useRovingTabindex } from "../../hooks";
4
+ import { composeRefs } from "../../Slot";
5
+
6
+ import { AccordionTriggerProps } from "../types";
7
+
8
+ import { useAccordionContext } from "./useAccordionContext";
9
+ import { useAccordionItemContext } from "./useAccordionItemContext";
10
+
11
+ export function useAccordionTrigger({
12
+ ref,
13
+ onClick,
14
+ disabled,
15
+ asChild = false,
16
+ ...rest
17
+ }: Omit<AccordionTriggerProps, "children">) {
18
+ const { buttonId, panelId, itemId, isExpanded } = useAccordionItemContext();
19
+ const {
20
+ toggleItem,
21
+ registerTrigger,
22
+ registeredTriggerItemIds,
23
+ disabledItemIds,
24
+ focusTrigger,
25
+ orientation,
26
+ dir,
27
+ } = useAccordionContext();
28
+ const triggerRef = useRef<HTMLButtonElement>(null);
29
+ const composedRef = ref ? composeRefs(triggerRef, ref) : triggerRef;
30
+
31
+ // Register/unregister this trigger with the context. The disabled flag
32
+ // is now tracked in registration metadata (via useCollection's value
33
+ // type) instead of being read from the rendered aria-disabled attribute,
34
+ // which keeps Accordion consistent with RadioGroup and is the model
35
+ // useRovingTabindex expects.
36
+ useEffect(() => {
37
+ registerTrigger(itemId, triggerRef.current, disabled);
38
+ return () => registerTrigger(itemId, null);
39
+ }, [itemId, disabled, registerTrigger]);
40
+
41
+ function handleClick(e: MouseEvent<HTMLButtonElement>) {
42
+ if (disabled) return;
43
+ toggleItem(itemId);
44
+ onClick?.(e);
45
+ }
46
+
47
+ // Pre-filter disabled triggers out of the navigable list — Accordion's
48
+ // ARIA contract is that arrow keys skip past disabled triggers (unlike
49
+ // Tabs, which lands on disabled triggers without activating them).
50
+ const enabledItemIds = useMemo(
51
+ () => registeredTriggerItemIds.filter((id) => !disabledItemIds.has(id)),
52
+ [registeredTriggerItemIds, disabledItemIds],
53
+ );
54
+ const { handleKeyDown: rovingKeyDown } = useRovingTabindex<string>({
55
+ orientation,
56
+ dir,
57
+ navigable: enabledItemIds,
58
+ currentKey: itemId,
59
+ onNavigate: focusTrigger,
60
+ includeHomeEnd: true,
61
+ });
62
+
63
+ function handleKeyDown(e: KeyboardEvent<HTMLButtonElement>) {
64
+ // Accordion-specific: Enter / Space toggle the focused item rather than
65
+ // activate something else, so they're handled here before delegating
66
+ // arrow / Home / End to the shared hook.
67
+ if ((e.key === "Enter" || e.key === " ") && !disabled) {
68
+ e.preventDefault();
69
+ toggleItem(itemId);
70
+ return;
71
+ }
72
+ rovingKeyDown(e);
73
+ }
74
+
75
+ const triggerProps = {
76
+ ref: composedRef,
77
+ "aria-expanded": isExpanded,
78
+ id: buttonId,
79
+ "aria-controls": panelId,
80
+ "aria-disabled": disabled,
81
+ "data-disabled": disabled,
82
+ ...(asChild && disabled ? { role: "button" } : {}),
83
+ onClick: handleClick,
84
+ onKeyDown: handleKeyDown,
85
+ "data-state": isExpanded ? "open" : "closed",
86
+ ...rest,
87
+ };
88
+
89
+ return { triggerProps };
90
+ }
@@ -0,0 +1 @@
1
+ export * from './Accordion';
@@ -0,0 +1,81 @@
1
+ import { ComponentProps, ReactNode, Ref } from "react";
2
+ import { HeadingLevel } from "../types";
3
+
4
+ type AccordionRootBaseProps = ComponentProps<"div"> & {
5
+ multiple?: boolean;
6
+ orientation?: "vertical" | "horizontal";
7
+ dir?: AccordionReadingDirection;
8
+ };
9
+
10
+ type AccordionRootUncontrolledProps = AccordionRootBaseProps & {
11
+ defaultValue?: string;
12
+ value?: never;
13
+ onValueChange?: never;
14
+ };
15
+
16
+ type AccordionRootControlledProps = AccordionRootBaseProps & {
17
+ defaultValue?: never;
18
+ value: string[];
19
+ onValueChange: (values: string[]) => void;
20
+ };
21
+
22
+ export type AccordionReadingDirection = "ltr" | "rtl";
23
+
24
+ export type AccordionRootProps =
25
+ | AccordionRootUncontrolledProps
26
+ | AccordionRootControlledProps;
27
+
28
+ export type AccordionItemProps = ComponentProps<"div"> & {
29
+ children: ReactNode;
30
+ value?: string; // Optional - if not provided, useId() will generate one
31
+ };
32
+
33
+ export type AccordionTriggerProps<
34
+ T extends HTMLElement = HTMLButtonElement,
35
+ > = Omit<ComponentProps<"button">, "disabled" | "ref"> & {
36
+ children: ReactNode;
37
+ disabled?: boolean;
38
+ asChild?: boolean;
39
+ /** Ref to the rendered element. Defaults to `HTMLButtonElement`; when using
40
+ * `asChild`, specify the child's element type (e.g. `HTMLAnchorElement`). */
41
+ ref?: Ref<T>;
42
+ };
43
+
44
+ export type AccordionHeaderProps = ComponentProps<"h3"> & {
45
+ children: ReactNode;
46
+ level?: HeadingLevel;
47
+ };
48
+
49
+ export type AccordionContentProps = ComponentProps<"div"> & {
50
+ children: ReactNode;
51
+ forceMount?: boolean;
52
+ };
53
+
54
+ export type AccordionTriggerIconProps = ComponentProps<"span"> & {
55
+ children: ReactNode;
56
+ };
57
+
58
+ export type AccordionContextValue = {
59
+ accordionId: string;
60
+ expandedItems: Set<string>;
61
+ orientation: "vertical" | "horizontal";
62
+ dir: AccordionReadingDirection;
63
+ toggleItem: (itemId: string) => void;
64
+ registerTrigger: (
65
+ itemId: string,
66
+ element: HTMLButtonElement | null,
67
+ disabled?: boolean,
68
+ ) => void;
69
+ registeredTriggerItemIds: string[];
70
+ disabledItemIds: Set<string>;
71
+ focusTrigger: (itemId: string) => void;
72
+ registerPanel: (itemId: string) => void;
73
+ unregisterPanel: (itemId: string) => void;
74
+ };
75
+
76
+ export type AccordionItemContextValue = {
77
+ buttonId: string;
78
+ panelId: string;
79
+ itemId: string;
80
+ isExpanded: boolean;
81
+ };
@@ -0,0 +1,43 @@
1
+ import { Slot } from "../Slot";
2
+ import { AlertProps } from "./types";
3
+
4
+ /**
5
+ * An assertive live region for high-priority, time-sensitive messages.
6
+ *
7
+ * Renders a `<div role="alert">`. The `alert` role carries an implicit
8
+ * `aria-live="assertive"` and `aria-atomic="true"`, so assistive
9
+ * technology interrupts the user to announce the message as soon as it
10
+ * appears — use it for errors and other content the user must see now.
11
+ * For non-urgent updates, reach for `Status` instead.
12
+ *
13
+ * Render the `Alert` conditionally: the live region announces content
14
+ * that appears *after* it is already in the DOM. Mounting an `Alert`
15
+ * that already contains its message may not be announced reliably.
16
+ *
17
+ * **`asChild` composition.** Renders the consumer's element instead of
18
+ * a `<div>`, merging `role="alert"` and all other props in via the
19
+ * {@link Slot} utility.
20
+ *
21
+ * @example Form error
22
+ * ```tsx
23
+ * {error && <Alert>{error}</Alert>}
24
+ * ```
25
+ *
26
+ * @example asChild — keep semantic markup
27
+ * ```tsx
28
+ * <Alert asChild>
29
+ * <section>Upload failed — try again.</section>
30
+ * </Alert>
31
+ * ```
32
+ */
33
+ export function Alert({ asChild = false, children, ...rest }: AlertProps) {
34
+ const rootProps = { role: "alert", ...rest };
35
+
36
+ if (asChild) {
37
+ return <Slot {...rootProps}>{children}</Slot>;
38
+ }
39
+
40
+ return <div {...rootProps}>{children}</div>;
41
+ }
42
+
43
+ Alert.displayName = "Alert";
@@ -0,0 +1,54 @@
1
+ # Alert
2
+
3
+ An assertive live region for high-priority, time-sensitive messages,
4
+ implementing the
5
+ [WAI-ARIA `alert` role](https://www.w3.org/TR/wai-aria-1.2/#alert).
6
+
7
+ ```tsx
8
+ import { Alert } from "@primitiv-ui/react";
9
+
10
+ {
11
+ error && <Alert>{error}</Alert>;
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
+ `Alert` renders a `<div role="alert">`. The `alert` role carries an
26
+ implicit `aria-live="assertive"` and `aria-atomic="true"`, so assistive
27
+ technology interrupts the user to announce the message as soon as it
28
+ appears. Use it for errors and other content the user must see now —
29
+ for non-urgent updates, reach for [`Status`](../Status/README.md).
30
+
31
+ ## Announce on appearance
32
+
33
+ A live region announces content that changes *after* the region is in
34
+ the DOM. Render the `Alert` **conditionally** so the message appears
35
+ into an already-mounted tree:
36
+
37
+ ```tsx
38
+ {
39
+ submitError && <Alert>{submitError}</Alert>;
40
+ }
41
+ ```
42
+
43
+ Mounting an `Alert` that already contains its message may not be
44
+ announced reliably.
45
+
46
+ ## asChild
47
+
48
+ Pass `asChild` to apply `role="alert"` to the consumer's own element:
49
+
50
+ ```tsx
51
+ <Alert asChild>
52
+ <section>Upload failed — try again.</section>
53
+ </Alert>
54
+ ```
@@ -0,0 +1,28 @@
1
+ import { Alert } from "..";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ describe("Alert component", () => {
5
+ it("should render a div with role alert containing its children", () => {
6
+ // Arrange
7
+ render(<Alert>Your changes could not be saved</Alert>);
8
+
9
+ // Assert
10
+ const alert = screen.getByRole("alert");
11
+ expect(alert.tagName).toBe("DIV");
12
+ expect(alert).toHaveTextContent("Your changes could not be saved");
13
+ });
14
+
15
+ it("should render the consumer element with asChild, keeping role alert", () => {
16
+ // Arrange
17
+ render(
18
+ <Alert asChild>
19
+ <section>Upload failed</section>
20
+ </Alert>,
21
+ );
22
+
23
+ // Assert
24
+ const alert = screen.getByRole("alert");
25
+ expect(alert.tagName).toBe("SECTION");
26
+ expect(alert).toHaveTextContent("Upload failed");
27
+ });
28
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./Alert";
2
+ export * from "./types";
@@ -0,0 +1,5 @@
1
+ import { ComponentProps } from "react";
2
+
3
+ export type AlertProps = ComponentProps<"div"> & {
4
+ asChild?: boolean;
5
+ };
@@ -0,0 +1,149 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+
3
+ import { Slot } from "../Slot";
4
+
5
+ import { AvatarContext } from "./AvatarContext";
6
+ import { useAvatarContext, useAvatarImage } from "./hooks";
7
+ import {
8
+ AvatarFallbackProps,
9
+ AvatarImageLoadingStatus,
10
+ AvatarImageProps,
11
+ AvatarRootProps,
12
+ } from "./types";
13
+
14
+ /**
15
+ * The root of an Avatar — a `<span>` container that owns the image loading
16
+ * status and provides {@link AvatarContext} to a descendant
17
+ * {@link Avatar.Image | `Avatar.Image`} and
18
+ * {@link Avatar.Fallback | `Avatar.Fallback`}.
19
+ *
20
+ * **Styling hooks.** `data-status="idle" | "loading" | "loaded" | "error"`.
21
+ *
22
+ * **`asChild` prop.** Pass `asChild` to render the consumer's own element as
23
+ * the container, with the `data-status` hook merged in.
24
+ */
25
+ function AvatarRoot({ asChild = false, children, ...rest }: AvatarRootProps) {
26
+ const [status, setStatus] = useState<AvatarImageLoadingStatus>("idle");
27
+
28
+ const contextValue = useMemo(() => ({ status, setStatus }), [status]);
29
+
30
+ const rootProps = { ...rest, "data-status": status };
31
+
32
+ return (
33
+ <AvatarContext.Provider value={contextValue}>
34
+ {asChild ? (
35
+ <Slot {...rootProps}>{children}</Slot>
36
+ ) : (
37
+ <span {...rootProps}>{children}</span>
38
+ )}
39
+ </AvatarContext.Provider>
40
+ );
41
+ }
42
+
43
+ AvatarRoot.displayName = "AvatarRoot";
44
+
45
+ /**
46
+ * The image of an Avatar — an `<img>` that reports its load lifecycle to the
47
+ * parent {@link Avatar.Root}.
48
+ *
49
+ * **Styling hooks.** `data-status` mirrors the root's status. The image stays
50
+ * mounted on error; hide a broken image with CSS, e.g.
51
+ * `img:not([data-status="loaded"]) { display: none }`.
52
+ *
53
+ * **`asChild` prop.** Pass `asChild` to render the consumer's own `<img>`,
54
+ * with the load handlers, ref, and `data-status` hook merged in.
55
+ *
56
+ * @throws if rendered outside an `Avatar.Root`.
57
+ */
58
+ function AvatarImage({ asChild = false, children, ...rest }: AvatarImageProps) {
59
+ const { status, setStatus } = useAvatarContext();
60
+ const { ref, onLoad, onError } = useAvatarImage(setStatus);
61
+
62
+ const imageProps = {
63
+ ...rest,
64
+ "data-status": status,
65
+ onLoad,
66
+ onError,
67
+ };
68
+
69
+ if (asChild) {
70
+ return (
71
+ <Slot {...imageProps} ref={ref}>
72
+ {children}
73
+ </Slot>
74
+ );
75
+ }
76
+
77
+ return <img {...imageProps} ref={ref} />;
78
+ }
79
+
80
+ AvatarImage.displayName = "AvatarImage";
81
+
82
+ /**
83
+ * The fallback of an Avatar — a `<span>` shown while the parent
84
+ * {@link Avatar.Root}'s image is anything other than `"loaded"` (missing,
85
+ * loading, or failed). Once the image loads, the fallback unmounts.
86
+ *
87
+ * Pass `delayMs` to withhold the fallback for that many milliseconds after
88
+ * mount, avoiding a flash of fallback content when the image loads quickly.
89
+ *
90
+ * **`asChild` prop.** Pass `asChild` to render the consumer's own element as
91
+ * the fallback, with the `data-status` hook merged in.
92
+ *
93
+ * @throws if rendered outside an `Avatar.Root`.
94
+ */
95
+ function AvatarFallback({
96
+ delayMs,
97
+ asChild = false,
98
+ children,
99
+ ...rest
100
+ }: AvatarFallbackProps) {
101
+ const { status } = useAvatarContext();
102
+ const [delayElapsed, setDelayElapsed] = useState(delayMs === undefined);
103
+
104
+ useEffect(() => {
105
+ if (delayMs === undefined) {
106
+ return;
107
+ }
108
+ const timer = window.setTimeout(() => setDelayElapsed(true), delayMs);
109
+ return () => window.clearTimeout(timer);
110
+ }, [delayMs]);
111
+
112
+ if (status === "loaded" || !delayElapsed) {
113
+ return null;
114
+ }
115
+
116
+ const fallbackProps = { ...rest, "data-status": status };
117
+
118
+ if (asChild) {
119
+ return <Slot {...fallbackProps}>{children}</Slot>;
120
+ }
121
+
122
+ return <span {...fallbackProps}>{children}</span>;
123
+ }
124
+
125
+ AvatarFallback.displayName = "AvatarFallback";
126
+
127
+ type TAvatarCompound = typeof AvatarRoot & {
128
+ Root: typeof AvatarRoot;
129
+ Image: typeof AvatarImage;
130
+ Fallback: typeof AvatarFallback;
131
+ };
132
+
133
+ /**
134
+ * Headless, accessible **Avatar** — a compound component for a user image
135
+ * with a graceful fallback. Zero styles ship.
136
+ *
137
+ * - {@link Avatar.Root | `Avatar.Root`} — container, owns loading status.
138
+ * - {@link Avatar.Image | `Avatar.Image`} — the `<img>`, reports its status.
139
+ * - {@link Avatar.Fallback | `Avatar.Fallback`} — shown until the image loads.
140
+ */
141
+ const AvatarCompound: TAvatarCompound = Object.assign(AvatarRoot, {
142
+ Root: AvatarRoot,
143
+ Image: AvatarImage,
144
+ Fallback: AvatarFallback,
145
+ });
146
+
147
+ AvatarCompound.displayName = "Avatar";
148
+
149
+ export { AvatarCompound as Avatar };
@@ -0,0 +1,20 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ import { AvatarImageLoadingStatus } from "./types";
4
+
5
+ /**
6
+ * Value shared by {@link Avatar.Root} with its descendant
7
+ * {@link Avatar.Image} and {@link Avatar.Fallback}.
8
+ */
9
+ export type AvatarContextValue = {
10
+ /** Current image loading status, owned by the root. */
11
+ status: AvatarImageLoadingStatus;
12
+ /** Reports a new loading status up to the root. */
13
+ setStatus: (status: AvatarImageLoadingStatus) => void;
14
+ };
15
+
16
+ export const [AvatarContext, useAvatarContext] =
17
+ createStrictContext<AvatarContextValue>(
18
+ "Avatar.Image and Avatar.Fallback must be rendered inside an <Avatar.Root>.",
19
+ "AvatarContext",
20
+ );