@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
@@ -1,26 +1,34 @@
1
1
  import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
2
2
 
3
3
  /**
4
- * Props for {@link SkipNav.Link} — `children`, an optional `contentId`,
5
- * plus all `AnchorHTMLAttributes` on the rendered `<a>`.
4
+ * Props for {@link SkipNavLink | `SkipNav.Link`} — `children`, an optional
5
+ * `contentId`, plus all native `<a>` attributes.
6
6
  */
7
7
  export type SkipNavLinkProps = {
8
+ /** The visible link text (e.g. `"Skip to main content"`). */
8
9
  children?: ReactNode;
9
10
  /**
10
- * Id of the matching {@link SkipNav.Content} target. The link's `href`
11
- * is derived as `#${contentId}`. Defaults to a shared id, so an
12
- * unconfigured `SkipNav.Link` / `SkipNav.Content` pair works as-is.
11
+ * Id of the matching {@link SkipNavContent | `SkipNav.Content`} target.
12
+ * The link's `href` is derived as `#${contentId}`. Defaults to the shared
13
+ * id (`"primitiv-skip-nav"`), so an unconfigured
14
+ * `SkipNav.Link` / `SkipNav.Content` pair works out of the box. Override
15
+ * only when you also set a matching native `id` prop on `SkipNav.Content`.
16
+ * @default "primitiv-skip-nav"
13
17
  */
14
18
  contentId?: string;
15
19
  } & AnchorHTMLAttributes<HTMLAnchorElement>;
16
20
 
17
21
  /**
18
- * Props for {@link SkipNav.Content} — `children` plus all `HTMLAttributes`
19
- * on the rendered `<div>`.
22
+ * Props for {@link SkipNavContent | `SkipNav.Content`} — `children` plus
23
+ * all native `<div>` attributes.
20
24
  *
21
- * Pass a custom `id` only if you set a matching `contentId` on
22
- * {@link SkipNav.Link}; it defaults to the same shared id.
25
+ * The component always renders with `tabIndex={-1}` (making it a valid
26
+ * programmatic focus target) and sets `id` to `"primitiv-skip-nav"` by
27
+ * default. Pass a native `id` prop to override the default; when you do,
28
+ * set the same value as `contentId` on {@link SkipNavLink | `SkipNav.Link`}
29
+ * so the anchor's `href` resolves correctly.
23
30
  */
24
31
  export type SkipNavContentProps = {
32
+ /** The main page content that follows the skip target. */
25
33
  children?: ReactNode;
26
34
  } & HTMLAttributes<HTMLDivElement>;
@@ -10,11 +10,11 @@ styles ship.
10
10
  ```tsx
11
11
  import { Slider } from "@primitiv-ui/react";
12
12
 
13
- <Slider.Root defaultValue={[40]} aria-label="Volume">
13
+ <Slider.Root defaultValue={[40]}>
14
14
  <Slider.Track>
15
15
  <Slider.Range />
16
16
  </Slider.Track>
17
- <Slider.Thumb />
17
+ <Slider.Thumb aria-label="Volume" />
18
18
  </Slider.Root>
19
19
  ```
20
20
 
@@ -56,12 +56,12 @@ Pass `defaultValue` (or omit it for a single thumb seeded at `min`). The
56
56
  component owns the value internally.
57
57
 
58
58
  ```tsx
59
- <Slider.Root defaultValue={[25, 75]} aria-label="Price range">
59
+ <Slider.Root defaultValue={[25, 75]}>
60
60
  <Slider.Track>
61
61
  <Slider.Range />
62
62
  </Slider.Track>
63
- <Slider.Thumb />
64
- <Slider.Thumb />
63
+ <Slider.Thumb aria-label="Minimum" />
64
+ <Slider.Thumb aria-label="Maximum" />
65
65
  </Slider.Root>
66
66
  ```
67
67
 
@@ -92,10 +92,40 @@ only the final value.
92
92
  defaultValue={[40]}
93
93
  onValueCommit={(value) => save(value)}
94
94
  >
95
- …
95
+ ...
96
96
  </Slider.Root>
