@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,276 @@
1
+ import { RefObject, useRef } from "react";
2
+ import { act, render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { Carousel } from "..";
6
+ import type { CarouselImperativeApi } from "..";
7
+ import { MockIntersectionObserver } from "../../test/intersectionObserverPolyfill";
8
+
9
+ describe("Carousel IntersectionObserver fallback + isInView", () => {
10
+ it("isInView(slideIndex) should reflect the observer's last reported intersection state", () => {
11
+ const ref = {
12
+ current: null,
13
+ } as unknown as RefObject<CarouselImperativeApi>;
14
+
15
+ render(
16
+ <Carousel.Root ref={ref} ariaLabel="Featured products">
17
+ <Carousel.Viewport>
18
+ <Carousel.Slide data-testid="slide-0" />
19
+ <Carousel.Slide data-testid="slide-1" />
20
+ <Carousel.Slide data-testid="slide-2" />
21
+ </Carousel.Viewport>
22
+ </Carousel.Root>,
23
+ );
24
+
25
+ expect(ref.current!.isInView(0)).toBe(false);
26
+ expect(ref.current!.isInView(1)).toBe(false);
27
+
28
+ const io = MockIntersectionObserver.latest!;
29
+ act(() => {
30
+ io.fire([
31
+ {
32
+ target: screen.getByTestId("slide-0"),
33
+ isIntersecting: true,
34
+ intersectionRatio: 1,
35
+ },
36
+ {
37
+ target: screen.getByTestId("slide-1"),
38
+ isIntersecting: false,
39
+ intersectionRatio: 0,
40
+ },
41
+ ]);
42
+ });
43
+
44
+ expect(ref.current!.isInView(0)).toBe(true);
45
+ expect(ref.current!.isInView(1)).toBe(false);
46
+ });
47
+
48
+ it("should drive currentPage from IO entries crossing the 0.6 threshold", () => {
49
+ function Parent() {
50
+ const ref = useRef<CarouselImperativeApi>(null);
51
+ return (
52
+ <Carousel.Root ref={ref} ariaLabel="Featured products">
53
+ <Carousel.Viewport data-testid="viewport">
54
+ <Carousel.Slide data-testid="slide-0" />
55
+ <Carousel.Slide data-testid="slide-1" />
56
+ <Carousel.Slide data-testid="slide-2" />
57
+ </Carousel.Viewport>
58
+ </Carousel.Root>
59
+ );
60
+ }
61
+
62
+ render(<Parent />);
63
+
64
+ // Settle the carousel's initial mount-time scroll so the IO callback
65
+ // is not gated by the in-flight programmatic scroll guard.
66
+ act(() => {
67
+ screen.getByTestId("viewport").dispatchEvent(new Event("scrollend"));
68
+ });
69
+
70
+ const io = MockIntersectionObserver.latest!;
71
+ act(() => {
72
+ io.fire([
73
+ {
74
+ target: screen.getByTestId("slide-0"),
75
+ isIntersecting: false,
76
+ intersectionRatio: 0.1,
77
+ },
78
+ {
79
+ target: screen.getByTestId("slide-2"),
80
+ isIntersecting: true,
81
+ intersectionRatio: 0.9,
82
+ },
83
+ ]);
84
+ });
85
+
86
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
87
+ "data-state",
88
+ "active",
89
+ );
90
+ });
91
+
92
+ it("should not change the page when no observed slides cross the visibility threshold", () => {
93
+ const onPageChange = vi.fn();
94
+ render(
95
+ <Carousel.Root
96
+ ariaLabel="Featured products"
97
+ page={0}
98
+ onPageChange={onPageChange}
99
+ >
100
+ <Carousel.Viewport>
101
+ <Carousel.Slide data-testid="slide-0" />
102
+ <Carousel.Slide data-testid="slide-1" />
103
+ </Carousel.Viewport>
104
+ </Carousel.Root>,
105
+ );
106
+
107
+ const io = MockIntersectionObserver.latest!;
108
+ act(() => {
109
+ io.fire([
110
+ {
111
+ target: screen.getByTestId("slide-0"),
112
+ isIntersecting: false,
113
+ intersectionRatio: 0.2,
114
+ },
115
+ {
116
+ target: screen.getByTestId("slide-1"),
117
+ isIntersecting: false,
118
+ intersectionRatio: 0.1,
119
+ },
120
+ ]);
121
+ });
122
+
123
+ expect(onPageChange).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it("should derive the page index from floor(slideIndex / slidesPerPage) when slidesPerPage > 1", () => {
127
+ render(
128
+ <Carousel.Root ariaLabel="Featured products" slidesPerPage={2}>
129
+ <Carousel.Viewport data-testid="viewport">
130
+ <Carousel.Slide data-testid="slide-0" />
131
+ <Carousel.Slide data-testid="slide-1" />
132
+ <Carousel.Slide data-testid="slide-2" />
133
+ <Carousel.Slide data-testid="slide-3" />
134
+ </Carousel.Viewport>
135
+ </Carousel.Root>,
136
+ );
137
+
138
+ // Settle the carousel's initial mount-time scroll so the IO callback
139
+ // is not gated by the in-flight programmatic scroll guard.
140
+ act(() => {
141
+ screen.getByTestId("viewport").dispatchEvent(new Event("scrollend"));
142
+ });
143
+
144
+ const io = MockIntersectionObserver.latest!;
145
+ act(() => {
146
+ io.fire([
147
+ {
148
+ target: screen.getByTestId("slide-3"),
149
+ isIntersecting: true,
150
+ intersectionRatio: 0.8,
151
+ },
152
+ ]);
153
+ });
154
+
155
+ // floor(3 / 2) = page 1; slides 2 and 3 active.
156
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
157
+ "data-state",
158
+ "active",
159
+ );
160
+ expect(screen.getByTestId("slide-3")).toHaveAttribute(
161
+ "data-state",
162
+ "active",
163
+ );
164
+ });
165
+
166
+ it("should not drive the page from the IntersectionObserver when the browser supports scrollsnapchange", () => {
167
+ // scrollsnapchange reports the precise centred snap target, so it owns
168
+ // the active page. The IO's lowest-index-visible heuristic is only a
169
+ // fallback for browsers without the event — and for a centre-aligned
170
+ // carousel that shows several slides at once it would override the
171
+ // correct page with the *leftmost* visible slide. When the event is
172
+ // available the observer must feed isInView only, not the page.
173
+ const ref = {
174
+ current: null,
175
+ } as unknown as RefObject<CarouselImperativeApi>;
176
+ // The presence of the on* handler property is the feature-detection
177
+ // signal; assigning it (even null) makes `"onscrollsnapchange" in window`
178
+ // true the way a supporting browser does.
179
+ (
180
+ window as unknown as { onscrollsnapchange: unknown }
181
+ ).onscrollsnapchange = null;
182
+ try {
183
+ render(
184
+ <Carousel.Root
185
+ ref={ref}
186
+ ariaLabel="Featured products"
187
+ snapAlign="center"
188
+ >
189
+ <Carousel.Viewport data-testid="viewport">
190
+ <Carousel.Slide data-testid="slide-0" />
191
+ <Carousel.Slide data-testid="slide-1" />
192
+ <Carousel.Slide data-testid="slide-2" />
193
+ </Carousel.Viewport>
194
+ </Carousel.Root>,
195
+ );
196
+
197
+ act(() => {
198
+ screen.getByTestId("viewport").dispatchEvent(new Event("scrollend"));
199
+ });
200
+
201
+ const io = MockIntersectionObserver.latest!;
202
+ act(() => {
203
+ io.fire([
204
+ {
205
+ target: screen.getByTestId("slide-2"),
206
+ isIntersecting: true,
207
+ intersectionRatio: 0.9,
208
+ },
209
+ ]);
210
+ });
211
+
212
+ // The page stays put — scrollsnapchange owns it…
213
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
214
+ "data-state",
215
+ "active",
216
+ );
217
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
218
+ "data-state",
219
+ "inactive",
220
+ );
221
+ // …but isInView still reflects what the observer reported.
222
+ expect(ref.current!.isInView(2)).toBe(true);
223
+ } finally {
224
+ delete (window as unknown as { onscrollsnapchange?: unknown })
225
+ .onscrollsnapchange;
226
+ }
227
+ });
228
+
229
+ it("should not let an IO callback reset the page while a programmatic scroll is in flight", async () => {
230
+ // Regression test: clicking NextTrigger calls next() which sets
231
+ // internalPage to 1 and triggers a scrollIntoView. If the IO callback
232
+ // fires before the scroll settles (slide-0 still ≥0.6 visible), the
233
+ // callback must not call goTo(0) and undo the navigation.
234
+ const user = userEvent.setup();
235
+ render(
236
+ <Carousel.Root ariaLabel="Featured products">
237
+ <Carousel.Viewport>
238
+ <Carousel.Slide data-testid="slide-0" />
239
+ <Carousel.Slide data-testid="slide-1" />
240
+ <Carousel.Slide data-testid="slide-2" />
241
+ </Carousel.Viewport>
242
+ <Carousel.NextTrigger>Next</Carousel.NextTrigger>
243
+ </Carousel.Root>,
244
+ );
245
+
246
+ await user.click(screen.getByRole("button", { name: "Next" }));
247
+
248
+ // Simulate the IO callback firing mid-scroll: slide-0 still highly
249
+ // visible because the smooth-scroll animation hasn't finished yet.
250
+ const io = MockIntersectionObserver.latest!;
251
+ act(() => {
252
+ io.fire([
253
+ {
254
+ target: screen.getByTestId("slide-0"),
255
+ isIntersecting: true,
256
+ intersectionRatio: 0.9,
257
+ },
258
+ {
259
+ target: screen.getByTestId("slide-1"),
260
+ isIntersecting: false,
261
+ intersectionRatio: 0.1,
262
+ },
263
+ ]);
264
+ });
265
+
266
+ // The page must remain at 1, not be reset to 0.
267
+ expect(screen.getByTestId("slide-1")).toHaveAttribute(
268
+ "data-state",
269
+ "active",
270
+ );
271
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
272
+ "data-state",
273
+ "inactive",
274
+ );
275
+ });
276
+ });
@@ -0,0 +1,158 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Carousel } from "..";
5
+
6
+ describe("Carousel keyboard navigation", () => {
7
+ it("should advance the active page when ArrowRight is pressed with the viewport focused", async () => {
8
+ const user = userEvent.setup();
9
+ render(
10
+ <Carousel.Root ariaLabel="Featured products">
11
+ <Carousel.Viewport data-testid="viewport">
12
+ <Carousel.Slide data-testid="slide-0" />
13
+ <Carousel.Slide data-testid="slide-1" />
14
+ <Carousel.Slide data-testid="slide-2" />
15
+ </Carousel.Viewport>
16
+ </Carousel.Root>,
17
+ );
18
+
19
+ // Tab into the carousel — the Viewport must be in the tab order so
20
+ // keyboard users can reach the rotation control without first
21
+ // tabbing through every slide's interactive content.
22
+ await user.tab();
23
+ expect(screen.getByTestId("viewport")).toHaveFocus();
24
+
25
+ await user.keyboard("{ArrowRight}");
26
+
27
+ expect(screen.getByTestId("slide-1")).toHaveAttribute(
28
+ "data-state",
29
+ "active",
30
+ );
31
+ });
32
+
33
+ it("should retreat the active page when ArrowLeft is pressed with the viewport focused", async () => {
34
+ const user = userEvent.setup();
35
+ render(
36
+ <Carousel.Root ariaLabel="Featured products" defaultPage={2}>
37
+ <Carousel.Viewport data-testid="viewport">
38
+ <Carousel.Slide data-testid="slide-0" />
39
+ <Carousel.Slide data-testid="slide-1" />
40
+ <Carousel.Slide data-testid="slide-2" />
41
+ </Carousel.Viewport>
42
+ </Carousel.Root>,
43
+ );
44
+
45
+ await user.tab();
46
+ await user.keyboard("{ArrowLeft}");
47
+
48
+ expect(screen.getByTestId("slide-1")).toHaveAttribute(
49
+ "data-state",
50
+ "active",
51
+ );
52
+ });
53
+
54
+ it("should jump to the first page when Home is pressed with the viewport focused", async () => {
55
+ const user = userEvent.setup();
56
+ render(
57
+ <Carousel.Root ariaLabel="Featured products" defaultPage={2}>
58
+ <Carousel.Viewport data-testid="viewport">
59
+ <Carousel.Slide data-testid="slide-0" />
60
+ <Carousel.Slide data-testid="slide-1" />
61
+ <Carousel.Slide data-testid="slide-2" />
62
+ </Carousel.Viewport>
63
+ </Carousel.Root>,
64
+ );
65
+
66
+ await user.tab();
67
+ await user.keyboard("{Home}");
68
+
69
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
70
+ "data-state",
71
+ "active",
72
+ );
73
+ });
74
+
75
+ it("should jump to the last page when End is pressed with the viewport focused", async () => {
76
+ const user = userEvent.setup();
77
+ render(
78
+ <Carousel.Root ariaLabel="Featured products">
79
+ <Carousel.Viewport data-testid="viewport">
80
+ <Carousel.Slide data-testid="slide-0" />
81
+ <Carousel.Slide data-testid="slide-1" />
82
+ <Carousel.Slide data-testid="slide-2" />
83
+ </Carousel.Viewport>
84
+ </Carousel.Root>,
85
+ );
86
+
87
+ await user.tab();
88
+ await user.keyboard("{End}");
89
+
90
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
91
+ "data-state",
92
+ "active",
93
+ );
94
+ });
95
+
96
+ it("should clamp at the boundaries when arrow keys are pressed", async () => {
97
+ function Fixture({ start }: { start: number }) {
98
+ return (
99
+ <Carousel.Root ariaLabel="Featured products" defaultPage={start}>
100
+ <Carousel.Viewport data-testid="viewport">
101
+ <Carousel.Slide data-testid="slide-0" />
102
+ <Carousel.Slide data-testid="slide-1" />
103
+ <Carousel.Slide data-testid="slide-2" />
104
+ </Carousel.Viewport>
105
+ </Carousel.Root>
106
+ );
107
+ }
108
+
109
+ const user = userEvent.setup();
110
+
111
+ const { unmount } = render(<Fixture start={2} />);
112
+ await user.tab();
113
+ await user.keyboard("{ArrowRight}");
114
+ expect(screen.getByTestId("slide-2")).toHaveAttribute(
115
+ "data-state",
116
+ "active",
117
+ );
118
+ unmount();
119
+
120
+ render(<Fixture start={0} />);
121
+ await user.tab();
122
+ await user.keyboard("{ArrowLeft}");
123
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
124
+ "data-state",
125
+ "active",
126
+ );
127
+ });
128
+
129
+ it("should leave focus inside a slide alone — arrow keys don't navigate when a child element is the focus target", async () => {
130
+ const onPageChange = vi.fn();
131
+ const user = userEvent.setup();
132
+ render(
133
+ <Carousel.Root
134
+ ariaLabel="Featured products"
135
+ page={0}
136
+ onPageChange={onPageChange}
137
+ >
138
+ <Carousel.Viewport data-testid="viewport">
139
+ <Carousel.Slide>
140
+ <button data-testid="slide-link">Open</button>
141
+ </Carousel.Slide>
142
+ <Carousel.Slide data-testid="slide-1" />
143
+ </Carousel.Viewport>
144
+ </Carousel.Root>,
145
+ );
146
+
147
+ // Tab past the Viewport onto the link inside slide-0.
148
+ await user.tab();
149
+ await user.tab();
150
+ expect(screen.getByTestId("slide-link")).toHaveFocus();
151
+
152
+ // ArrowRight while a slide-internal control has focus must keep its
153
+ // native semantics rather than steal the keypress for navigation.
154
+ await user.keyboard("{ArrowRight}");
155
+
156
+ expect(onPageChange).not.toHaveBeenCalled();
157
+ });
158
+ });
@@ -0,0 +1,232 @@
1
+ import { useState } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { Carousel } from "..";
6
+
7
+ describe("Carousel.PlayPauseTrigger basic shape", () => {
8
+ it("should render the PlayPauseTrigger as <button type='button'>", () => {
9
+ render(
10
+ <Carousel.Root ariaLabel="Featured products" autoplay>
11
+ <Carousel.PlayPauseTrigger />
12
+ </Carousel.Root>,
13
+ );
14
+
15
+ const trigger = screen.getByRole("button");
16
+ expect(trigger).toBeVisible();
17
+ expect(trigger).toHaveAttribute("type", "button");
18
+ });
19
+
20
+ it("should accept a className prop", () => {
21
+ const testClass = "custom-play-pause";
22
+ render(
23
+ <Carousel.Root ariaLabel="Featured products" autoplay>
24
+ <Carousel.PlayPauseTrigger className={testClass} />
25
+ </Carousel.Root>,
26
+ );
27
+
28
+ expect(screen.getByRole("button")).toHaveAttribute("class", testClass);
29
+ });
30
+
31
+ it("should apply a className of empty string by default when not specified", () => {
32
+ render(
33
+ <Carousel.Root ariaLabel="Featured products" autoplay>
34
+ <Carousel.PlayPauseTrigger />
35
+ </Carousel.Root>,
36
+ );
37
+
38
+ expect(screen.getByRole("button")).toHaveAttribute("class", "");
39
+ });
40
+
41
+ it("should throw an error when used outside Carousel.Root", () => {
42
+ expect(() => {
43
+ render(<Carousel.PlayPauseTrigger />);
44
+ }).toThrow("Component must be rendered as a child of Carousel.Root");
45
+ });
46
+ });
47
+
48
+ describe("Carousel.PlayPauseTrigger uncontrolled playing state", () => {
49
+ it("should default to playing=false (paused) and announce 'Start automatic slide show'", () => {
50
+ render(
51
+ <Carousel.Root ariaLabel="Featured products" autoplay>
52
+ <Carousel.PlayPauseTrigger />
53
+ </Carousel.Root>,
54
+ );
55
+
56
+ expect(
57
+ screen.getByRole("button", { name: "Start automatic slide show" }),
58
+ ).toBeVisible();
59
+ });
60
+
61
+ it("should accept defaultPlaying={true} to seed the initial playing state", () => {
62
+ render(
63
+ <Carousel.Root ariaLabel="Featured products" autoplay defaultPlaying>
64
+ <Carousel.PlayPauseTrigger />
65
+ </Carousel.Root>,
66
+ );
67
+
68
+ expect(
69
+ screen.getByRole("button", { name: "Stop automatic slide show" }),
70
+ ).toBeVisible();
71
+ });
72
+
73
+ it("should toggle playing when the trigger is clicked", async () => {
74
+ const user = userEvent.setup();
75
+ render(
76
+ <Carousel.Root ariaLabel="Featured products" autoplay>
77
+ <Carousel.PlayPauseTrigger />
78
+ </Carousel.Root>,
79
+ );
80
+
81
+ await user.click(
82
+ screen.getByRole("button", { name: "Start automatic slide show" }),
83
+ );
84
+ expect(
85
+ screen.getByRole("button", { name: "Stop automatic slide show" }),
86
+ ).toBeVisible();
87
+
88
+ await user.click(
89
+ screen.getByRole("button", { name: "Stop automatic slide show" }),
90
+ );
91
+ expect(
92
+ screen.getByRole("button", { name: "Start automatic slide show" }),
93
+ ).toBeVisible();
94
+ });
95
+ });
96
+
97
+ describe("Carousel.PlayPauseTrigger controlled playing state", () => {
98
+ it("should reflect the playing prop, not internal state", () => {
99
+ render(
100
+ <Carousel.Root
101
+ ariaLabel="Featured products"
102
+ autoplay
103
+ playing
104
+ onPlayingChange={() => {}}
105
+ >
106
+ <Carousel.PlayPauseTrigger />
107
+ </Carousel.Root>,
108
+ );
109
+
110
+ expect(
111
+ screen.getByRole("button", { name: "Stop automatic slide show" }),
112
+ ).toBeVisible();
113
+ });
114
+
115
+ it("should call onPlayingChange when clicked, without updating internal state", async () => {
116
+ const user = userEvent.setup();
117
+ const onPlayingChange = vi.fn();
118
+ render(
119
+ <Carousel.Root
120
+ ariaLabel="Featured products"
121
+ autoplay
122
+ playing={false}
123
+ onPlayingChange={onPlayingChange}
124
+ >
125
+ <Carousel.PlayPauseTrigger />
126
+ </Carousel.Root>,
127
+ );
128
+
129
+ await user.click(screen.getByRole("button"));
130
+
131
+ expect(onPlayingChange).toHaveBeenCalledTimes(1);
132
+ expect(onPlayingChange).toHaveBeenCalledWith(true);
133
+ // Parent has not re-rendered with a new playing prop, so the
134
+ // visible state must remain at the controlled value.
135
+ expect(
136
+ screen.getByRole("button", { name: "Start automatic slide show" }),
137
+ ).toBeVisible();
138
+ });
139
+
140
+ it("should reflect external playing changes that don't go through the trigger", async () => {
141
+ const user = userEvent.setup();
142
+ function Parent() {
143
+ const [playing, setPlaying] = useState(false);
144
+ return (
145
+ <>
146
+ <button type="button" onClick={() => setPlaying(true)}>
147
+ External play
148
+ </button>
149
+ <Carousel.Root
150
+ ariaLabel="Featured products"
151
+ autoplay
152
+ playing={playing}
153
+ onPlayingChange={setPlaying}
154
+ >
155
+ <Carousel.PlayPauseTrigger />
156
+ </Carousel.Root>
157
+ </>
158
+ );
159
+ }
160
+
161
+ render(<Parent />);
162
+
163
+ await user.click(screen.getByRole("button", { name: "External play" }));
164
+
165
+ expect(
166
+ screen.getByRole("button", { name: "Stop automatic slide show" }),
167
+ ).toBeVisible();
168
+ });
169
+ });
170
+
171
+ describe("Carousel.PlayPauseTrigger render-prop children", () => {
172
+ it("should pass { playing } to a function child and render its return value", () => {
173
+ render(
174
+ <Carousel.Root ariaLabel="Featured products" autoplay>
175
+ <Carousel.PlayPauseTrigger>
176
+ {({ playing }) => (playing ? "Pause slideshow" : "Start slideshow")}
177
+ </Carousel.PlayPauseTrigger>
178
+ </Carousel.Root>,
179
+ );
180
+
181
+ expect(screen.getByRole("button")).toHaveTextContent("Start slideshow");
182
+ });
183
+
184
+ it("should re-render the function child when playing flips", async () => {
185
+ const user = userEvent.setup();
186
+ render(
187
+ <Carousel.Root ariaLabel="Featured products" autoplay>
188
+ <Carousel.PlayPauseTrigger>
189
+ {({ playing }) => (playing ? "Pause slideshow" : "Start slideshow")}
190
+ </Carousel.PlayPauseTrigger>
191
+ </Carousel.Root>,
192
+ );
193
+
194
+ await user.click(screen.getByRole("button"));
195
+
196
+ expect(screen.getByRole("button")).toHaveTextContent("Pause slideshow");
197
+ });
198
+
199
+ it("should render static children unchanged when no function is provided", () => {
200
+ render(
201
+ <Carousel.Root ariaLabel="Featured products" autoplay>
202
+ <Carousel.PlayPauseTrigger>
203
+ <span data-testid="play-pause-icon" />
204
+ </Carousel.PlayPauseTrigger>
205
+ </Carousel.Root>,
206
+ );
207
+
208
+ expect(screen.getByTestId("play-pause-icon")).toBeVisible();
209
+ });
210
+ });
211
+
212
+ describe("Carousel.PlayPauseTrigger styling hooks", () => {
213
+ it('should emit data-state="paused" when not playing', () => {
214
+ render(
215
+ <Carousel.Root ariaLabel="Featured products" autoplay>
216
+ <Carousel.PlayPauseTrigger />
217
+ </Carousel.Root>,
218
+ );
219
+
220
+ expect(screen.getByRole("button")).toHaveAttribute("data-state", "paused");
221
+ });
222
+
223
+ it('should emit data-state="playing" when playing', () => {
224
+ render(
225
+ <Carousel.Root ariaLabel="Featured products" autoplay defaultPlaying>
226
+ <Carousel.PlayPauseTrigger />
227
+ </Carousel.Root>,
228
+ );
229
+
230
+ expect(screen.getByRole("button")).toHaveAttribute("data-state", "playing");
231
+ });
232
+ });