@primitiv-ui/react 0.1.28 → 0.1.30

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 (456) hide show
  1. package/README.md +19 -2
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -0,0 +1,239 @@
1
+ import { act, fireEvent, render, screen } from "@testing-library/react";
2
+
3
+ import { Carousel } from "../index.ts";
4
+
5
+ // Autoplay status: an onAutoplayStatusChange callback mirroring Ark UI's
6
+ // shape, firing on every tick (not just play/pause toggles) so a consumer
7
+ // can react to autoplay in JS — e.g. logging analytics on each rotation.
8
+ describe("Carousel autoplay status", () => {
9
+ beforeEach(() => {
10
+ vi.useFakeTimers();
11
+ });
12
+
13
+ afterEach(() => {
14
+ vi.useRealTimers();
15
+ });
16
+
17
+ it("should call onAutoplayStatusChange with type 'autoplay.start' as soon as the timer becomes active", () => {
18
+ const onAutoplayStatusChange = vi.fn();
19
+ render(
20
+ <Carousel.Root
21
+ ariaLabel="Featured products"
22
+ autoplay
23
+ defaultPlaying
24
+ onAutoplayStatusChange={onAutoplayStatusChange}
25
+ >
26
+ <Carousel.Viewport>
27
+ <Carousel.Slide />
28
+ <Carousel.Slide />
29
+ </Carousel.Viewport>
30
+ </Carousel.Root>,
31
+ );
32
+
33
+ expect(onAutoplayStatusChange).toHaveBeenCalledWith({
34
+ type: "autoplay.start",
35
+ page: 0,
36
+ isPlaying: true,
37
+ });
38
+ });
39
+
40
+ it("should call onAutoplayStatusChange with type 'autoplay' when a scheduled tick fires", () => {
41
+ const onAutoplayStatusChange = vi.fn();
42
+ render(
43
+ <Carousel.Root
44
+ ariaLabel="Featured products"
45
+ autoplay
46
+ defaultPlaying
47
+ onAutoplayStatusChange={onAutoplayStatusChange}
48
+ >
49
+ <Carousel.Viewport>
50
+ <Carousel.Slide />
51
+ <Carousel.Slide />
52
+ <Carousel.Slide />
53
+ </Carousel.Viewport>
54
+ </Carousel.Root>,
55
+ );
56
+ onAutoplayStatusChange.mockClear();
57
+
58
+ act(() => {
59
+ vi.advanceTimersByTime(4000);
60
+ });
61
+
62
+ expect(onAutoplayStatusChange).toHaveBeenCalledWith({
63
+ type: "autoplay",
64
+ page: 0,
65
+ isPlaying: true,
66
+ });
67
+ });
68
+
69
+ it("should not re-emit 'autoplay.start' on ticks after the timer is already running", () => {
70
+ const onAutoplayStatusChange = vi.fn();
71
+ render(
72
+ <Carousel.Root
73
+ ariaLabel="Featured products"
74
+ autoplay
75
+ defaultPlaying
76
+ onAutoplayStatusChange={onAutoplayStatusChange}
77
+ >
78
+ <Carousel.Viewport>
79
+ <Carousel.Slide />
80
+ <Carousel.Slide />
81
+ <Carousel.Slide />
82
+ </Carousel.Viewport>
83
+ </Carousel.Root>,
84
+ );
85
+ onAutoplayStatusChange.mockClear();
86
+
87
+ act(() => {
88
+ vi.advanceTimersByTime(8000);
89
+ });
90
+
91
+ expect(onAutoplayStatusChange).not.toHaveBeenCalledWith(
92
+ expect.objectContaining({ type: "autoplay.start" }),
93
+ );
94
+ });
95
+
96
+ it("should call onAutoplayStatusChange with type 'autoplay.stop' when playing flips to false", () => {
97
+ const onAutoplayStatusChange = vi.fn();
98
+ render(
99
+ <Carousel.Root
100
+ ariaLabel="Featured products"
101
+ autoplay
102
+ defaultPlaying
103
+ onAutoplayStatusChange={onAutoplayStatusChange}
104
+ >
105
+ <Carousel.Viewport>
106
+ <Carousel.Slide />
107
+ <Carousel.Slide />
108
+ </Carousel.Viewport>
109
+ <Carousel.PlayPauseTrigger />
110
+ </Carousel.Root>,
111
+ );
112
+ onAutoplayStatusChange.mockClear();
113
+
114
+ act(() => {
115
+ fireEvent.click(screen.getByRole("button"));
116
+ });
117
+
118
+ expect(onAutoplayStatusChange).toHaveBeenCalledWith({
119
+ type: "autoplay.stop",
120
+ page: 0,
121
+ isPlaying: false,
122
+ });
123
+ });
124
+
125
+ it("should call onAutoplayStatusChange with type 'autoplay.stop' once the last page is reached", () => {
126
+ const onAutoplayStatusChange = vi.fn();
127
+ render(
128
+ <Carousel.Root
129
+ ariaLabel="Featured products"
130
+ autoplay
131
+ defaultPlaying
132
+ onAutoplayStatusChange={onAutoplayStatusChange}
133
+ >
134
+ <Carousel.Viewport>
135
+ <Carousel.Slide />
136
+ <Carousel.Slide />
137
+ </Carousel.Viewport>
138
+ </Carousel.Root>,
139
+ );
140
+ onAutoplayStatusChange.mockClear();
141
+
142
+ act(() => {
143
+ vi.advanceTimersByTime(4000);
144
+ });
145
+
146
+ expect(onAutoplayStatusChange).toHaveBeenCalledWith({
147
+ type: "autoplay.stop",
148
+ page: 1,
149
+ isPlaying: false,
150
+ });
151
+ });
152
+
153
+ it("should call onAutoplayStatusChange with type 'autoplay.stop' then 'autoplay.start' across a hover pause/resume", () => {
154
+ const onAutoplayStatusChange = vi.fn();
155
+ render(
156
+ <Carousel.Root
157
+ ariaLabel="Featured products"
158
+ autoplay
159
+ defaultPlaying
160
+ data-testid="carousel-root"
161
+ onAutoplayStatusChange={onAutoplayStatusChange}
162
+ >
163
+ <Carousel.Viewport>
164
+ <Carousel.Slide />
165
+ <Carousel.Slide />
166
+ <Carousel.Slide />
167
+ </Carousel.Viewport>
168
+ </Carousel.Root>,
169
+ );
170
+ const root = screen.getByTestId("carousel-root");
171
+ onAutoplayStatusChange.mockClear();
172
+
173
+ act(() => {
174
+ fireEvent.mouseEnter(root);
175
+ });
176
+ expect(onAutoplayStatusChange).toHaveBeenCalledWith({
177
+ type: "autoplay.stop",
178
+ page: 0,
179
+ isPlaying: false,
180
+ });
181
+
182
+ onAutoplayStatusChange.mockClear();
183
+ act(() => {
184
+ fireEvent.mouseLeave(root);
185
+ });
186
+ expect(onAutoplayStatusChange).toHaveBeenCalledWith({
187
+ type: "autoplay.start",
188
+ page: 0,
189
+ isPlaying: true,
190
+ });
191
+ });
192
+
193
+ it("should never call onAutoplayStatusChange when autoplay is disabled", () => {
194
+ const onAutoplayStatusChange = vi.fn();
195
+ render(
196
+ <Carousel.Root
197
+ ariaLabel="Featured products"
198
+ defaultPlaying
199
+ onAutoplayStatusChange={onAutoplayStatusChange}
200
+ >
201
+ <Carousel.Viewport>
202
+ <Carousel.Slide />
203
+ <Carousel.Slide />
204
+ </Carousel.Viewport>
205
+ </Carousel.Root>,
206
+ );
207
+
208
+ act(() => {
209
+ vi.advanceTimersByTime(60_000);
210
+ });
211
+
212
+ expect(onAutoplayStatusChange).not.toHaveBeenCalled();
213
+ });
214
+
215
+ it("should report the live active page in the callback payload", () => {
216
+ const onAutoplayStatusChange = vi.fn();
217
+ render(
218
+ <Carousel.Root
219
+ ariaLabel="Featured products"
220
+ autoplay
221
+ defaultPlaying
222
+ defaultPage={1}
223
+ onAutoplayStatusChange={onAutoplayStatusChange}
224
+ >
225
+ <Carousel.Viewport>
226
+ <Carousel.Slide />
227
+ <Carousel.Slide />
228
+ <Carousel.Slide />
229
+ </Carousel.Viewport>
230
+ </Carousel.Root>,
231
+ );
232
+
233
+ expect(onAutoplayStatusChange).toHaveBeenCalledWith({
234
+ type: "autoplay.start",
235
+ page: 1,
236
+ isPlaying: true,
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,292 @@
1
+ import { createRef } from "react";
2
+ import { act, fireEvent, render, screen } from "@testing-library/react";
3
+
4
+ import { Carousel } from "../index.ts";
5
+ import type { CarouselImperativeApi } from "../index.ts";
6
+
7
+ // Drag status: imperative isDragging() + an onDragStatusChange callback,
8
+ // mirroring Ark UI's api.isDragging / onDragStatusChange shape. The
9
+ // existing data-dragging DOM hook already tracks this for CSS, but a
10
+ // consumer couldn't react to drag start/end in JS (e.g. to pause a video
11
+ // in the active slide while dragging) until now.
12
+ describe("Carousel drag status", () => {
13
+ function drag(viewport: HTMLElement) {
14
+ fireEvent.pointerDown(viewport, {
15
+ pointerType: "mouse",
16
+ pointerId: 1,
17
+ clientX: 200,
18
+ clientY: 0,
19
+ });
20
+ fireEvent.pointerMove(viewport, {
21
+ pointerType: "mouse",
22
+ pointerId: 1,
23
+ clientX: 240,
24
+ clientY: 0,
25
+ });
26
+ }
27
+
28
+ it("should report isDragging() as false before any drag", () => {
29
+ const ref = createRef<CarouselImperativeApi>();
30
+ render(
31
+ <Carousel.Root ref={ref} ariaLabel="Featured products" allowMouseDrag>
32
+ <Carousel.Viewport data-testid="viewport">
33
+ <Carousel.Slide />
34
+ <Carousel.Slide />
35
+ </Carousel.Viewport>
36
+ </Carousel.Root>,
37
+ );
38
+
39
+ expect(ref.current!.isDragging()).toBe(false);
40
+ });
41
+
42
+ it("should report isDragging() as true once a drag crosses the threshold", () => {
43
+ const ref = createRef<CarouselImperativeApi>();
44
+ render(
45
+ <Carousel.Root ref={ref} ariaLabel="Featured products" allowMouseDrag>
46
+ <Carousel.Viewport data-testid="viewport">
47
+ <Carousel.Slide />
48
+ <Carousel.Slide />
49
+ </Carousel.Viewport>
50
+ </Carousel.Root>,
51
+ );
52
+
53
+ act(() => {
54
+ drag(screen.getByTestId("viewport"));
55
+ });
56
+
57
+ expect(ref.current!.isDragging()).toBe(true);
58
+ });
59
+
60
+ it("should report isDragging() as false again after pointerup", () => {
61
+ const ref = createRef<CarouselImperativeApi>();
62
+ render(
63
+ <Carousel.Root ref={ref} ariaLabel="Featured products" allowMouseDrag>
64
+ <Carousel.Viewport data-testid="viewport">
65
+ <Carousel.Slide />
66
+ <Carousel.Slide />
67
+ </Carousel.Viewport>
68
+ </Carousel.Root>,
69
+ );
70
+ const viewport = screen.getByTestId("viewport");
71
+
72
+ act(() => {
73
+ drag(viewport);
74
+ fireEvent.pointerUp(viewport, { pointerType: "mouse", pointerId: 1 });
75
+ });
76
+
77
+ expect(ref.current!.isDragging()).toBe(false);
78
+ });
79
+
80
+ it("should not flip isDragging() for a movement under the click-vs-drag threshold", () => {
81
+ const ref = createRef<CarouselImperativeApi>();
82
+ render(
83
+ <Carousel.Root ref={ref} ariaLabel="Featured products" allowMouseDrag>
84
+ <Carousel.Viewport data-testid="viewport">
85
+ <Carousel.Slide />
86
+ <Carousel.Slide />
87
+ </Carousel.Viewport>
88
+ </Carousel.Root>,
89
+ );
90
+ const viewport = screen.getByTestId("viewport");
91
+
92
+ act(() => {
93
+ fireEvent.pointerDown(viewport, {
94
+ pointerType: "mouse",
95
+ pointerId: 1,
96
+ clientX: 200,
97
+ clientY: 0,
98
+ });
99
+ fireEvent.pointerMove(viewport, {
100
+ pointerType: "mouse",
101
+ pointerId: 1,
102
+ clientX: 201,
103
+ clientY: 0,
104
+ });
105
+ });
106
+
107
+ expect(ref.current!.isDragging()).toBe(false);
108
+ });
109
+
110
+ it("should call onDragStatusChange with type 'dragging.start' when a drag crosses the threshold", () => {
111
+ const onDragStatusChange = vi.fn();
112
+ render(
113
+ <Carousel.Root
114
+ ariaLabel="Featured products"
115
+ allowMouseDrag
116
+ onDragStatusChange={onDragStatusChange}
117
+ >
118
+ <Carousel.Viewport data-testid="viewport">
119
+ <Carousel.Slide />
120
+ <Carousel.Slide />
121
+ </Carousel.Viewport>
122
+ </Carousel.Root>,
123
+ );
124
+
125
+ act(() => {
126
+ drag(screen.getByTestId("viewport"));
127
+ });
128
+
129
+ expect(onDragStatusChange).toHaveBeenCalledWith({
130
+ type: "dragging.start",
131
+ page: 0,
132
+ isDragging: true,
133
+ });
134
+ });
135
+
136
+ it("should call onDragStatusChange with type 'dragging' on subsequent moves while already dragging", () => {
137
+ const onDragStatusChange = vi.fn();
138
+ render(
139
+ <Carousel.Root
140
+ ariaLabel="Featured products"
141
+ allowMouseDrag
142
+ onDragStatusChange={onDragStatusChange}
143
+ >
144
+ <Carousel.Viewport data-testid="viewport">
145
+ <Carousel.Slide />
146
+ <Carousel.Slide />
147
+ </Carousel.Viewport>
148
+ </Carousel.Root>,
149
+ );
150
+ const viewport = screen.getByTestId("viewport");
151
+
152
+ act(() => {
153
+ drag(viewport);
154
+ onDragStatusChange.mockClear();
155
+ fireEvent.pointerMove(viewport, {
156
+ pointerType: "mouse",
157
+ pointerId: 1,
158
+ clientX: 260,
159
+ clientY: 0,
160
+ });
161
+ });
162
+
163
+ expect(onDragStatusChange).toHaveBeenCalledWith({
164
+ type: "dragging",
165
+ page: 0,
166
+ isDragging: true,
167
+ });
168
+ });
169
+
170
+ it("should call onDragStatusChange with type 'dragging.end' on pointerup after a real drag", () => {
171
+ const onDragStatusChange = vi.fn();
172
+ render(
173
+ <Carousel.Root
174
+ ariaLabel="Featured products"
175
+ allowMouseDrag
176
+ onDragStatusChange={onDragStatusChange}
177
+ >
178
+ <Carousel.Viewport data-testid="viewport">
179
+ <Carousel.Slide />
180
+ <Carousel.Slide />
181
+ </Carousel.Viewport>
182
+ </Carousel.Root>,
183
+ );
184
+ const viewport = screen.getByTestId("viewport");
185
+
186
+ act(() => {
187
+ drag(viewport);
188
+ onDragStatusChange.mockClear();
189
+ fireEvent.pointerUp(viewport, { pointerType: "mouse", pointerId: 1 });
190
+ });
191
+
192
+ expect(onDragStatusChange).toHaveBeenCalledWith({
193
+ type: "dragging.end",
194
+ page: 0,
195
+ isDragging: false,
196
+ });
197
+ });
198
+
199
+ it("should call onDragStatusChange with type 'dragging.end' on pointercancel after a real drag", () => {
200
+ const onDragStatusChange = vi.fn();
201
+ render(
202
+ <Carousel.Root
203
+ ariaLabel="Featured products"
204
+ allowMouseDrag
205
+ onDragStatusChange={onDragStatusChange}
206
+ >
207
+ <Carousel.Viewport data-testid="viewport">
208
+ <Carousel.Slide />
209
+ <Carousel.Slide />
210
+ </Carousel.Viewport>
211
+ </Carousel.Root>,
212
+ );
213
+ const viewport = screen.getByTestId("viewport");
214
+
215
+ act(() => {
216
+ drag(viewport);
217
+ onDragStatusChange.mockClear();
218
+ fireEvent.pointerCancel(viewport, {
219
+ pointerType: "mouse",
220
+ pointerId: 1,
221
+ });
222
+ });
223
+
224
+ expect(onDragStatusChange).toHaveBeenCalledWith({
225
+ type: "dragging.end",
226
+ page: 0,
227
+ isDragging: false,
228
+ });
229
+ });
230
+
231
+ it("should not call onDragStatusChange for a movement that never crosses the drag threshold", () => {
232
+ const onDragStatusChange = vi.fn();
233
+ render(
234
+ <Carousel.Root
235
+ ariaLabel="Featured products"
236
+ allowMouseDrag
237
+ onDragStatusChange={onDragStatusChange}
238
+ >
239
+ <Carousel.Viewport data-testid="viewport">
240
+ <Carousel.Slide />
241
+ <Carousel.Slide />
242
+ </Carousel.Viewport>
243
+ </Carousel.Root>,
244
+ );
245
+ const viewport = screen.getByTestId("viewport");
246
+
247
+ act(() => {
248
+ fireEvent.pointerDown(viewport, {
249
+ pointerType: "mouse",
250
+ pointerId: 1,
251
+ clientX: 200,
252
+ clientY: 0,
253
+ });
254
+ fireEvent.pointerMove(viewport, {
255
+ pointerType: "mouse",
256
+ pointerId: 1,
257
+ clientX: 201,
258
+ clientY: 0,
259
+ });
260
+ fireEvent.pointerUp(viewport, { pointerType: "mouse", pointerId: 1 });
261
+ });
262
+
263
+ expect(onDragStatusChange).not.toHaveBeenCalled();
264
+ });
265
+
266
+ it("should report the live currentPage in the callback payload", () => {
267
+ const onDragStatusChange = vi.fn();
268
+ render(
269
+ <Carousel.Root
270
+ ariaLabel="Featured products"
271
+ defaultPage={1}
272
+ allowMouseDrag
273
+ onDragStatusChange={onDragStatusChange}
274
+ >
275
+ <Carousel.Viewport data-testid="viewport">
276
+ <Carousel.Slide />
277
+ <Carousel.Slide />
278
+ </Carousel.Viewport>
279
+ </Carousel.Root>,
280
+ );
281
+
282
+ act(() => {
283
+ drag(screen.getByTestId("viewport"));
284
+ });
285
+
286
+ expect(onDragStatusChange).toHaveBeenCalledWith({
287
+ type: "dragging.start",
288
+ page: 1,
289
+ isDragging: true,
290
+ });
291
+ });
292
+ });
@@ -0,0 +1,38 @@
1
+ import { render } from "@testing-library/react";
2
+
3
+ import { Carousel } from "../index.ts";
4
+
5
+ describe("Carousel dynamic slide count", () => {
6
+ it("should not crash the IntersectionObserver when the slide count shrinks in the same render that changes another viewport-effect dependency", () => {
7
+ // Regression: the IO effect closes over `slideKeys` from its render but
8
+ // reads the live `slidesRef`. When the slide count drops in the same commit
9
+ // that another effect dependency changes (e.g. a Builder Reset flips
10
+ // `transition` fade→slide and drops the count at once), the effect re-runs
11
+ // with the pre-drop `slideKeys` while the removed slides have already left
12
+ // `slidesRef` in the mutation phase — so `observer.observe` was handed
13
+ // `undefined` for the orphaned keys.
14
+ function Harness({
15
+ count,
16
+ transition,
17
+ }: {
18
+ count: number;
19
+ transition: "slide" | "fade";
20
+ }) {
21
+ return (
22
+ <Carousel.Root ariaLabel="Featured products" transition={transition}>
23
+ <Carousel.Viewport>
24
+ {Array.from({ length: count }, (_, index) => (
25
+ <Carousel.Slide key={index} data-testid={`slide-${index}`} />
26
+ ))}
27
+ </Carousel.Viewport>
28
+ </Carousel.Root>
29
+ );
30
+ }
31
+
32
+ const { rerender } = render(<Harness count={8} transition="fade" />);
33
+
34
+ expect(() =>
35
+ rerender(<Harness count={4} transition="slide" />),
36
+ ).not.toThrow();
37
+ });
38
+ });
@@ -251,6 +251,88 @@ describe("Carousel.Indicator", () => {
251
251
  "active",
252
252
  );
253
253
  });
254
+
255
+ it("should render a non-interactive element (not a button) when readOnly", () => {
256
+ const { container } = render(
257
+ <Carousel.Root ariaLabel="Featured products">
258
+ <Carousel.IndicatorGroup label="Choose slide">
259
+ <Carousel.Indicator index={0} readOnly />
260
+ </Carousel.IndicatorGroup>
261
+ </Carousel.Root>,
262
+ );
263
+
264
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
265
+ const dot = container.querySelector("[data-carousel-indicator]")!;
266
+ expect(dot.tagName).toBe("SPAN");
267
+ expect(dot).toHaveAttribute("aria-hidden", "true");
268
+ });
269
+
270
+ it("should still track the active page via data-state when readOnly", async () => {
271
+ const user = userEvent.setup();
272
+ const { container } = render(
273
+ <Carousel.Root ariaLabel="Featured products">
274
+ <Carousel.Viewport>
275
+ <Carousel.Slide data-testid="slide-0" />
276
+ <Carousel.Slide data-testid="slide-1" />
277
+ </Carousel.Viewport>
278
+ <Carousel.NextTrigger>Next</Carousel.NextTrigger>
279
+ <Carousel.IndicatorGroup label="Choose slide">
280
+ <Carousel.Indicator index={0} readOnly />
281
+ <Carousel.Indicator index={1} readOnly />
282
+ </Carousel.IndicatorGroup>
283
+ </Carousel.Root>,
284
+ );
285
+
286
+ const dots = container.querySelectorAll("[data-carousel-indicator]");
287
+ expect(dots[0]).toHaveAttribute("data-state", "active");
288
+ expect(dots[1]).toHaveAttribute("data-state", "inactive");
289
+
290
+ await user.click(screen.getByRole("button", { name: "Next" }));
291
+
292
+ expect(dots[0]).toHaveAttribute("data-state", "inactive");
293
+ expect(dots[1]).toHaveAttribute("data-state", "active");
294
+ });
295
+
296
+ it("should not navigate when a readOnly indicator is clicked", async () => {
297
+ const user = userEvent.setup();
298
+ const { container } = render(
299
+ <Carousel.Root ariaLabel="Featured products">
300
+ <Carousel.Viewport>
301
+ <Carousel.Slide data-testid="slide-0" />
302
+ <Carousel.Slide data-testid="slide-1" />
303
+ </Carousel.Viewport>
304
+ <Carousel.IndicatorGroup label="Choose slide">
305
+ <Carousel.Indicator index={0} readOnly />
306
+ <Carousel.Indicator index={1} readOnly />
307
+ </Carousel.IndicatorGroup>
308
+ </Carousel.Root>,
309
+ );
310
+
311
+ const dots = container.querySelectorAll("[data-carousel-indicator]");
312
+ await user.click(dots[1]);
313
+
314
+ expect(screen.getByTestId("slide-0")).toHaveAttribute(
315
+ "data-state",
316
+ "active",
317
+ );
318
+ });
319
+
320
+ it("should still invoke the consumer's onClick handler when readOnly", async () => {
321
+ const user = userEvent.setup();
322
+ const onClick = vi.fn();
323
+ const { container } = render(
324
+ <Carousel.Root ariaLabel="Featured products">
325
+ <Carousel.IndicatorGroup label="Choose slide">
326
+ <Carousel.Indicator index={0} readOnly onClick={onClick} />
327
+ </Carousel.IndicatorGroup>
328
+ </Carousel.Root>,
329
+ );
330
+
331
+ const dot = container.querySelector("[data-carousel-indicator]")!;
332
+ await user.click(dot);
333
+
334
+ expect(onClick).toHaveBeenCalledTimes(1);
335
+ });
254
336
  });
255
337
 
256
338
  describe("Carousel.Indicators (auto-rendered)", () => {
@@ -396,6 +478,25 @@ describe("Carousel.Indicators (auto-rendered)", () => {
396
478
  testClass,
397
479
  );
398
480
  });
481
+
482
+ it("should forward readOnly to every auto-rendered indicator", () => {
483
+ const { container } = render(
484
+ <Carousel.Root ariaLabel="Featured products">
485
+ <Carousel.Viewport>
486
+ <Carousel.Slide />
487
+ <Carousel.Slide />
488
+ </Carousel.Viewport>
489
+ <Carousel.Indicators label="Choose slide" readOnly />
490
+ </Carousel.Root>,
491
+ );
492
+
493
+ expect(
494
+ screen.queryAllByRole("button", { name: /^Slide \d+$/ }),
495
+ ).toHaveLength(0);
496
+ expect(
497
+ container.querySelectorAll("[data-carousel-indicator]"),
498
+ ).toHaveLength(2);
499
+ });
399
500
  });
400
501
 
401
502
  describe("Carousel.Indicator active state", () => {