97
97
  ```
98
98
 
99
+ ## Labelling
100
+
101
+ **The accessible name goes on the `Thumb`, not the Root.** The Thumb is the
102
+ `role="slider"`; the Root renders a plain `<span>` with no role, so an
103
+ `aria-label` there is attached to nothing and announced nowhere. It is a silent
104
+ failure — the slider still looks and behaves correctly — and the examples in this
105
+ file used to model it.
106
+
107
+ ```tsx
108
+ <Slider.Thumb aria-label="Volume" />
109
+ ```
110
+
111
+ For a **visible** label, give it an id and point the thumb at it. A
112
+ `<label htmlFor>` cannot associate with a `<span role="slider">`, and `Slider`
113
+ does not read `FieldContext` (only `Input`, `Textarea` and `Select` do) — so a
114
+ `Field.Label` does not reach the thumb on its own:
115
+
116
+ ```tsx
117
+ <Field.Root>
118
+ <Field.Label id="volume-label">Volume</Field.Label>
119
+ <Slider.Root defaultValue={[40]}>
120
+ <Slider.Track><Slider.Range /></Slider.Track>
121
+ <Slider.Thumb aria-labelledby="volume-label" />
122
+ </Slider.Root>
123
+ </Field.Root>
124
+ ```
125
+
126
+ On a range, name each thumb separately — two thumbs sharing one label are
127
+ announced identically, so you cannot tell by ear which end you are on.
128
+
99
129
  ## Keyboard interaction
100
130
 
101
131
  Each thumb is independently focusable.
@@ -141,7 +171,7 @@ surrounding `<form>`. Multi-thumb sliders suffix the name with `[]`
141
171
  ```tsx
142
172
  <form>
143
173
  <Slider.Root defaultValue={[10, 90]} name="price">
144
- …
174
+ ...
145
175
  </Slider.Root>
146
176
  </form>
147
177
  ```
@@ -55,6 +55,8 @@ import { getRangeStyle } from "./utils";
55
55
  *
56
56
  * @throws if `min` is not less than `max`, or `step` is not greater than 0.
57
57
  *
58
+ * @extends HTMLSpanElement
59
+ *
58
60
  * @example Single-thumb, uncontrolled
59
61
  * ```tsx
60
62
  * <Slider.Root defaultValue={[50]}>
@@ -153,6 +155,15 @@ SliderRoot.displayName = "SliderRoot";
153
155
  * **`asChild` prop.** Render a consumer element in place of the `<span>`.
154
156
  *
155
157
  * @throws if rendered outside a `Slider.Root`.
158
+ *
159
+ * @extends HTMLSpanElement
160
+ *
161
+ * @example
162
+ * ```tsx
163
+ * <Slider.Track>
164
+ * <Slider.Range />
165
+ * </Slider.Track>
166
+ * ```
156
167
  */
