@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,67 @@
1
+ import { CSSProperties } from "react";
2
+ import { Slot } from "../Slot";
3
+ import { VisuallyHiddenProps } from "./types";
4
+
5
+ const visuallyHiddenStyle: CSSProperties = {
6
+ position: "absolute",
7
+ width: 1,
8
+ height: 1,
9
+ padding: 0,
10
+ margin: -1,
11
+ overflow: "hidden",
12
+ clip: "rect(0 0 0 0)",
13
+ clipPath: "inset(50%)",
14
+ whiteSpace: "nowrap",
15
+ borderWidth: 0,
16
+ };
17
+
18
+ /**
19
+ * Visually hides its children while keeping them in the accessibility tree.
20
+ *
21
+ * Renders a `<span>` carrying the canonical screen-reader-only clip styles:
22
+ * the content is removed from the visual layout but still announced by
23
+ * assistive technology. Use it for text that gives a control or region an
24
+ * accessible name without showing on screen.
25
+ *
26
+ * **Functional styles.** Unlike other `@primitiv-ui/react` components, the
27
+ * clip styles are applied inline because they *are* the component's
28
+ * behaviour, not decoration. A consumer `style` is merged on top, so any
29
+ * individual property can still be overridden.
30
+ *
31
+ * **`asChild` composition.** Renders the consumer's element instead of a
32
+ * `<span>`, merging the clip styles in via the {@link Slot} utility.
33
+ *
34
+ * @example Accessible name for an icon-only button
35
+ * ```tsx
36
+ * <button>
37
+ * <SearchIcon aria-hidden="true" />
38
+ * <VisuallyHidden>Search</VisuallyHidden>
39
+ * </button>
40
+ * ```
41
+ *
42
+ * @example asChild — keep semantic markup hidden
43
+ * ```tsx
44
+ * <VisuallyHidden asChild>
45
+ * <h2>Section heading</h2>
46
+ * </VisuallyHidden>
47
+ * ```
48
+ */
49
+ export function VisuallyHidden({
50
+ asChild = false,
51
+ children,
52
+ style,
53
+ ...rest
54
+ }: VisuallyHiddenProps) {
55
+ const rootProps = {
56
+ ...rest,
57
+ style: { ...visuallyHiddenStyle, ...style },
58
+ };
59
+
60
+ if (asChild) {
61
+ return <Slot {...rootProps}>{children}</Slot>;
62
+ }
63
+
64
+ return <span {...rootProps}>{children}</span>;
65
+ }
66
+
67
+ VisuallyHidden.displayName = "VisuallyHidden";
@@ -0,0 +1,59 @@
1
+ import { VisuallyHidden } from "..";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ describe("VisuallyHidden component", () => {
5
+ it("should render a span containing its children", () => {
6
+ // Arrange
7
+ render(<VisuallyHidden>Loading complete</VisuallyHidden>);
8
+
9
+ // Assert
10
+ const hidden = screen.getByText("Loading complete");
11
+ expect(hidden.tagName).toBe("SPAN");
12
+ });
13
+
14
+ it("should apply the screen-reader-only clip styles", () => {
15
+ // Arrange
16
+ render(<VisuallyHidden>Loading complete</VisuallyHidden>);
17
+
18
+ // Assert
19
+ const hidden = screen.getByText("Loading complete");
20
+ expect(hidden).toHaveStyle({
21
+ position: "absolute",
22
+ width: "1px",
23
+ height: "1px",
24
+ overflow: "hidden",
25
+ whiteSpace: "nowrap",
26
+ });
27
+ });
28
+
29
+ it("should merge a consumer style over the clip styles", () => {
30
+ // Arrange
31
+ render(
32
+ <VisuallyHidden style={{ position: "static", display: "block" }}>
33
+ Loading complete
34
+ </VisuallyHidden>,
35
+ );
36
+
37
+ // Assert
38
+ const hidden = screen.getByText("Loading complete");
39
+ expect(hidden).toHaveStyle({
40
+ position: "static",
41
+ display: "block",
42
+ whiteSpace: "nowrap",
43
+ });
44
+ });
45
+
46
+ it("should render the consumer element with asChild, keeping the clip styles", () => {
47
+ // Arrange
48
+ render(
49
+ <VisuallyHidden asChild>
50
+ <label>Search</label>
51
+ </VisuallyHidden>,
52
+ );
53
+
54
+ // Assert
55
+ const hidden = screen.getByText("Search");
56
+ expect(hidden.tagName).toBe("LABEL");
57
+ expect(hidden).toHaveStyle({ position: "absolute", overflow: "hidden" });
58
+ });
59
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./VisuallyHidden";
2
+ export * from "./types";
@@ -0,0 +1,5 @@
1
+ import { ComponentProps } from "react";
2
+
3
+ export type VisuallyHiddenProps = ComponentProps<"span"> & {
4
+ asChild?: boolean;
5
+ };
@@ -0,0 +1,3 @@
1
+ export { useControllableState } from "./useControllableState";
2
+ export { useCollection } from "./useCollection";
3
+ export { useRovingTabindex } from "./useRovingTabindex";
@@ -0,0 +1,74 @@
1
+ import { useCallback, useRef, useState, type RefObject } from "react";
2
+
3
+ type UseCollectionOptions = {
4
+ /**
5
+ * Whether to update the `keys` state when a registered item is
6
+ * unregistered (the registrar is called with a `null` value). Defaults to
7
+ * `true`.
8
+ *
9
+ * Set to `false` for collections that may unmount after a render-time
10
+ * throw (such as Accordion's trigger validation): in that flow, an effect
11
+ * cleanup running outside React's `act()` and updating state would surface
12
+ * as a "setState outside act" warning and mask the original error. With
13
+ * `false`, the ref is still updated — the validation effect that reads
14
+ * the ref still works — but the keys array stays stable until the next
15
+ * mount.
16
+ */
17
+ updateKeysOnCleanup?: boolean;
18
+ };
19
+
20
+ /**
21
+ * Manages a registry of mounted child elements (or any per-key value)
22
+ * inside a compound component. Used by Roots that need to know which
23
+ * Triggers / Items currently exist — for roving tabindex, for keyboard
24
+ * navigation, and for validating consumer-supplied `value` against
25
+ * registered keys.
26
+ *
27
+ * Returns a stable `register(key, value | null)` callback (call with
28
+ * `null` to unregister), a `RefObject` exposing the live `Map` (for
29
+ * imperative reads inside event handlers, where you want the latest
30
+ * value without rendering), and a `keys` array tracked as React state
31
+ * (so consumers re-render whenever items mount or unmount).
32
+ *
33
+ * @example Inside a Root hook
34
+ * ```ts
35
+ * const { register, itemsRef, keys } = useCollection<string, HTMLButtonElement>();
36
+ *
37
+ * // Inside a Trigger's effect:
38
+ * useEffect(() => {
39
+ * register(value, ref.current);
40
+ * return () => register(value, null);
41
+ * }, [value, register]);
42
+ *
43
+ * // Imperatively focus the first registered trigger:
44
+ * keys[0] && itemsRef.current.get(keys[0])?.focus();
45
+ * ```
46
+ */
47
+ export function useCollection<K, V>(
48
+ options: UseCollectionOptions = {},
49
+ ): {
50
+ register: (key: K, value: V | null) => void;
51
+ itemsRef: RefObject<Map<K, V>>;
52
+ keys: K[];
53
+ } {
54
+ const { updateKeysOnCleanup = true } = options;
55
+ const itemsRef = useRef<Map<K, V>>(new Map());
56
+ const [keys, setKeys] = useState<K[]>([]);
57
+
58
+ const register = useCallback(
59
+ (key: K, value: V | null) => {
60
+ if (value !== null) {
61
+ itemsRef.current.set(key, value);
62
+ setKeys(Array.from(itemsRef.current.keys()));
63
+ } else {
64
+ itemsRef.current.delete(key);
65
+ if (updateKeysOnCleanup) {
66
+ setKeys(Array.from(itemsRef.current.keys()));
67
+ }
68
+ }
69
+ },
70
+ [updateKeysOnCleanup],
71
+ );
72
+
73
+ return { register, itemsRef, keys };
74
+ }
@@ -0,0 +1,81 @@
1
+ import { useCallback, useState } from "react";
2
+
3
+ /**
4
+ * Manages the controlled / uncontrolled state pair (`value` +
5
+ * `defaultValue` + `onChange`) that almost every compound component in
6
+ * this library exposes on its Root. Centralises the "is the consumer
7
+ * driving this externally?" branch and the matching `useState`
8
+ * fallback.
9
+ *
10
+ * Behaviour:
11
+ *
12
+ * - When `controlled` is `undefined`, the hook is **uncontrolled**: it
13
+ * holds an internal `useState` initialised from `defaultValue`.
14
+ * Calling the returned setter updates that internal state **and**
15
+ * notifies `onChange`.
16
+ * - When `controlled` is anything other than `undefined`, the hook is
17
+ * **controlled**: the returned `value` is `controlled` directly,
18
+ * internal state is ignored, and the setter only notifies
19
+ * `onChange` (the consumer is expected to flip `controlled` on the
20
+ * next render).
21
+ *
22
+ * The setter does **not** dedupe — callers that need to skip
23
+ * `onChange` when the value hasn't changed should add that check at
24
+ * the call site (e.g. RadioGroup's `select`, Dropdown's `setOpen`).
25
+ *
26
+ * @example Boolean open/close state
27
+ * ```ts
28
+ * const [open, setOpen, isControlled] = useControllableState(
29
+ * controlledOpen, defaultOpen ?? false, onOpenChange,
30
+ * );
31
+ * ```
32
+ *
33
+ * @example Optional string value with no default
34
+ * ```ts
35
+ * const [value, setValue] = useControllableState<string>(
36
+ * controlledValue, undefined, onValueChange,
37
+ * );
38
+ * ```
39
+ *
40
+ * @param controlled - Consumer-supplied value, or `undefined` when the
41
+ * consumer wants the component to manage its own state.
42
+ * @param defaultValue - Initial value when uncontrolled. Read once on
43
+ * mount; subsequent changes to this argument are ignored.
44
+ * @param onChange - Optional notification fired whenever the setter is
45
+ * called, in both controlled and uncontrolled modes.
46
+ *
47
+ * @returns `[value, setValue, isControlled]`. `value` is `T` when a
48
+ * non-undefined `controlled` or `defaultValue` is supplied,
49
+ * otherwise `T | undefined`.
50
+ */
51
+ export function useControllableState<T>(
52
+ controlled: T | undefined,
53
+ defaultValue: T,
54
+ onChange?: (next: T) => void,
55
+ ): readonly [T, (next: T) => void, boolean];
56
+ export function useControllableState<T>(
57
+ controlled: T | undefined,
58
+ defaultValue: T | undefined,
59
+ onChange?: (next: T) => void,
60
+ ): readonly [T | undefined, (next: T) => void, boolean];
61
+ export function useControllableState<T>(
62
+ controlled: T | undefined,
63
+ defaultValue: T | undefined,
64
+ onChange?: (next: T) => void,
65
+ ): readonly [T | undefined, (next: T) => void, boolean] {
66
+ const isControlled = controlled !== undefined;
67
+ const [uncontrolled, setUncontrolled] = useState<T | undefined>(defaultValue);
68
+ const value = isControlled ? controlled : uncontrolled;
69
+
70
+ const setValue = useCallback(
71
+ (next: T) => {
72
+ if (!isControlled) {
73
+ setUncontrolled(next);
74
+ }
75
+ onChange?.(next);
76
+ },
77
+ [isControlled, onChange],
78
+ );
79
+
80
+ return [value, setValue, isControlled] as const;
81
+ }
@@ -0,0 +1,178 @@
1
+ import { useCallback, type KeyboardEvent } from "react";
2
+
3
+ import {
4
+ getKeyToActionMap,
5
+ type RovingKeyAction,
6
+ } from "../utils/getKeyToActionMap";
7
+
8
+ type UseRovingTabindexOptions<K> = {
9
+ /**
10
+ * Which arrow-key axis is active. `"horizontal"` enables Arrow
11
+ * Left/Right, `"vertical"` enables Arrow Up/Down, `"both"` enables all
12
+ * four. {@link UseRovingTabindexOptions.dir} swaps the horizontal pair
13
+ * under RTL for both `"horizontal"` and `"both"` orientation.
14
+ */
15
+ orientation: "horizontal" | "vertical" | "both";
16
+ /**
17
+ * Reading direction. When `"rtl"`, swaps the horizontal arrow pair —
18
+ * meaningful for `"horizontal"` and `"both"` orientation. Defaults to
19
+ * `"ltr"`.
20
+ */
21
+ dir?: "ltr" | "rtl";
22
+ /**
23
+ * The ordered list of keys the navigation should walk through. The
24
+ * caller is responsible for filtering: e.g. RadioGroup and Accordion
25
+ * pre-filter out disabled values (arrow skips them); Tabs passes the
26
+ * unfiltered list (arrow lands on disabled tabs without activating
27
+ * them, which is Tabs' specific keyboard contract).
28
+ */
29
+ navigable: K[];
30
+ /**
31
+ * The key of the trigger that owns the keydown event. Used as the
32
+ * pivot for "next"/"prev" navigation.
33
+ */
34
+ currentKey: K;
35
+ /**
36
+ * Callback invoked with the chosen target and the abstract action
37
+ * that produced it. The consumer decides what "go to" means — focus
38
+ * only (Accordion), focus + select (RadioGroup), focus + maybe
39
+ * activate (Tabs).
40
+ *
41
+ * Not called when no key matches and not called when `navigable` is
42
+ * empty.
43
+ */
44
+ onNavigate: (targetKey: K, action: RovingKeyAction) => void;
45
+ /**
46
+ * When `true`, Home maps to "first" and End to "last". Defaults to
47
+ * `false`.
48
+ */
49
+ includeHomeEnd?: boolean;
50
+ /**
51
+ * When `true`, Enter and Space map to "activate" and trigger
52
+ * `onNavigate(currentKey, "activate")`. Defaults to `false`.
53
+ */
54
+ includeActivate?: boolean;
55
+ };
56
+
57
+ /**
58
+ * The keyboard half of the WAI-ARIA roving-tabindex pattern: maps
59
+ * arrow / Home / End / Enter-Space keys to navigation between siblings
60
+ * in a compound widget, with optional RTL inversion and Home/End/
61
+ * Enter-Space toggles. The state half (which key is the current Tab
62
+ * stop, focus management, the actual selection / activation) lives in
63
+ * the consumer — the hook just decides "given this key press, which
64
+ * sibling should we navigate to?" and delegates via `onNavigate`.
65
+ *
66
+ * The orientation/dir/Home-End/activate keymap is built by
67
+ * {@link getKeyToActionMap}, which the hook composes internally.
68
+ *
69
+ * The action passed to `onNavigate` is the abstract `RovingKeyAction`
70
+ * vocabulary: `"next" | "prev" | "first" | "last" | "activate"`.
71
+ * Consumers that only care about movement can ignore the action;
72
+ * consumers that distinguish navigation-vs-activation (Tabs in manual
73
+ * activation mode) branch on it.
74
+ *
75
+ * @example RadioGroup — select on every arrow, both orientations
76
+ * ```ts
77
+ * const enabledValues = itemValues.filter((v) => !disabledValues.has(v));
78
+ * const { handleKeyDown } = useRovingTabindex({
79
+ * orientation: "both",
80
+ * navigable: enabledValues,
81
+ * currentKey: value,
82
+ * onNavigate: (target) => {
83
+ * select(target);
84
+ * focusItem(target);
85
+ * },
86
+ * });
87
+ * ```
88
+ *
89
+ * @example Tabs — land on disabled but only activate enabled, supports manual mode
90
+ * ```ts
91
+ * const { handleKeyDown } = useRovingTabindex({
92
+ * orientation, dir,
93
+ * navigable: triggerValues, // unfiltered: arrow lands on disabled
94
+ * currentKey: value,
95
+ * includeHomeEnd: true,
96
+ * includeActivate: true,
97
+ * onNavigate: (target, action) => {
98
+ * const isDisabled = disabledTriggerValues.has(target);
99
+ * const shouldActivate =
100
+ * !isDisabled &&
101
+ * (activationMode === "automatic" ||
102
+ * (activationMode === "manual" && action === "activate"));
103
+ * if (shouldActivate) activateTab(target);
104
+ * focusItem(target);
105
+ * },
106
+ * });
107
+ * ```
108
+ */
109
+ export function useRovingTabindex<K>({
110
+ orientation,
111
+ dir = "ltr",
112
+ navigable,
113
+ currentKey,
114
+ onNavigate,
115
+ includeHomeEnd = false,
116
+ includeActivate = false,
117
+ }: UseRovingTabindexOptions<K>): {
118
+ handleKeyDown: (e: KeyboardEvent<HTMLElement>) => void;
119
+ } {
120
+ const handleKeyDown = useCallback(
121
+ (e: KeyboardEvent<HTMLElement>) => {
122
+ const action = getKeyToActionMap({
123
+ orientation,
124
+ dir,
125
+ homeEnd: includeHomeEnd,
126
+ activate: includeActivate,
127
+ })[e.key];
128
+
129
+ if (!action) return;
130
+ if (navigable.length === 0) return;
131
+
132
+ if (action === "activate") {
133
+ e.preventDefault();
134
+ onNavigate(currentKey, action);
135
+ return;
136
+ }
137
+
138
+ const total = navigable.length;
139
+ const currentIndex = navigable.indexOf(currentKey);
140
+ let targetIndex: number;
141
+ switch (action) {
142
+ case "first":
143
+ targetIndex = 0;
144
+ break;
145
+ case "last":
146
+ targetIndex = total - 1;
147
+ break;
148
+ case "next":
149
+ case "prev":
150
+ // Bail when the current key isn't in `navigable` — typically
151
+ // because the consumer filtered out a disabled current item
152
+ // (RadioGroup's contract: arrow keys do nothing while focus is
153
+ // on a disabled radio). Home/End above still work regardless,
154
+ // since they don't depend on a current position.
155
+ if (currentIndex === -1) return;
156
+ targetIndex =
157
+ action === "next"
158
+ ? (currentIndex + 1) % total
159
+ : (currentIndex - 1 + total) % total;
160
+ break;
161
+ }
162
+
163
+ e.preventDefault();
164
+ onNavigate(navigable[targetIndex], action);
165
+ },
166
+ [
167
+ orientation,
168
+ dir,
169
+ navigable,
170
+ currentKey,
171
+ onNavigate,
172
+ includeHomeEnd,
173
+ includeActivate,
174
+ ],
175
+ );
176
+
177
+ return { handleKeyDown };
178
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ export * from "./AccessibleIcon";
2
+ export * from "./Accordion";
3
+ export * from "./Alert";
4
+ export * from "./Avatar";
5
+ export * from "./Breadcrumb";
6
+ export * from "./Button";
7
+ export * from "./Carousel";
8
+ export * from "./Checkbox";
9
+ export * from "./CheckboxCard";
10
+ export * from "./Collapsible";
11
+ export * from "./ContextMenu";
12
+ export * from "./DirectionProvider";
13
+ export * from "./Divider";
14
+ export * from "./Dropdown";
15
+ export * from "./EmptyState";
16
+ export * from "./Field";
17
+ export * from "./Fieldset";
18
+ export * from "./Input";
19
+ export * from "./InputGroup";
20
+ export * from "./MillerColumns";
21
+ export * from "./Modal";
22
+ export * from "./Portal";
23
+ export * from "./Progress";
24
+ export * from "./RadioCard";
25
+ export * from "./RadioGroup";
26
+ export * from "./Select";
27
+ export * from "./SkipNav";
28
+ export * from "./Slider";
29
+ export * from "./Status";
30
+ export * from "./Switch";
31
+ export * from "./Table";
32
+ export * from "./Tabs";
33
+ export * from "./Textarea";
34
+ export * from "./Toggle";
35
+ export * from "./ToggleGroup";
36
+ export * from "./Tooltip";
37
+ export * from "./Tree";
38
+ export * from "./VisuallyHidden";
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Minimal jsdom polyfill for IntersectionObserver.
3
+ *
4
+ * jsdom does not implement IntersectionObserver. The component relies
5
+ * on it as the user-driven scroll → state fallback for browsers
6
+ * without `scrollsnapchange`, and as the source of truth for
7
+ * `isInView(slideIndex)` on the imperative API. Tests can grab the
8
+ * most-recently-constructed instance via `MockIntersectionObserver
9
+ * .latest` and call `.fire(entries)` to simulate visibility changes
10
+ * on observed elements.
11
+ */
12
+
13
+ type MockEntry = {
14
+ target: Element;
15
+ isIntersecting: boolean;
16
+ intersectionRatio: number;
17
+ };
18
+
19
+ export class MockIntersectionObserver {
20
+ static instances: MockIntersectionObserver[] = [];
21
+ static get latest(): MockIntersectionObserver | undefined {
22
+ return MockIntersectionObserver.instances[
23
+ MockIntersectionObserver.instances.length - 1
24
+ ];
25
+ }
26
+ static reset() {
27
+ MockIntersectionObserver.instances = [];
28
+ }
29
+
30
+ callback: IntersectionObserverCallback;
31
+ options?: IntersectionObserverInit;
32
+ observed = new Set<Element>();
33
+ root: Element | Document | null = null;
34
+ rootMargin = "";
35
+ thresholds: ReadonlyArray<number> = [];
36
+
37
+ constructor(cb: IntersectionObserverCallback, options?: IntersectionObserverInit) {
38
+ this.callback = cb;
39
+ this.options = options;
40
+ MockIntersectionObserver.instances.push(this);
41
+ }
42
+
43
+ observe(target: Element) {
44
+ this.observed.add(target);
45
+ }
46
+ unobserve(target: Element) {
47
+ this.observed.delete(target);
48
+ }
49
+ disconnect() {
50
+ this.observed.clear();
51
+ }
52
+ takeRecords(): IntersectionObserverEntry[] {
53
+ return [];
54
+ }
55
+
56
+ /** Test-only: invoke the callback with synthesised entries for any
57
+ * targets that are currently being observed. */
58
+ fire(entries: MockEntry[]) {
59
+ const matching = entries
60
+ .filter((e) => this.observed.has(e.target))
61
+ .map(
62
+ (e) =>
63
+ ({
64
+ target: e.target,
65
+ isIntersecting: e.isIntersecting,
66
+ intersectionRatio: e.intersectionRatio,
67
+ boundingClientRect: e.target.getBoundingClientRect(),
68
+ intersectionRect: e.target.getBoundingClientRect(),
69
+ rootBounds: null,
70
+ time: 0,
71
+ }) as IntersectionObserverEntry,
72
+ );
73
+ if (matching.length > 0) {
74
+ this.callback(matching, this as unknown as IntersectionObserver);
75
+ }
76
+ }
77
+ }
78
+
79
+ export function installIntersectionObserverPolyfill() {
80
+ if (typeof window === "undefined") return;
81
+ (window as unknown as { IntersectionObserver: typeof MockIntersectionObserver })
82
+ .IntersectionObserver = MockIntersectionObserver;
83
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Minimal jsdom polyfill for the HTML Popover API.
3
+ *
4
+ * jsdom does not yet implement `showPopover()`, `hidePopover()`, or
5
+ * `togglePopover()`. This polyfill installs the three methods on the
6
+ * HTMLElement prototype so components under test can drive the popover
7
+ * API the same way they do in a real browser. Open state is mirrored to
8
+ * a `data-popover-open` attribute so jsdom assertions can observe it.
9
+ * `beforetoggle` / `toggle` events are dispatched to match the spec.
10
+ */
11
+
12
+ type PopoverState = "open" | "closed";
13
+
14
+ const stateMap = new WeakMap<HTMLElement, PopoverState>();
15
+
16
+ function getState(el: HTMLElement): PopoverState {
17
+ return stateMap.get(el) ?? "closed";
18
+ }
19
+
20
+ function setState(el: HTMLElement, next: PopoverState) {
21
+ stateMap.set(el, next);
22
+ if (next === "open") {
23
+ el.setAttribute("data-popover-open", "");
24
+ } else {
25
+ el.removeAttribute("data-popover-open");
26
+ }
27
+ }
28
+
29
+ function fireToggleEvents(
30
+ el: HTMLElement,
31
+ oldState: PopoverState,
32
+ newState: PopoverState,
33
+ ): boolean {
34
+ const before = new Event("beforetoggle", { cancelable: true, bubbles: false });
35
+ Object.defineProperty(before, "oldState", { value: oldState });
36
+ Object.defineProperty(before, "newState", { value: newState });
37
+ const allowed = el.dispatchEvent(before);
38
+ if (!allowed) return false;
39
+
40
+ const after = new Event("toggle", { cancelable: false, bubbles: false });
41
+ Object.defineProperty(after, "oldState", { value: oldState });
42
+ Object.defineProperty(after, "newState", { value: newState });
43
+ el.dispatchEvent(after);
44
+ return true;
45
+ }
46
+
47
+ export function installPopoverPolyfill() {
48
+ if (typeof HTMLElement === "undefined") return;
49
+ const proto = HTMLElement.prototype as unknown as {
50
+ showPopover?: () => void;
51
+ hidePopover?: () => void;
52
+ togglePopover?: (force?: boolean) => boolean;
53
+ };
54
+ if (typeof proto.showPopover === "function") return;
55
+
56
+ proto.showPopover = function showPopover(this: HTMLElement) {
57
+ if (getState(this) === "open") return;
58
+ if (!fireToggleEvents(this, "closed", "open")) return;
59
+ setState(this, "open");
60
+ };
61
+
62
+ proto.hidePopover = function hidePopover(this: HTMLElement) {
63
+ if (getState(this) === "closed") return;
64
+ if (!fireToggleEvents(this, "open", "closed")) return;
65
+ setState(this, "closed");
66
+ };
67
+
68
+ proto.togglePopover = function togglePopover(
69
+ this: HTMLElement,
70
+ force?: boolean,
71
+ ) {
72
+ const current = getState(this);
73
+ const target: PopoverState =
74
+ force === undefined
75
+ ? current === "open"
76
+ ? "closed"
77
+ : "open"
78
+ : force
79
+ ? "open"
80
+ : "closed";
81
+ if (target === current) return current === "open";
82
+ if (target === "open") this.showPopover!();
83
+ else this.hidePopover!();
84
+ return getState(this) === "open";
85
+ };
86
+ }