157
168
  export function SliderTrack({
158
169
  children,
@@ -184,6 +195,15 @@ SliderTrack.displayName = "SliderTrack";
184
195
  * **`asChild` prop.** Render a consumer element in place of the `<span>`.
185
196
  *
186
197
  * @throws if rendered outside a `Slider.Root`.
198
+ *
199
+ * @extends HTMLSpanElement
200
+ *
201
+ * @example
202
+ * ```tsx
203
+ * <Slider.Track>
204
+ * <Slider.Range />
205
+ * </Slider.Track>
206
+ * ```
187
207
  */
188
208
  export function SliderRange({
189
209
  style,
@@ -226,12 +246,24 @@ SliderRange.displayName = "SliderRange";
226
246
  * **Styling hooks.** `data-orientation` and `data-disabled`. Position is set
227
247
  * via an inline `style` offset; consumer `style` is shallow-merged on top.
228
248
  *
229
- * Provide an accessible name with `aria-label` / `aria-labelledby`.
249
+ * **Accessibility.** Provide an accessible name with `aria-label` /
250
+ * `aria-labelledby`. `aria-valuenow` always tracks the raw number; when that
251
+ * number is not self-explanatory to a screen reader (a currency amount, a
252
+ * date, a named step), pass `aria-valuetext` with the human-readable form
253
+ * (e.g. `aria-valuetext="$50"`) — it is forwarded straight through to the
254
+ * element and read in place of the bare number.
230
255
  *
231
256
  * **`asChild` prop.** Render a consumer element in place of the `<span>`;
232
257
  * the slider role, ARIA value attributes, keyboard handler, and ref merge in.
233
258
  *
234
259
  * @throws if rendered outside a `Slider.Root`.
260
+ *
261
+ * @extends HTMLSpanElement
262
+ *
263
+ * @example Named value with `aria-valuetext`
264
+ * ```tsx
265
+ * <Slider.Thumb aria-label="Price" aria-valuetext={`$${value}`} />
266
+ * ```
235
267
  */
236
268
  export function SliderThumb({
237
269
  style,
@@ -298,18 +330,24 @@ export type TSliderCompound = typeof SliderRoot & {
298
330
  * - {@link SliderRange | `Slider.Range`} — the filled portion of the track.
299
331
  * - {@link SliderThumb | `Slider.Thumb`} — a draggable handle; one per value.
300
332
  *
333
+ * The accessible name belongs on the **Thumb**, which is the `role="slider"`.
334
+ * The Root renders a plain `<span>` with no role, so an `aria-label` there is
335
+ * attached to nothing and announced nowhere — a silent failure, because the
336
+ * slider still looks and behaves correctly. This example used to put it on the
337
+ * Root.
338
+ *
301
339
  * @example
302
340
  * ```tsx
303
341
  * import { Slider } from "@primitiv-ui/react";
304
342
  *
305
- * <Slider.Root defaultValue={[40]} aria-label="Volume">
343
+ * <Slider.Root defaultValue={[40]}>
306
344
  * <Slider.Track><Slider.Range /></Slider.Track>
307
- * <Slider.Thumb />
345
+ * <Slider.Thumb aria-label="Volume" />
308
346
  * </Slider.Root>
309
347
  * ```
310
348
  *
311
349
  * @see {@link SliderRoot} for state modes, form submission, and validation.
312
- * @see {@link SliderThumb} for keyboard interaction.
350
+ * @see {@link SliderThumb} for keyboard interaction and labelling.
313
351
  */
314
352
  const SliderCompound: TSliderCompound = Object.assign(SliderRoot, {
315
353
  Root: SliderRoot,
@@ -3,19 +3,46 @@ import { createStrictContext } from "../utils/index.ts";
3
3
 
4
4
  import type { SliderDirection, SliderOrientation } from "./types";
5
5
 
6
- /** Shared state published by `Slider.Root` to its sub-components: the current values, range/step bounds, orientation and direction, thumb registration, and value-commit callbacks. */
6
+ /**
7
+ * Shared state published by {@link SliderRoot | `Slider.Root`} to its
8
+ * sub-components: the current values, range/step bounds, orientation and
9
+ * direction, thumb registration, and the value-change / value-commit
10
+ * callbacks. Read it with {@link useSliderContext}; it throws when a
11
+ * sub-component is rendered outside a `<Slider.Root>`.
12
+ */
7
13
  export type SliderContextValue = {
14
+ /** The current value array — one entry per registered thumb, in order. */
8
15
  values: number[];
16
+ /** Lowest value (track start edge). */
9
17
  min: number;
18
+ /** Highest value (track end edge). */
10
19
  max: number;
20
+ /** Granularity; every value snaps to a multiple of this anchored at `min`. */
11
21
  step: number;
22
+ /** Resolved layout axis; see {@link SliderOrientation}. */
12
23
  orientation: SliderOrientation;
24
+ /** Resolved reading direction (after inheriting from `DirectionProvider`); see {@link SliderDirection}. */
13
25
  dir: SliderDirection;
26
+ /** Whether the axis is reversed relative to its natural direction. */
14
27
  inverted: boolean;
28
+ /** Whether interaction is disabled across every part. */
15
29
  disabled: boolean;
30
+ /**
31
+ * Registers (or, with `null`, unregisters) a thumb by its stable id so Root
32
+ * can order the thumbs and focus them on pointer interaction. Called from
33
+ * each {@link SliderThumb | `Slider.Thumb`} on mount/unmount.
34
+ */
16
35
  registerThumb: (id: string, element: HTMLSpanElement | null) => void;
36
+ /** Registered thumb ids in DOM order; a thumb's index into `values` is its position here. */
17
37
  orderedThumbIds: string[];
38
+ /**
39
+ * Sets the thumb at `index` to `value`, clamped between its neighbours (plus
40
+ * `minStepsBetweenThumbs`) and the `[min, max]` bounds. Returns the next
41
+ * value array when it changed, or `null` when the clamped value was
42
+ * unchanged (so callers can skip a redundant commit).
43
+ */
18
44
  setThumbValue: (index: number, value: number) => number[] | null;
45
+ /** Fires `onValueCommit` with the settled array at the end of an interaction. */
19
46
  commit: (values: number[]) => void;
20
47
  };
21
48
 
@@ -1,57 +1,205 @@
1
1
  import type { ComponentProps } from "react";
2
2
 
3
- /** Layout axis of the slider track. */
3
+ /**
4
+ * Layout axis of the slider track. `"horizontal"` lays the track out along
5
+ * the x-axis and binds ArrowLeft/ArrowRight (plus ArrowUp/ArrowDown);
6
+ * `"vertical"` lays it out along the y-axis, positions thumbs from the
7
+ * bottom edge, and maps ArrowUp to *increase*.
8
+ */
4
9
  export type SliderOrientation = "horizontal" | "vertical";
5
- /** Reading direction of the slider, affecting which end is the minimum. */
10
+ /**
11
+ * Reading direction of a **horizontal** slider, affecting which end of the
12
+ * track is the minimum and the arrow-key direction. `"rtl"` mirrors the axis
13
+ * so the value increases leftwards. Ignored on the vertical axis.
14
+ */
6
15
  export type SliderDirection = "ltr" | "rtl";
7
16
 
8
- /** Props common to both controlled and uncontrolled `Slider.Root`: range/step bounds, orientation, direction, and the native `<span>` attributes. */
17
+ /**
18
+ * Props common to both the controlled ({@link SliderRootControlledProps}) and
19
+ * uncontrolled ({@link SliderRootUncontrolledProps}) `Slider.Root` shapes:
20
+ * the range/step bounds, orientation, reading direction, form `name`, and the
21
+ * native `<span>` attributes.
22
+ *
23
+ * `defaultValue` and `dir` are `Omit`-ted from the inherited `<span>` props
24
+ * because both are redeclared with narrower slider-specific types — the
25
+ * native `defaultValue` is a string/number DOM attribute (replaced by the
26
+ * `number[]` value array) and the native `dir` is `string` (narrowed to
27
+ * {@link SliderDirection}). Omitting them keeps the narrowed types (and this
28
+ * JSDoc) visible to the docs-data extractor rather than the wider DOM ones.
29
+ */
9
30
  export type SliderRootSharedProps = Omit<
10
31
  ComponentProps<"span">,
11
32
  "defaultValue" | "dir"
12
33
  > & {
34
+ /**
35
+ * Lowest value the slider can take — the value at the track's start edge.
36
+ * Must be strictly less than {@link SliderRootSharedProps.max | `max`};
37
+ * `Slider.Root` throws during render otherwise.
38
+ * @default 0
39
+ */
13
40
  min?: number;
41
+ /**
42
+ * Highest value the slider can take — the value at the track's end edge.
43
+ * Must be strictly greater than {@link SliderRootSharedProps.min | `min`};
44
+ * `Slider.Root` throws during render otherwise.
45
+ * @default 100
46
+ */
14
47
  max?: number;
48
+ /**
49
+ * Granularity of the value: every thumb value snaps to a multiple of `step`
50
+ * anchored at `min`, and each arrow-key press moves by one `step` (Page
51
+ * Up/Down move by ten). Fractional steps (e.g. `0.1`) are snapped to the
52
+ * step's decimal precision. Must be greater than `0`; `Slider.Root` throws
53
+ * during render otherwise.
54
+ * @default 1
55
+ */
15
56
  step?: number;
57
+ /**
58
+ * Minimum gap, expressed **in steps**, enforced between adjacent thumbs on a
59
+ * multi-thumb slider. The gap in value units is `minStepsBetweenThumbs *
60
+ * step`. A thumb is clamped so it can never come closer than this to its
61
+ * neighbours (and can never cross them). Has no effect on a single-thumb
62
+ * slider.
63
+ * @default 0
64
+ */
16
65
  minStepsBetweenThumbs?: number;
66
+ /**
67
+ * Layout axis of the track; see {@link SliderOrientation}. Sets
68
+ * `data-orientation` and each thumb's `aria-orientation`, and selects which
69
+ * pointer axis and arrow keys drive the value.
70
+ * @default "horizontal"
71
+ */
17
72
  orientation?: SliderOrientation;
73
+ /**
74
+ * Reading direction of a horizontal slider; see {@link SliderDirection}.
75
+ * Affects which end is the minimum, the arrow-key direction, pointer
76
+ * mapping, and the inline positioning styles. When omitted, it is inherited
77
+ * from the nearest {@link DirectionProvider}, falling back to `"ltr"` when
78
+ * there is no provider; an explicit prop always wins.
79
+ */
18
80
  dir?: SliderDirection;
81
+ /**
82
+ * Reverse the direction the value increases along the axis. Combines with
83
+ * {@link SliderRootSharedProps.orientation | `orientation`} and
84
+ * {@link SliderRootSharedProps.dir | `dir`} to flip the pointer mapping,
85
+ * arrow keys, and the edge the thumb/range offset is anchored to.
86
+ * @default false
87
+ */
19
88
  inverted?: boolean;
89
+ /**
90
+ * Disable all interaction. Every part receives `data-disabled=""`, thumbs
91
+ * leave the tab order and gain `aria-disabled="true"`, and both keyboard and
92
+ * pointer handlers become no-ops.
93
+ * @default false
94
+ */
20
95
  disabled?: boolean;
96
+ /**
97
+ * When set, `Slider.Root` renders one hidden `<input>` per thumb carrying
98
+ * that thumb's value so the slider posts with a surrounding `<form>`. A
99
+ * multi-thumb slider suffixes the name with `[]` (e.g. `name="range"` →
100
+ * `range[]`); a single-thumb slider uses the bare name. When omitted, no
101
+ * hidden inputs are rendered.
102
+ */
21
103
  name?: string;
104
+ /**
105
+ * Render the child element in place of the default `<span>` via the
106
+ * {@link Slot} pattern, merging `Slider.Root`'s `dir`, `data-*` hooks,
107
+ * pointer handler, and ref onto it.
108
+ * @default false
109
+ */
22
110
  asChild?: boolean;
23
111
  };
24
112
 
25
- /** Uncontrolled `Slider.Root` props: seed with `defaultValue`; `value` is disallowed. */
113
+ /**
114
+ * Uncontrolled variant of {@link SliderRootProps}: the component owns the
115
+ * value array internally. Seed it with `defaultValue` (or omit it for a
116
+ * single thumb seeded at `min`); `value` is forbidden.
117
+ */
26
118
  export type SliderRootUncontrolledProps = SliderRootSharedProps & {
119
+ /**
120
+ * The value array on first render — one number per
121
+ * {@link SliderThumb | `Slider.Thumb`}, in order (`[value]` for a single
122
+ * thumb, `[low, high]` for a range). After mount the component owns the
123
+ * value. Defaults to `[min]` when omitted.
124
+ */
27
125
  defaultValue?: number[];
126
+ /** Forbidden in uncontrolled mode — use `defaultValue` instead. */
28
127
  value?: never;
128
+ /**
129
+ * Called with the full next value array on every change (each arrow-key
130
+ * press and every pointer increment during a drag). Optional in
131
+ * uncontrolled mode.
132
+ */
29
133
  onValueChange?: (value: number[]) => void;
134
+ /**
135
+ * Called **once** with the settled value array when an interaction ends — a
136
+ * pointer release, or a completed keyboard press. Use it to persist only the
137
+ * final value rather than every intermediate step.
138
+ */
30
139
  onValueCommit?: (value: number[]) => void;
31
140
  };
32
141
 
33
- /** Controlled `Slider.Root` props: drive with `value`; `defaultValue` is disallowed. */
142
+ /**
143
+ * Controlled variant of {@link SliderRootProps}: the parent owns the value
144
+ * array via `value` and is expected to keep it in sync through
145
+ * `onValueChange`. `defaultValue` is forbidden.
146
+ */
34
147
  export type SliderRootControlledProps = SliderRootSharedProps & {
148
+ /** Forbidden in controlled mode — use `value` instead. */
35
149
  defaultValue?: never;
150
+ /**
151
+ * The current value array — one number per
152
+ * {@link SliderThumb | `Slider.Thumb`}, in order (`[value]` for a single
153
+ * thumb, `[low, high]` for a range). Must be kept in sync by the parent via
154
+ * `onValueChange`.
155
+ */
36
156
  value: number[];
157
+ /**
158
+ * Called with the full next value array on every change (each arrow-key
159
+ * press and every pointer increment during a drag). Update `value` from it
160
+ * to keep the slider in sync.
161
+ */
37
162
  onValueChange?: (value: number[]) => void;
163
+ /**
164
+ * Called **once** with the settled value array when an interaction ends — a
165
+ * pointer release, or a completed keyboard press. Use it to persist only the
166
+ * final value rather than every intermediate step.
167
+ */
38
168
  onValueCommit?: (value: number[]) => void;
39
169
  };
40
170
 
41
- /** Props for `Slider.Root` — the discriminated union of controlled ({@link SliderRootControlledProps}) and uncontrolled ({@link SliderRootUncontrolledProps}) modes. */
171
+ /** Props for {@link SliderRoot | `Slider.Root`} — the discriminated union of controlled ({@link SliderRootControlledProps}) and uncontrolled ({@link SliderRootUncontrolledProps}) modes; only one shape is accepted by TypeScript at a time. */
42
172
  export type SliderRootProps =
43
173
  | SliderRootUncontrolledProps
44
174
  | SliderRootControlledProps;
45
175
 
46
- /** Props for `Slider.Track` — the full-length rail; native `<span>` props plus `asChild`. */
176
+ /** Props for {@link SliderTrack | `Slider.Track`} — the full-length rail the thumbs travel along; all native `<span>` attributes plus `asChild`. */
47
177
  export type SliderTrackProps = ComponentProps<"span"> & {
178
+ /**
179
+ * Render the child element in place of the default `<span>` via the
180
+ * {@link Slot} pattern, merging the `data-orientation` / `data-disabled`
181
+ * hooks onto it.
182
+ * @default false
183
+ */
48
184
  asChild?: boolean;
49
185
  };
50
- /** Props for `Slider.Range` — the filled segment between the minimum and the active thumb; native `<span>` props plus `asChild`. */
186
+ /** Props for {@link SliderRange | `Slider.Range`} — the filled segment between the lowest and highest thumb (or from the track start to the single thumb); all native `<span>` attributes plus `asChild`. */
51
187
  export type SliderRangeProps = ComponentProps<"span"> & {
188
+ /**
189
+ * Render the child element in place of the default `<span>` via the
190
+ * {@link Slot} pattern, merging the `data-orientation` / `data-disabled`
191
+ * hooks and the computed inline positioning `style` onto it.
192
+ * @default false
193
+ */
52
194
  asChild?: boolean;
53
195
  };
54
- /** Props for `Slider.Thumb` — a draggable handle; native `<span>` props plus `asChild`. */
196
+ /** Props for {@link SliderThumb | `Slider.Thumb`} — a draggable, focusable handle; render one per entry in the value array. All native `<span>` attributes plus `asChild`. */
55
197
  export type SliderThumbProps = ComponentProps<"span"> & {
198
+ /**
199
+ * Render the child element in place of the default `<span>` via the
200
+ * {@link Slot} pattern, merging the `role="slider"` semantics, `aria-value*`
201
+ * attributes, keyboard handler, positioning `style`, and ref onto it.
202
+ * @default false
203
+ */
56
204
  asChild?: boolean;
57
205
  };
package/src/Slot/Slot.tsx CHANGED
@@ -34,7 +34,15 @@ import { AnyProps, PossibleRef } from "../types";
34
34
  function setRef<T>(ref: PossibleRef<T>, value: T) {
35
35
  if (typeof ref === "function") {
36
36
  ref(value);
37
- } else if (ref !== null && ref !== undefined) {
37
+ return;
38
+ }
39
+ // Forcing this guard true dereferences a null/undefined ref (a child with no
40
+ // ref of its own), throwing during React's commit-phase ref assignment. The
41
+ // "no ref of its own" test catches it, but Stryker's vitest runner can't
42
+ // attribute that commit-phase throw (the Avatar/Slot-guard limitation) — not
43
+ // equivalent.
44
+ // Stryker disable next-line ConditionalExpression
45
+ if (ref !== null && ref !== undefined) {
38
46
  (ref as RefObject<T>).current = value;
39
47
  }
40
48
  }
@@ -42,7 +50,15 @@ function setRef<T>(ref: PossibleRef<T>, value: T) {
42
50
  /**
43
51
  * Combines multiple refs into a single callback ref that sets all of them
44
52
  * simultaneously. Handles function refs, object refs (`{ current }`), and
45
- * `undefined` — any of which may be mixed freely.
53
+ * `undefined` — any of which may be mixed freely, in any order, and any
54
+ * number of them.
55
+ *
56
+ * Used throughout the headless components whenever a sub-component owns an
57
+ * internal ref (for measuring, focus management, roving-tabindex, etc.)
58
+ * *and* needs to forward the consumer's own `ref` to the same DOM node —
59
+ * two refs, one element, one callback ref that satisfies both. `Slot`
60
+ * itself uses `composeRefs` internally to merge its `forwardedRef` with the
61
+ * child element's own ref (see {@link Slot}).
46
62
  *
47
63
  * @example Compose an internal ref with a consumer-supplied external ref:
48
64
  * ```tsx
@@ -67,6 +83,10 @@ function getRef(element: React.ReactElement): PossibleRef<unknown> {
67
83
  const { props, ref } = element as React.ReactElement & {
68
84
  ref?: PossibleRef<unknown>;
69
85
  };
86
+ // Stryker disable next-line ConditionalExpression: in React 19, element.ref
87
+ // returns the same ref as props.ref for real elements, so preferring props.ref
88
+ // vs. falling back to ref is indistinguishable at runtime — an equivalent
89
+ // mutant. The ternary stays for the React <=18 shape, where the two differ.
70
90
  return (props as AnyProps).ref !== undefined
71
91
  ? ((props as AnyProps).ref as PossibleRef<unknown>)
72
92
  : ref;
@@ -109,29 +129,67 @@ function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
109
129
  /**
110
130
  * Renders its single child element with the Slot's own props merged in.
111
131
  *
112
- * Used to implement the `asChild` pattern: a component that normally renders
113
- * its own DOM element can instead delegate to a consumer-supplied element
114
- * while preserving all of its own behaviour (ARIA attributes, event handlers,
115
- * `ref`, etc.).
132
+ * `Slot` is the composition primitive behind every `asChild` prop in this
133
+ * library (`Button`, `Tabs.Trigger`, `Select.Root`, `Breadcrumb.Link`, and
134
+ * more). A component that normally renders its own DOM element (say,
135
+ * `<button>`) can instead delegate rendering to a single consumer-supplied
136
+ * element — typically a routing library's `<Link>` or a custom styled
137
+ * element — while preserving all of its own behaviour: ARIA attributes,
138
+ * data attributes, event handlers, and `ref` all still land on the
139
+ * rendered DOM node, just not the node `Slot` itself would have created.
140
+ * The pattern avoids an extra wrapper element in the DOM, which matters for
141
+ * layout (no unwanted `<button><a>...</a></button>` nesting) and for CSS
142
+ * selectors that expect a single element.
116
143
  *
117
- * **Prop-merging rules**:
118
- * - **Event handlers** compose — child's handler fires first, then Slot's.
119
- * - **`style`** is shallow-merged — child wins on key collisions.
120
- * - **`className`** strings are concatenated (`slotClass childClass`).
121
- * - **All other props** default to the child's value; Slot provides the
122
- * fallback when the child doesn't specify one.
123
- * - **Refs** from both sides are composed via {@link composeRefs}.
144
+ * **Usage shape** — every component using `Slot` follows the same
145
+ * branch:
146
+ *
147
+ * ```tsx
148
+ * if (asChild) {
149
+ * return <Slot {...ownProps}>{children}</Slot>;
150
+ * }
151
+ * return <button {...ownProps}>{children}</button>;
152
+ * ```
153
+ *
154
+ * `ownProps` is the exact same object passed to both branches — `Slot`'s
155
+ * job is only to change *where* those props land, never to change what
156
+ * they are.
124
157
  *
125
- * **Constraints**
126
- * - Exactly one React element child is required; Slot throws otherwise.
127
- * - The child must accept a `ref` (i.e. a DOM element or a `forwardRef`
128
- * component).
158
+ * **Prop-merging rules** (`Slot`'s own props are the "slot" side, the
159
+ * single child's own props are the "child" side):
160
+ * - **Event handlers** (any prop matching `/^on[A-Z]/`) compose — the
161
+ * **child's** handler fires first, then the **slot's**. This is the
162
+ * opposite composition direction, and a different merge strategy, from
163
+ * the standalone {@link composeEventHandlers} helper (their-handler-first
164
+ * with a `preventDefault()` veto) — the two are unrelated mechanisms
165
+ * that happen to solve adjacent problems. `Slot`'s handlers always both
166
+ * run; there is no opt-out.
167
+ * - **`style`** is shallow-merged — the child's style object wins on key
168
+ * collisions, so a consumer's inline `style` can override individual
169
+ * properties Slot would otherwise set.
170
+ * - **`className`** strings are concatenated (`"slotClass childClass"`),
171
+ * so both the component's own class hooks and the consumer's class
172
+ * survive.
173
+ * - **All other props** default to the child's value; Slot's value is only
174
+ * used as a fallback when the child doesn't specify that prop at all
175
+ * (key absent, or explicitly `undefined`).
176
+ * - **Refs** from both sides are composed via {@link composeRefs}, so
177
+ * neither the component's internal ref nor the consumer's own ref on the
178
+ * child element is lost.
129
179
  *
130
- * **React version compatibility.** Slot reads the child's ref from
180
+ * **Constraints.** Exactly one React element child is required — `Slot`
181
+ * throws `"Slot requires exactly one React element child."` for zero
182
+ * children or a non-element child (e.g. a plain string). The child must
183
+ * also accept a `ref` (a DOM element or a `forwardRef` component); passing
184
+ * a plain function component as the child silently drops the ref.
185
+ *
186
+ * **React version compatibility.** Slot reads the child's own ref from
131
187
  * `element.props.ref` (React 19+) with a fallback to `element.ref`
132
188
  * (React ≤18) so both runtime versions compose refs correctly.
133
189
  *
134
- * @example
190
+ * @extends HTMLElement
191
+ *
192
+ * @example Basic `asChild` branch inside a component
135
193
  * ```tsx
136
194
  * // Inside a component that normally renders <button>:
137
195
  * if (asChild) {
@@ -139,11 +197,28 @@ function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
139
197
  * }
140
198
  * return <button {...buttonProps}>{children}</button>;
141
199
  * ```
200
+ *
201
+ * @example Consumer rendering a router link in place of the default element
202
+ * ```tsx
203
+ * <Button asChild onClick={trackClick}>
204
+ * <RouterLink to="/settings" className="settings-link">
205
+ * Settings
206
+ * </RouterLink>
207
+ * </Button>
208
+ * // Rendered DOM: a single <a class="...settings-link"> — trackClick and the
209
+ * // link's own onClick both fire; no wrapping <button>.
210
+ * ```
142
211
  */
143
212
  export const Slot: ForwardRefExoticComponent<
144
213
  PropsWithoutRef<SlotProps> & RefAttributes<HTMLElement>
145
214
  > = forwardRef<HTMLElement, SlotProps>(
146
215
  ({ children, ...slotProps }, forwardedRef) => {
216
+ // Stryker disable next-line ConditionalExpression: forcing this guard false
217
+ // lets invalid children fall through to a *different* throw (the destructure
218
+ // in getRef below), which the error tests do catch — but Stryker's vitest
219
+ // runner does not attribute that React render-time throw to a covering test
220
+ // (the same limitation seen on Avatar's ref guard). The operator/block twins
221
+ // of this line are scored; only this variant can't be. Not equivalent.
147
222
  if (Children.count(children) !== 1 || !isValidElement(children)) {
148
223
  throw new Error("Slot requires exactly one React element child.");
149
224
  }