@primitiv-ui/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (585) hide show
  1. package/README.md +79 -0
  2. package/package.json +59 -0
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +40 -0
  4. package/src/AccessibleIcon/README.md +42 -0
  5. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +47 -0
  6. package/src/AccessibleIcon/index.ts +2 -0
  7. package/src/AccessibleIcon/types.ts +8 -0
  8. package/src/Accordion/Accordion.tsx +412 -0
  9. package/src/Accordion/AccordionContext.ts +12 -0
  10. package/src/Accordion/README.md +202 -0
  11. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +237 -0
  12. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +333 -0
  13. package/src/Accordion/__tests__/Accordion.controlled-state.test.tsx +175 -0
  14. package/src/Accordion/__tests__/Accordion.data-attributes.test.tsx +272 -0
  15. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +311 -0
  16. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +119 -0
  17. package/src/Accordion/__tests__/Accordion.forceMount.test.tsx +119 -0
  18. package/src/Accordion/__tests__/Accordion.keyboard-interaction.test.tsx +736 -0
  19. package/src/Accordion/__tests__/Accordion.mouse-interaction.test.tsx +212 -0
  20. package/src/Accordion/__tests__/Accordion.multiple-mode.test.tsx +90 -0
  21. package/src/Accordion/__tests__/Accordion.reading-direction.test.tsx +139 -0
  22. package/src/Accordion/__tests__/Accordion.uncontrolled-state.test.tsx +154 -0
  23. package/src/Accordion/hooks/index.ts +6 -0
  24. package/src/Accordion/hooks/useAccordionContext.ts +1 -0
  25. package/src/Accordion/hooks/useAccordionHeaderContext.ts +10 -0
  26. package/src/Accordion/hooks/useAccordionItem.ts +22 -0
  27. package/src/Accordion/hooks/useAccordionItemContext.ts +1 -0
  28. package/src/Accordion/hooks/useAccordionRoot.ts +151 -0
  29. package/src/Accordion/hooks/useAccordionTrigger.ts +90 -0
  30. package/src/Accordion/index.ts +1 -0
  31. package/src/Accordion/types.ts +81 -0
  32. package/src/Alert/Alert.tsx +43 -0
  33. package/src/Alert/README.md +54 -0
  34. package/src/Alert/__tests__/Alert.test.tsx +28 -0
  35. package/src/Alert/index.ts +2 -0
  36. package/src/Alert/types.ts +5 -0
  37. package/src/Avatar/Avatar.tsx +149 -0
  38. package/src/Avatar/AvatarContext.ts +20 -0
  39. package/src/Avatar/README.md +116 -0
  40. package/src/Avatar/__tests__/Avatar.asChild.test.tsx +53 -0
  41. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +14 -0
  42. package/src/Avatar/__tests__/Avatar.error-handling.test.tsx +30 -0
  43. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +75 -0
  44. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +81 -0
  45. package/src/Avatar/hooks/index.ts +2 -0
  46. package/src/Avatar/hooks/useAvatarContext.ts +1 -0
  47. package/src/Avatar/hooks/useAvatarImage.ts +40 -0
  48. package/src/Avatar/index.ts +3 -0
  49. package/src/Avatar/types.ts +44 -0
  50. package/src/Breadcrumb/Breadcrumb.tsx +234 -0
  51. package/src/Breadcrumb/README.md +111 -0
  52. package/src/Breadcrumb/__tests__/Breadcrumb.asChild.test.tsx +33 -0
  53. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +132 -0
  54. package/src/Breadcrumb/index.ts +2 -0
  55. package/src/Breadcrumb/types.ts +22 -0
  56. package/src/Button/Button.tsx +95 -0
  57. package/src/Button/README.md +112 -0
  58. package/src/Button/__tests__/Button.asChild.test.tsx +91 -0
  59. package/src/Button/__tests__/Button.basic-rendering.test.tsx +126 -0
  60. package/src/Button/__tests__/Button.contract.test.tsx +72 -0
  61. package/src/Button/__tests__/Button.disabled.test.tsx +52 -0
  62. package/src/Button/__tests__/Button.icon-usage.test.tsx +57 -0
  63. package/src/Button/__tests__/Button.keyboard-interaction.test.tsx +70 -0
  64. package/src/Button/index.ts +2 -0
  65. package/src/Button/types.ts +8 -0
  66. package/src/Carousel/Carousel.tsx +708 -0
  67. package/src/Carousel/CarouselContext.ts +11 -0
  68. package/src/Carousel/README.md +848 -0
  69. package/src/Carousel/__tests__/Carousel.asChild.test.tsx +178 -0
  70. package/src/Carousel/__tests__/Carousel.auto-play.test.tsx +617 -0
  71. package/src/Carousel/__tests__/Carousel.basic-rendering.test.tsx +569 -0
  72. package/src/Carousel/__tests__/Carousel.controlled-state.test.tsx +137 -0
  73. package/src/Carousel/__tests__/Carousel.error-handling.test.tsx +81 -0
  74. package/src/Carousel/__tests__/Carousel.ids.test.tsx +111 -0
  75. package/src/Carousel/__tests__/Carousel.imperative-api.test.tsx +213 -0
  76. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +560 -0
  77. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +276 -0
  78. package/src/Carousel/__tests__/Carousel.keyboard-navigation.test.tsx +158 -0
  79. package/src/Carousel/__tests__/Carousel.play-pause.test.tsx +232 -0
  80. package/src/Carousel/__tests__/Carousel.prev-next.test.tsx +68 -0
  81. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +49 -0
  82. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +87 -0
  83. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +179 -0
  84. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +109 -0
  85. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +151 -0
  86. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +183 -0
  87. package/src/Carousel/__tests__/Carousel.touch-interaction.test.tsx +96 -0
  88. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +70 -0
  89. package/src/Carousel/__tests__/Carousel.translations.test.tsx +157 -0
  90. package/src/Carousel/__tests__/Carousel.uncontrolled-state.test.tsx +146 -0
  91. package/src/Carousel/hooks/index.ts +4 -0
  92. package/src/Carousel/hooks/useCarouselContext.ts +13 -0
  93. package/src/Carousel/hooks/useCarouselRoot.ts +450 -0
  94. package/src/Carousel/hooks/useCarouselSlide.ts +45 -0
  95. package/src/Carousel/hooks/useCarouselViewport.ts +290 -0
  96. package/src/Carousel/index.ts +3 -0
  97. package/src/Carousel/types.ts +400 -0
  98. package/src/Checkbox/Checkbox.tsx +228 -0
  99. package/src/Checkbox/CheckboxContext.ts +12 -0
  100. package/src/Checkbox/README.md +156 -0
  101. package/src/Checkbox/__tests__/Checkbox.asChild.test.tsx +69 -0
  102. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +41 -0
  103. package/src/Checkbox/__tests__/Checkbox.controlled-state.test.tsx +82 -0
  104. package/src/Checkbox/__tests__/Checkbox.disabled.test.tsx +15 -0
  105. package/src/Checkbox/__tests__/Checkbox.indeterminate.test.tsx +82 -0
  106. package/src/Checkbox/__tests__/Checkbox.indicator.test.tsx +117 -0
  107. package/src/Checkbox/__tests__/Checkbox.uncontrolled-state.test.tsx +89 -0
  108. package/src/Checkbox/hooks/index.ts +2 -0
  109. package/src/Checkbox/hooks/useCheckboxContext.ts +1 -0
  110. package/src/Checkbox/hooks/useCheckboxRoot.ts +32 -0
  111. package/src/Checkbox/index.ts +1 -0
  112. package/src/Checkbox/types.ts +33 -0
  113. package/src/CheckboxCard/CheckboxCard.tsx +208 -0
  114. package/src/CheckboxCard/CheckboxCardContext.ts +12 -0
  115. package/src/CheckboxCard/README.md +114 -0
  116. package/src/CheckboxCard/__tests__/CheckboxCard.asChild.test.tsx +54 -0
  117. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +58 -0
  118. package/src/CheckboxCard/__tests__/CheckboxCard.controlled-state.test.tsx +77 -0
  119. package/src/CheckboxCard/__tests__/CheckboxCard.disabled.test.tsx +55 -0
  120. package/src/CheckboxCard/__tests__/CheckboxCard.error-handling.test.tsx +20 -0
  121. package/src/CheckboxCard/__tests__/CheckboxCard.indeterminate.test.tsx +60 -0
  122. package/src/CheckboxCard/__tests__/CheckboxCard.indicator.test.tsx +136 -0
  123. package/src/CheckboxCard/__tests__/CheckboxCard.uncontrolled-state.test.tsx +73 -0
  124. package/src/CheckboxCard/hooks/index.ts +2 -0
  125. package/src/CheckboxCard/hooks/useCheckboxCardContext.ts +1 -0
  126. package/src/CheckboxCard/hooks/useCheckboxCardRoot.ts +30 -0
  127. package/src/CheckboxCard/index.ts +3 -0
  128. package/src/CheckboxCard/types.ts +33 -0
  129. package/src/Collapsible/Collapsible.tsx +316 -0
  130. package/src/Collapsible/CollapsibleContext.ts +7 -0
  131. package/src/Collapsible/README.md +174 -0
  132. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +240 -0
  133. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +118 -0
  134. package/src/Collapsible/__tests__/Collapsible.controlled-state.test.tsx +134 -0
  135. package/src/Collapsible/__tests__/Collapsible.disabled.test.tsx +132 -0
  136. package/src/Collapsible/__tests__/Collapsible.error-handling.test.tsx +40 -0
  137. package/src/Collapsible/__tests__/Collapsible.forceMount.test.tsx +111 -0
  138. package/src/Collapsible/__tests__/Collapsible.triggerIcon.test.tsx +93 -0
  139. package/src/Collapsible/__tests__/Collapsible.uncontrolled-state.test.tsx +125 -0
  140. package/src/Collapsible/hooks/index.ts +2 -0
  141. package/src/Collapsible/hooks/useCollapsibleRoot.ts +34 -0
  142. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +49 -0
  143. package/src/Collapsible/index.ts +1 -0
  144. package/src/Collapsible/types.ts +48 -0
  145. package/src/ContextMenu/ContextMenu.tsx +1004 -0
  146. package/src/ContextMenu/ContextMenuContentContext.ts +15 -0
  147. package/src/ContextMenu/ContextMenuContext.ts +21 -0
  148. package/src/ContextMenu/ContextMenuGroupContext.ts +8 -0
  149. package/src/ContextMenu/ContextMenuItemIndicatorContext.ts +8 -0
  150. package/src/ContextMenu/ContextMenuRadioGroupContext.ts +9 -0
  151. package/src/ContextMenu/ContextMenuSubContext.ts +15 -0
  152. package/src/ContextMenu/README.md +275 -0
  153. package/src/ContextMenu/__tests__/ContextMenu.asChild.test.tsx +186 -0
  154. package/src/ContextMenu/__tests__/ContextMenu.basic-rendering.test.tsx +39 -0
  155. package/src/ContextMenu/__tests__/ContextMenu.checkbox-item.test.tsx +145 -0
  156. package/src/ContextMenu/__tests__/ContextMenu.error-handling.test.tsx +113 -0
  157. package/src/ContextMenu/__tests__/ContextMenu.group-label.test.tsx +48 -0
  158. package/src/ContextMenu/__tests__/ContextMenu.item-indicator.test.tsx +88 -0
  159. package/src/ContextMenu/__tests__/ContextMenu.item.test.tsx +106 -0
  160. package/src/ContextMenu/__tests__/ContextMenu.keyboard-interaction.test.tsx +172 -0
  161. package/src/ContextMenu/__tests__/ContextMenu.mouse-interaction.test.tsx +227 -0
  162. package/src/ContextMenu/__tests__/ContextMenu.radio-item.test.tsx +127 -0
  163. package/src/ContextMenu/__tests__/ContextMenu.reading-direction.test.tsx +152 -0
  164. package/src/ContextMenu/__tests__/ContextMenu.separator.test.tsx +47 -0
  165. package/src/ContextMenu/__tests__/ContextMenu.state-modes.test.tsx +119 -0
  166. package/src/ContextMenu/__tests__/ContextMenu.sub.test.tsx +262 -0
  167. package/src/ContextMenu/__tests__/ContextMenu.typeahead.test.tsx +89 -0
  168. package/src/ContextMenu/constants.ts +4 -0
  169. package/src/ContextMenu/index.ts +1 -0
  170. package/src/ContextMenu/types.ts +199 -0
  171. package/src/DirectionProvider/DirectionContext.ts +21 -0
  172. package/src/DirectionProvider/DirectionProvider.tsx +31 -0
  173. package/src/DirectionProvider/README.md +62 -0
  174. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +29 -0
  175. package/src/DirectionProvider/index.ts +3 -0
  176. package/src/DirectionProvider/types.ts +10 -0
  177. package/src/Divider/Divider.tsx +57 -0
  178. package/src/Divider/README.md +57 -0
  179. package/src/Divider/__tests__/Divider.test.tsx +41 -0
  180. package/src/Divider/index.ts +1 -0
  181. package/src/Divider/types.ts +5 -0
  182. package/src/Dropdown/Dropdown.tsx +842 -0
  183. package/src/Dropdown/DropdownContentContext.ts +15 -0
  184. package/src/Dropdown/DropdownContext.ts +17 -0
  185. package/src/Dropdown/DropdownGroupContext.ts +8 -0
  186. package/src/Dropdown/DropdownItemIndicatorContext.ts +13 -0
  187. package/src/Dropdown/DropdownRadioGroupContext.ts +9 -0
  188. package/src/Dropdown/DropdownSubContext.ts +15 -0
  189. package/src/Dropdown/README.md +284 -0
  190. package/src/Dropdown/__tests__/Dropdown.asChild.test.tsx +286 -0
  191. package/src/Dropdown/__tests__/Dropdown.basic-rendering.test.tsx +43 -0
  192. package/src/Dropdown/__tests__/Dropdown.checkbox-item.test.tsx +121 -0
  193. package/src/Dropdown/__tests__/Dropdown.disabled.test.tsx +143 -0
  194. package/src/Dropdown/__tests__/Dropdown.error-handling.test.tsx +85 -0
  195. package/src/Dropdown/__tests__/Dropdown.group-label.test.tsx +68 -0
  196. package/src/Dropdown/__tests__/Dropdown.item-indicator.test.tsx +260 -0
  197. package/src/Dropdown/__tests__/Dropdown.item.test.tsx +72 -0
  198. package/src/Dropdown/__tests__/Dropdown.keyboard-edge-cases.test.tsx +77 -0
  199. package/src/Dropdown/__tests__/Dropdown.keyboard-interaction.test.tsx +310 -0
  200. package/src/Dropdown/__tests__/Dropdown.mouse-interaction.test.tsx +347 -0
  201. package/src/Dropdown/__tests__/Dropdown.radio-item.test.tsx +134 -0
  202. package/src/Dropdown/__tests__/Dropdown.reading-direction.test.tsx +153 -0
  203. package/src/Dropdown/__tests__/Dropdown.separator.test.tsx +46 -0
  204. package/src/Dropdown/__tests__/Dropdown.state-modes.test.tsx +100 -0
  205. package/src/Dropdown/__tests__/Dropdown.sub.test.tsx +185 -0
  206. package/src/Dropdown/__tests__/Dropdown.trigger.test.tsx +110 -0
  207. package/src/Dropdown/__tests__/Dropdown.typeahead.test.tsx +133 -0
  208. package/src/Dropdown/constants.ts +4 -0
  209. package/src/Dropdown/hooks/index.ts +9 -0
  210. package/src/Dropdown/hooks/useCloseSiblingSub.ts +13 -0
  211. package/src/Dropdown/hooks/useDropdownContent.ts +162 -0
  212. package/src/Dropdown/hooks/useDropdownContext.ts +1 -0
  213. package/src/Dropdown/hooks/useDropdownGroup.ts +18 -0
  214. package/src/Dropdown/hooks/useDropdownItem.ts +49 -0
  215. package/src/Dropdown/hooks/useDropdownLabel.ts +15 -0
  216. package/src/Dropdown/hooks/useDropdownRoot.ts +57 -0
  217. package/src/Dropdown/hooks/useDropdownSubContext.ts +1 -0
  218. package/src/Dropdown/hooks/useDropdownTrigger.ts +31 -0
  219. package/src/Dropdown/index.ts +1 -0
  220. package/src/Dropdown/types.ts +200 -0
  221. package/src/EmptyState/EmptyState.tsx +245 -0
  222. package/src/EmptyState/README.md +129 -0
  223. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +32 -0
  224. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +30 -0
  225. package/src/EmptyState/__tests__/EmptyState.Media.test.tsx +34 -0
  226. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +28 -0
  227. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +26 -0
  228. package/src/EmptyState/index.ts +2 -0
  229. package/src/EmptyState/types.ts +21 -0
  230. package/src/Field/Field.tsx +239 -0
  231. package/src/Field/FieldContext.ts +22 -0
  232. package/src/Field/README.md +167 -0
  233. package/src/Field/__tests__/Field.asChild.test.tsx +83 -0
  234. package/src/Field/__tests__/Field.basic-rendering.test.tsx +104 -0
  235. package/src/Field/__tests__/Field.state-cascade.test.tsx +75 -0
  236. package/src/Field/hooks/index.ts +2 -0
  237. package/src/Field/hooks/useFieldContext.ts +1 -0
  238. package/src/Field/hooks/useFieldProps.ts +57 -0
  239. package/src/Field/index.ts +2 -0
  240. package/src/Field/types.ts +33 -0
  241. package/src/Fieldset/Fieldset.tsx +104 -0
  242. package/src/Fieldset/README.md +74 -0
  243. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +81 -0
  244. package/src/Fieldset/__tests__/Fieldset.disabled.test.tsx +41 -0
  245. package/src/Fieldset/index.ts +2 -0
  246. package/src/Fieldset/types.ts +5 -0
  247. package/src/Input/Input.tsx +120 -0
  248. package/src/Input/README.md +180 -0
  249. package/src/Input/__tests__/Input.asChild.test.tsx +85 -0
  250. package/src/Input/__tests__/Input.basic-rendering.test.tsx +118 -0
  251. package/src/Input/__tests__/Input.disabled.test.tsx +49 -0
  252. package/src/Input/__tests__/Input.field-integration.test.tsx +148 -0
  253. package/src/Input/index.ts +2 -0
  254. package/src/Input/types.ts +7 -0
  255. package/src/InputGroup/InputGroup.tsx +228 -0
  256. package/src/InputGroup/README.md +178 -0
  257. package/src/InputGroup/__tests__/InputGroup.asChild.test.tsx +109 -0
  258. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +106 -0
  259. package/src/InputGroup/index.ts +2 -0
  260. package/src/InputGroup/types.ts +13 -0
  261. package/src/MillerColumns/MillerColumns.tsx +329 -0
  262. package/src/MillerColumns/MillerColumnsContext.ts +25 -0
  263. package/src/MillerColumns/README.md +278 -0
  264. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +82 -0
  265. package/src/MillerColumns/__tests__/MillerColumns.asChild.test.tsx +106 -0
  266. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +68 -0
  267. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +52 -0
  268. package/src/MillerColumns/__tests__/MillerColumns.column-projection.test.tsx +161 -0
  269. package/src/MillerColumns/__tests__/MillerColumns.controlled-state.test.tsx +90 -0
  270. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +77 -0
  271. package/src/MillerColumns/__tests__/MillerColumns.disabled-items.test.tsx +65 -0
  272. package/src/MillerColumns/__tests__/MillerColumns.error-handling.test.tsx +57 -0
  273. package/src/MillerColumns/__tests__/MillerColumns.fixtures.ts +15 -0
  274. package/src/MillerColumns/__tests__/MillerColumns.item-indicator.test.tsx +57 -0
  275. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +181 -0
  276. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +47 -0
  277. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +137 -0
  278. package/src/MillerColumns/__tests__/MillerColumns.roving-tabindex.test.tsx +91 -0
  279. package/src/MillerColumns/__tests__/MillerColumns.selection.test.tsx +54 -0
  280. package/src/MillerColumns/__tests__/MillerColumns.uncontrolled-state.test.tsx +70 -0
  281. package/src/MillerColumns/hooks/index.ts +7 -0
  282. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +23 -0
  283. package/src/MillerColumns/hooks/useMillerColumnsColumnContext.ts +1 -0
  284. package/src/MillerColumns/hooks/useMillerColumnsContext.ts +1 -0
  285. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +157 -0
  286. package/src/MillerColumns/hooks/useMillerColumnsItemContext.ts +1 -0
  287. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +76 -0
  288. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  289. package/src/MillerColumns/index.ts +3 -0
  290. package/src/MillerColumns/types.ts +93 -0
  291. package/src/MillerColumns/useMillerColumnsSelection.ts +31 -0
  292. package/src/MillerColumns/utils.ts +75 -0
  293. package/src/Modal/Modal.tsx +474 -0
  294. package/src/Modal/ModalContext.ts +13 -0
  295. package/src/Modal/README.md +207 -0
  296. package/src/Modal/__tests__/Modal.accessibility.test.tsx +167 -0
  297. package/src/Modal/__tests__/Modal.asChild.test.tsx +162 -0
  298. package/src/Modal/__tests__/Modal.click-outside.test.tsx +115 -0
  299. package/src/Modal/__tests__/Modal.content.test.tsx +193 -0
  300. package/src/Modal/__tests__/Modal.controlled-state.test.tsx +120 -0
  301. package/src/Modal/__tests__/Modal.error-handling.test.tsx +30 -0
  302. package/src/Modal/__tests__/Modal.escape-hatches.test.tsx +99 -0
  303. package/src/Modal/__tests__/Modal.imperative-api.test.tsx +119 -0
  304. package/src/Modal/__tests__/Modal.nested.test.tsx +106 -0
  305. package/src/Modal/__tests__/Modal.overlay.test.tsx +99 -0
  306. package/src/Modal/__tests__/Modal.portal.test.tsx +90 -0
  307. package/src/Modal/__tests__/Modal.presence.test.tsx +111 -0
  308. package/src/Modal/__tests__/Modal.trigger.test.tsx +70 -0
  309. package/src/Modal/__tests__/Modal.uncontrolled-state.test.tsx +72 -0
  310. package/src/Modal/__tests__/dialog-polyfill.ts +40 -0
  311. package/src/Modal/hooks/index.ts +4 -0
  312. package/src/Modal/hooks/useModalContent.ts +62 -0
  313. package/src/Modal/hooks/useModalContext.ts +1 -0
  314. package/src/Modal/hooks/useModalRoot.ts +81 -0
  315. package/src/Modal/hooks/useModalTrigger.ts +25 -0
  316. package/src/Modal/index.ts +3 -0
  317. package/src/Modal/types.ts +76 -0
  318. package/src/Portal/Portal.tsx +28 -0
  319. package/src/Portal/README.md +70 -0
  320. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +17 -0
  321. package/src/Portal/index.ts +2 -0
  322. package/src/Portal/types.ts +6 -0
  323. package/src/Progress/Progress.tsx +178 -0
  324. package/src/Progress/ProgressContext.ts +15 -0
  325. package/src/Progress/README.md +112 -0
  326. package/src/Progress/__tests__/Progress.asChild.test.tsx +37 -0
  327. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +65 -0
  328. package/src/Progress/__tests__/Progress.error-handling.test.tsx +40 -0
  329. package/src/Progress/__tests__/Progress.fixtures.ts +7 -0
  330. package/src/Progress/__tests__/Progress.value.test.tsx +83 -0
  331. package/src/Progress/hooks/index.ts +2 -0
  332. package/src/Progress/hooks/useProgressContext.ts +1 -0
  333. package/src/Progress/hooks/useProgressRoot.ts +45 -0
  334. package/src/Progress/index.ts +3 -0
  335. package/src/Progress/types.ts +43 -0
  336. package/src/RadioCard/README.md +133 -0
  337. package/src/RadioCard/RadioCard.tsx +334 -0
  338. package/src/RadioCard/RadioCardContext.ts +23 -0
  339. package/src/RadioCard/RadioCardItemContext.ts +10 -0
  340. package/src/RadioCard/__tests__/RadioCard.asChild.test.tsx +76 -0
  341. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +87 -0
  342. package/src/RadioCard/__tests__/RadioCard.controlled-state.test.tsx +107 -0
  343. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +61 -0
  344. package/src/RadioCard/__tests__/RadioCard.error-handling.test.tsx +35 -0
  345. package/src/RadioCard/__tests__/RadioCard.indicator.test.tsx +119 -0
  346. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +158 -0
  347. package/src/RadioCard/__tests__/RadioCard.orientation.test.tsx +90 -0
  348. package/src/RadioCard/__tests__/RadioCard.reading-direction.test.tsx +65 -0
  349. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +108 -0
  350. package/src/RadioCard/hooks/index.ts +3 -0
  351. package/src/RadioCard/hooks/useRadioCardContext.ts +1 -0
  352. package/src/RadioCard/hooks/useRadioCardItemContext.ts +1 -0
  353. package/src/RadioCard/hooks/useRadioCardRoot.ts +77 -0
  354. package/src/RadioCard/index.ts +4 -0
  355. package/src/RadioCard/types.ts +51 -0
  356. package/src/RadioGroup/README.md +185 -0
  357. package/src/RadioGroup/RadioGroup.tsx +353 -0
  358. package/src/RadioGroup/RadioGroupContext.ts +23 -0
  359. package/src/RadioGroup/RadioGroupItemContext.ts +10 -0
  360. package/src/RadioGroup/__tests__/RadioGroup.asChild.test.tsx +105 -0
  361. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +72 -0
  362. package/src/RadioGroup/__tests__/RadioGroup.controlled-state.test.tsx +109 -0
  363. package/src/RadioGroup/__tests__/RadioGroup.disabled-keydown-guards.test.tsx +68 -0
  364. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +79 -0
  365. package/src/RadioGroup/__tests__/RadioGroup.error-handling.test.tsx +33 -0
  366. package/src/RadioGroup/__tests__/RadioGroup.indicator.test.tsx +85 -0
  367. package/src/RadioGroup/__tests__/RadioGroup.keyboard-interaction.test.tsx +135 -0
  368. package/src/RadioGroup/__tests__/RadioGroup.orientation.test.tsx +90 -0
  369. package/src/RadioGroup/__tests__/RadioGroup.reading-direction.test.tsx +65 -0
  370. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +45 -0
  371. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +105 -0
  372. package/src/RadioGroup/__tests__/RadioGroup.uncontrolled-state.test.tsx +96 -0
  373. package/src/RadioGroup/hooks/index.ts +3 -0
  374. package/src/RadioGroup/hooks/useRadioGroupContext.ts +1 -0
  375. package/src/RadioGroup/hooks/useRadioGroupItemContext.ts +1 -0
  376. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +87 -0
  377. package/src/RadioGroup/index.ts +1 -0
  378. package/src/RadioGroup/types.ts +51 -0
  379. package/src/Select/README.md +203 -0
  380. package/src/Select/Select.tsx +204 -0
  381. package/src/Select/__tests__/Select.asChild.test.tsx +36 -0
  382. package/src/Select/__tests__/Select.basic-rendering.test.tsx +17 -0
  383. package/src/Select/__tests__/Select.controlled-state.test.tsx +69 -0
  384. package/src/Select/__tests__/Select.data-attributes.test.tsx +29 -0
  385. package/src/Select/__tests__/Select.field-integration.test.tsx +150 -0
  386. package/src/Select/__tests__/Select.group.test.tsx +42 -0
  387. package/src/Select/__tests__/Select.placeholder.test.tsx +32 -0
  388. package/src/Select/index.ts +2 -0
  389. package/src/Select/types.ts +89 -0
  390. package/src/SkipNav/README.md +87 -0
  391. package/src/SkipNav/SkipNav.tsx +116 -0
  392. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +23 -0
  393. package/src/SkipNav/__tests__/SkipNav.ids.test.tsx +19 -0
  394. package/src/SkipNav/index.ts +1 -0
  395. package/src/SkipNav/types.ts +26 -0
  396. package/src/Slider/README.md +215 -0
  397. package/src/Slider/Slider.tsx +308 -0
  398. package/src/Slider/SliderContext.ts +24 -0
  399. package/src/Slider/__tests__/Slider.asChild.test.tsx +119 -0
  400. package/src/Slider/__tests__/Slider.basic-rendering.test.tsx +157 -0
  401. package/src/Slider/__tests__/Slider.controlled-state.test.tsx +78 -0
  402. package/src/Slider/__tests__/Slider.disabled.test.tsx +82 -0
  403. package/src/Slider/__tests__/Slider.error-handling.test.tsx +45 -0
  404. package/src/Slider/__tests__/Slider.fixtures.ts +53 -0
  405. package/src/Slider/__tests__/Slider.form.test.tsx +67 -0
  406. package/src/Slider/__tests__/Slider.inverted.test.tsx +112 -0
  407. package/src/Slider/__tests__/Slider.keyboard-interaction.test.tsx +118 -0
  408. package/src/Slider/__tests__/Slider.multiple-thumbs.test.tsx +84 -0
  409. package/src/Slider/__tests__/Slider.orientation.test.tsx +101 -0
  410. package/src/Slider/__tests__/Slider.pointer-interaction.test.tsx +205 -0
  411. package/src/Slider/__tests__/Slider.reading-direction.test.tsx +99 -0
  412. package/src/Slider/__tests__/Slider.uncontrolled-state.test.tsx +69 -0
  413. package/src/Slider/__tests__/Slider.value-commit.test.tsx +103 -0
  414. package/src/Slider/hooks/index.ts +3 -0
  415. package/src/Slider/hooks/useSliderContext.ts +1 -0
  416. package/src/Slider/hooks/useSliderRoot.ts +197 -0
  417. package/src/Slider/hooks/useSliderThumb.ts +77 -0
  418. package/src/Slider/index.ts +3 -0
  419. package/src/Slider/types.ts +48 -0
  420. package/src/Slider/utils.ts +155 -0
  421. package/src/Slot/Slot.tsx +158 -0
  422. package/src/Slot/__tests__/Slot.test.tsx +163 -0
  423. package/src/Slot/__tests__/composeEventHandlers.test.ts +74 -0
  424. package/src/Slot/composeEventHandlers.ts +38 -0
  425. package/src/Slot/index.ts +3 -0
  426. package/src/Slot/types.ts +5 -0
  427. package/src/Status/README.md +50 -0
  428. package/src/Status/Status.tsx +44 -0
  429. package/src/Status/__tests__/Status.test.tsx +28 -0
  430. package/src/Status/index.ts +2 -0
  431. package/src/Status/types.ts +5 -0
  432. package/src/Switch/README.md +121 -0
  433. package/src/Switch/Switch.tsx +167 -0
  434. package/src/Switch/SwitchContext.ts +10 -0
  435. package/src/Switch/__tests__/Switch.asChild.test.tsx +56 -0
  436. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +76 -0
  437. package/src/Switch/__tests__/Switch.contract.test.tsx +109 -0
  438. package/src/Switch/__tests__/Switch.controlled-state.test.tsx +79 -0
  439. package/src/Switch/__tests__/Switch.disabled.test.tsx +60 -0
  440. package/src/Switch/__tests__/Switch.error-handling.test.tsx +20 -0
  441. package/src/Switch/__tests__/Switch.keyboard-interaction.test.tsx +56 -0
  442. package/src/Switch/__tests__/Switch.thumb.test.tsx +84 -0
  443. package/src/Switch/__tests__/Switch.uncontrolled-state.test.tsx +83 -0
  444. package/src/Switch/hooks/index.ts +2 -0
  445. package/src/Switch/hooks/useSwitchContext.ts +1 -0
  446. package/src/Switch/hooks/useSwitchRoot.ts +28 -0
  447. package/src/Switch/index.ts +3 -0
  448. package/src/Switch/types.ts +37 -0
  449. package/src/Table/README.md +205 -0
  450. package/src/Table/Table.tsx +380 -0
  451. package/src/Table/__tests__/Table.Body.test.tsx +61 -0
  452. package/src/Table/__tests__/Table.Caption.test.tsx +70 -0
  453. package/src/Table/__tests__/Table.Cell.test.tsx +73 -0
  454. package/src/Table/__tests__/Table.Footer.test.tsx +61 -0
  455. package/src/Table/__tests__/Table.Head.test.tsx +61 -0
  456. package/src/Table/__tests__/Table.Header.test.tsx +73 -0
  457. package/src/Table/__tests__/Table.Root.test.tsx +49 -0
  458. package/src/Table/__tests__/Table.Row.test.tsx +67 -0
  459. package/src/Table/__tests__/Table.ScrollArea.test.tsx +83 -0
  460. package/src/Table/index.ts +1 -0
  461. package/src/Table/types.ts +63 -0
  462. package/src/Tabs/README.md +110 -0
  463. package/src/Tabs/Tabs.tsx +434 -0
  464. package/src/Tabs/TabsContext.ts +13 -0
  465. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +114 -0
  466. package/src/Tabs/__tests__/Tabs.asChild.test.tsx +91 -0
  467. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +483 -0
  468. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +133 -0
  469. package/src/Tabs/__tests__/Tabs.controlled-state.test.tsx +152 -0
  470. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +203 -0
  471. package/src/Tabs/__tests__/Tabs.error-handling.test.tsx +82 -0
  472. package/src/Tabs/__tests__/Tabs.fixtures.ts +171 -0
  473. package/src/Tabs/__tests__/Tabs.imperative-api.test.tsx +118 -0
  474. package/src/Tabs/__tests__/Tabs.keyboard-interaction.test.tsx +192 -0
  475. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +61 -0
  476. package/src/Tabs/__tests__/Tabs.mouse-interaction.test.tsx +216 -0
  477. package/src/Tabs/__tests__/Tabs.reading-direction.test.tsx +58 -0
  478. package/src/Tabs/__tests__/Tabs.uncontrolled-state.test.tsx +197 -0
  479. package/src/Tabs/hooks/index.ts +4 -0
  480. package/src/Tabs/hooks/useTabsContent.ts +27 -0
  481. package/src/Tabs/hooks/useTabsContext.ts +1 -0
  482. package/src/Tabs/hooks/useTabsRoot.ts +148 -0
  483. package/src/Tabs/hooks/useTabsTrigger.ts +111 -0
  484. package/src/Tabs/index.ts +3 -0
  485. package/src/Tabs/types.ts +99 -0
  486. package/src/Tabs/utils.ts +8 -0
  487. package/src/Textarea/README.md +98 -0
  488. package/src/Textarea/Textarea.tsx +93 -0
  489. package/src/Textarea/__tests__/Textarea.asChild.test.tsx +85 -0
  490. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +107 -0
  491. package/src/Textarea/__tests__/Textarea.disabled.test.tsx +49 -0
  492. package/src/Textarea/__tests__/Textarea.field-integration.test.tsx +134 -0
  493. package/src/Textarea/index.ts +2 -0
  494. package/src/Textarea/types.ts +7 -0
  495. package/src/Toggle/README.md +97 -0
  496. package/src/Toggle/Toggle.tsx +81 -0
  497. package/src/Toggle/__tests__/Toggle.asChild.test.tsx +42 -0
  498. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +28 -0
  499. package/src/Toggle/__tests__/Toggle.controlled-state.test.tsx +60 -0
  500. package/src/Toggle/__tests__/Toggle.disabled.test.tsx +34 -0
  501. package/src/Toggle/__tests__/Toggle.keyboard-interaction.test.tsx +42 -0
  502. package/src/Toggle/__tests__/Toggle.uncontrolled-state.test.tsx +40 -0
  503. package/src/Toggle/index.ts +2 -0
  504. package/src/Toggle/types.ts +23 -0
  505. package/src/ToggleGroup/README.md +137 -0
  506. package/src/ToggleGroup/ToggleGroup.tsx +298 -0
  507. package/src/ToggleGroup/ToggleGroupContext.ts +9 -0
  508. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +65 -0
  509. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +50 -0
  510. package/src/ToggleGroup/__tests__/ToggleGroup.disabled.test.tsx +54 -0
  511. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +151 -0
  512. package/src/ToggleGroup/__tests__/ToggleGroup.multiple-mode.test.tsx +144 -0
  513. package/src/ToggleGroup/__tests__/ToggleGroup.reading-direction.test.tsx +28 -0
  514. package/src/ToggleGroup/__tests__/ToggleGroup.single-mode.test.tsx +139 -0
  515. package/src/ToggleGroup/hooks/index.ts +2 -0
  516. package/src/ToggleGroup/hooks/useToggleGroupContext.ts +1 -0
  517. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +110 -0
  518. package/src/ToggleGroup/index.ts +2 -0
  519. package/src/ToggleGroup/types.ts +72 -0
  520. package/src/Tooltip/README.md +214 -0
  521. package/src/Tooltip/Tooltip.tsx +260 -0
  522. package/src/Tooltip/TooltipContext.ts +20 -0
  523. package/src/Tooltip/__tests__/Tooltip.asChild.test.tsx +77 -0
  524. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +180 -0
  525. package/src/Tooltip/__tests__/Tooltip.controlled-state.test.tsx +128 -0
  526. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  527. package/src/Tooltip/__tests__/Tooltip.focus-interaction.test.tsx +88 -0
  528. package/src/Tooltip/__tests__/Tooltip.hover-interaction.test.tsx +179 -0
  529. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +85 -0
  530. package/src/Tooltip/__tests__/Tooltip.uncontrolled-state.test.tsx +67 -0
  531. package/src/Tooltip/hooks/index.ts +4 -0
  532. package/src/Tooltip/hooks/useTooltipContent.ts +53 -0
  533. package/src/Tooltip/hooks/useTooltipProvider.ts +41 -0
  534. package/src/Tooltip/hooks/useTooltipRoot.ts +106 -0
  535. package/src/Tooltip/hooks/useTooltipTrigger.ts +44 -0
  536. package/src/Tooltip/index.ts +1 -0
  537. package/src/Tooltip/types.ts +64 -0
  538. package/src/Tree/README.md +339 -0
  539. package/src/Tree/Tree.tsx +571 -0
  540. package/src/Tree/TreeContext.ts +24 -0
  541. package/src/Tree/__tests__/Tree.aria.test.tsx +53 -0
  542. package/src/Tree/__tests__/Tree.asChild.test.tsx +134 -0
  543. package/src/Tree/__tests__/Tree.basic-rendering.test.tsx +111 -0
  544. package/src/Tree/__tests__/Tree.branch-behaviour.test.tsx +87 -0
  545. package/src/Tree/__tests__/Tree.controlled-expansion.test.tsx +92 -0
  546. package/src/Tree/__tests__/Tree.data-attributes.test.tsx +88 -0
  547. package/src/Tree/__tests__/Tree.disabled-items.test.tsx +196 -0
  548. package/src/Tree/__tests__/Tree.error-handling.test.tsx +71 -0
  549. package/src/Tree/__tests__/Tree.forceMount.test.tsx +72 -0
  550. package/src/Tree/__tests__/Tree.keyboard-interaction.test.tsx +150 -0
  551. package/src/Tree/__tests__/Tree.multiple-selection.test.tsx +151 -0
  552. package/src/Tree/__tests__/Tree.range-selection.test.tsx +200 -0
  553. package/src/Tree/__tests__/Tree.recursion-depth.test.tsx +73 -0
  554. package/src/Tree/__tests__/Tree.roving-tabindex.test.tsx +117 -0
  555. package/src/Tree/__tests__/Tree.selection-path.test.tsx +404 -0
  556. package/src/Tree/__tests__/Tree.single-selection.test.tsx +108 -0
  557. package/src/Tree/__tests__/Tree.uncontrolled-expansion.test.tsx +69 -0
  558. package/src/Tree/hooks/index.ts +3 -0
  559. package/src/Tree/hooks/useTreeItemKeyboard.ts +86 -0
  560. package/src/Tree/hooks/useTreePath.ts +68 -0
  561. package/src/Tree/hooks/useTreeRoot.ts +279 -0
  562. package/src/Tree/index.ts +3 -0
  563. package/src/Tree/types.ts +224 -0
  564. package/src/Tree/utils.ts +59 -0
  565. package/src/VisuallyHidden/README.md +58 -0
  566. package/src/VisuallyHidden/VisuallyHidden.tsx +67 -0
  567. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +59 -0
  568. package/src/VisuallyHidden/index.ts +2 -0
  569. package/src/VisuallyHidden/types.ts +5 -0
  570. package/src/hooks/index.ts +3 -0
  571. package/src/hooks/useCollection.ts +74 -0
  572. package/src/hooks/useControllableState.ts +81 -0
  573. package/src/hooks/useRovingTabindex.ts +178 -0
  574. package/src/index.ts +38 -0
  575. package/src/test/intersectionObserverPolyfill.ts +83 -0
  576. package/src/test/popoverPolyfill.ts +86 -0
  577. package/src/test/scrollPolyfill.ts +23 -0
  578. package/src/types.ts +13 -0
  579. package/src/utils/__tests__/createStrictContext.test.tsx +69 -0
  580. package/src/utils/__tests__/deriveId.test.ts +28 -0
  581. package/src/utils/__tests__/getKeyToActionMap.test.ts +106 -0
  582. package/src/utils/createStrictContext.ts +49 -0
  583. package/src/utils/deriveId.ts +31 -0
  584. package/src/utils/getKeyToActionMap.ts +95 -0
  585. package/src/utils/index.ts +3 -0
@@ -0,0 +1,77 @@
1
+ import { useCallback, useEffect, useId, useRef } from "react";
2
+ import type { KeyboardEvent } from "react";
3
+
4
+ import { useSliderContext } from "../SliderContext";
5
+ import { getKeyAction, getThumbStyle, snapToStep } from "../utils";
6
+
7
+ export function useSliderThumb() {
8
+ const {
9
+ values,
10
+ min,
11
+ max,
12
+ step,
13
+ orientation,
14
+ dir,
15
+ inverted,
16
+ disabled,
17
+ registerThumb,
18
+ orderedThumbIds,
19
+ setThumbValue,
20
+ commit,
21
+ } = useSliderContext();
22
+ const id = useId();
23
+ const ref = useRef<HTMLSpanElement>(null);
24
+
25
+ useEffect(() => {
26
+ registerThumb(id, ref.current);
27
+ return () => registerThumb(id, null);
28
+ }, [id, registerThumb]);
29
+
30
+ const registeredIndex = orderedThumbIds.indexOf(id);
31
+ const index = registeredIndex === -1 ? 0 : registeredIndex;
32
+ const value = values[index];
33
+ const style = getThumbStyle(value, min, max, { orientation, dir, inverted });
34
+
35
+ const onKeyDown = useCallback(
36
+ (event: KeyboardEvent<HTMLSpanElement>) => {
37
+ if (disabled) {
38
+ return;
39
+ }
40
+ const action = getKeyAction(event.key, { orientation, dir, inverted });
41
+ if (action === null) {
42
+ return;
43
+ }
44
+ let target: number;
45
+ if (action === "min") {
46
+ target = min;
47
+ } else if (action === "max") {
48
+ target = max;
49
+ } else {
50
+ const isPageKey = event.key === "PageUp" || event.key === "PageDown";
51
+ const magnitude = isPageKey ? step * 10 : step;
52
+ const delta = action === "increase" ? magnitude : -magnitude;
53
+ target = snapToStep(value + delta, min, step);
54
+ }
55
+ event.preventDefault();
56
+ const next = setThumbValue(index, target);
57
+ if (next) {
58
+ commit(next);
59
+ }
60
+ },
61
+ [
62
+ value,
63
+ index,
64
+ min,
65
+ max,
66
+ step,
67
+ orientation,
68
+ dir,
69
+ inverted,
70
+ disabled,
71
+ setThumbValue,
72
+ commit,
73
+ ],
74
+ );
75
+
76
+ return { ref, value, min, max, orientation, disabled, style, onKeyDown };
77
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./Slider";
2
+ export * from "./SliderContext";
3
+ export * from "./types";
@@ -0,0 +1,48 @@
1
+ import type { ComponentProps } from "react";
2
+
3
+ export type SliderOrientation = "horizontal" | "vertical";
4
+ export type SliderDirection = "ltr" | "rtl";
5
+
6
+ type SliderRootSharedProps = Omit<
7
+ ComponentProps<"span">,
8
+ "defaultValue" | "dir"
9
+ > & {
10
+ min?: number;
11
+ max?: number;
12
+ step?: number;
13
+ minStepsBetweenThumbs?: number;
14
+ orientation?: SliderOrientation;
15
+ dir?: SliderDirection;
16
+ inverted?: boolean;
17
+ disabled?: boolean;
18
+ name?: string;
19
+ asChild?: boolean;
20
+ };
21
+
22
+ type SliderRootUncontrolledProps = SliderRootSharedProps & {
23
+ defaultValue?: number[];
24
+ value?: never;
25
+ onValueChange?: (value: number[]) => void;
26
+ onValueCommit?: (value: number[]) => void;
27
+ };
28
+
29
+ type SliderRootControlledProps = SliderRootSharedProps & {
30
+ defaultValue?: never;
31
+ value: number[];
32
+ onValueChange?: (value: number[]) => void;
33
+ onValueCommit?: (value: number[]) => void;
34
+ };
35
+
36
+ export type SliderRootProps =
37
+ | SliderRootUncontrolledProps
38
+ | SliderRootControlledProps;
39
+
40
+ export type SliderTrackProps = ComponentProps<"span"> & {
41
+ asChild?: boolean;
42
+ };
43
+ export type SliderRangeProps = ComponentProps<"span"> & {
44
+ asChild?: boolean;
45
+ };
46
+ export type SliderThumbProps = ComponentProps<"span"> & {
47
+ asChild?: boolean;
48
+ };
@@ -0,0 +1,155 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ import type { SliderDirection, SliderOrientation } from "./types";
4
+
5
+ type Edge = "left" | "right" | "top" | "bottom";
6
+
7
+ export type SliderAxisArgs = {
8
+ orientation: SliderOrientation;
9
+ dir: SliderDirection;
10
+ inverted: boolean;
11
+ };
12
+
13
+ export type SliderKeyAction = "increase" | "decrease" | "min" | "max";
14
+
15
+ const OPPOSITE_EDGE: Record<Edge, Edge> = {
16
+ left: "right",
17
+ right: "left",
18
+ top: "bottom",
19
+ bottom: "top",
20
+ };
21
+
22
+ /** Constrain `value` to the inclusive `[min, max]` range. */
23
+ export function clamp(value: number, min: number, max: number): number {
24
+ return Math.min(Math.max(value, min), max);
25
+ }
26
+
27
+ /** Map a value onto its 0–100 position within the `[min, max]` range. */
28
+ export function valueToPercent(value: number, min: number, max: number): number {
29
+ return clamp(((value - min) / (max - min)) * 100, 0, 100);
30
+ }
31
+
32
+ /** Round a value to the nearest step, anchored at `min`, at step precision. */
33
+ export function snapToStep(value: number, min: number, step: number): number {
34
+ const snapped = min + Math.round((value - min) / step) * step;
35
+ const decimals = (String(step).split(".")[1] ?? "").length;
36
+ return Number(snapped.toFixed(decimals));
37
+ }
38
+
39
+ /**
40
+ * Resolve which physical edge a thumb's offset is anchored to, accounting
41
+ * for orientation, reading direction, and the `inverted` flag.
42
+ */
43
+ export function getOffsetEdge({ orientation, dir, inverted }: SliderAxisArgs): Edge {
44
+ const base: Edge =
45
+ orientation === "vertical" ? "bottom" : dir === "rtl" ? "right" : "left";
46
+ return inverted ? OPPOSITE_EDGE[base] : base;
47
+ }
48
+
49
+ /** Inline style positioning a thumb at its value along the track. */
50
+ export function getThumbStyle(
51
+ value: number,
52
+ min: number,
53
+ max: number,
54
+ edgeArgs: SliderAxisArgs,
55
+ ): CSSProperties {
56
+ const edge = getOffsetEdge(edgeArgs);
57
+ return { position: "absolute", [edge]: `${valueToPercent(value, min, max)}%` };
58
+ }
59
+
60
+ /**
61
+ * Inline style stretching the range between the lowest and highest thumb
62
+ * (or from the track start to the single thumb).
63
+ */
64
+ export function getRangeStyle(
65
+ values: number[],
66
+ min: number,
67
+ max: number,
68
+ edgeArgs: SliderAxisArgs,
69
+ ): CSSProperties {
70
+ const edge = getOffsetEdge(edgeArgs);
71
+ const percents = values.map((value) => valueToPercent(value, min, max));
72
+ const startPercent = values.length > 1 ? Math.min(...percents) : 0;
73
+ const endPercent = Math.max(...percents);
74
+ return {
75
+ position: "absolute",
76
+ [edge]: `${startPercent}%`,
77
+ [OPPOSITE_EDGE[edge]]: `${100 - endPercent}%`,
78
+ };
79
+ }
80
+
81
+ type PointerValueArgs = SliderAxisArgs & {
82
+ min: number;
83
+ max: number;
84
+ step: number;
85
+ };
86
+
87
+ /** Resolve the value at a pointer's position on the track. */
88
+ export function getPointerValue(
89
+ clientX: number,
90
+ clientY: number,
91
+ rect: { left: number; width: number; bottom: number; height: number },
92
+ { min, max, step, orientation, dir, inverted }: PointerValueArgs,
93
+ ): number {
94
+ let percent =
95
+ orientation === "vertical"
96
+ ? (rect.bottom - clientY) / rect.height
97
+ : (clientX - rect.left) / rect.width;
98
+ const reversed =
99
+ orientation === "vertical" ? inverted : (dir === "ltr") === inverted;
100
+ if (reversed) {
101
+ percent = 1 - percent;
102
+ }
103
+ percent = clamp(percent, 0, 1);
104
+ return clamp(snapToStep(min + percent * (max - min), min, step), min, max);
105
+ }
106
+
107
+ /**
108
+ * Map a key press to an abstract value action, accounting for orientation,
109
+ * reading direction, and the `inverted` flag. Returns `null` for keys the
110
+ * slider does not handle.
111
+ */
112
+ export function getKeyAction(
113
+ key: string,
114
+ { orientation, dir, inverted }: SliderAxisArgs,
115
+ ): SliderKeyAction | null {
116
+ switch (key) {
117
+ case "Home":
118
+ return "min";
119
+ case "End":
120
+ return "max";
121
+ case "PageUp":
122
+ return "increase";
123
+ case "PageDown":
124
+ return "decrease";
125
+ case "ArrowUp":
126
+ case "ArrowDown": {
127
+ const upIncreases = orientation === "vertical" ? !inverted : true;
128
+ const increases = key === "ArrowUp" ? upIncreases : !upIncreases;
129
+ return increases ? "increase" : "decrease";
130
+ }
131
+ case "ArrowRight":
132
+ case "ArrowLeft": {
133
+ const rightIncreases =
134
+ orientation === "vertical" ? true : (dir === "ltr") !== inverted;
135
+ const increases = key === "ArrowRight" ? rightIncreases : !rightIncreases;
136
+ return increases ? "increase" : "decrease";
137
+ }
138
+ default:
139
+ return null;
140
+ }
141
+ }
142
+
143
+ /** Index of the thumb whose value sits nearest to `value` (ties favour the first). */
144
+ export function getClosestThumbIndex(value: number, values: number[]): number {
145
+ let closestIndex = 0;
146
+ let smallestDistance = Infinity;
147
+ values.forEach((thumbValue, index) => {
148
+ const distance = Math.abs(thumbValue - value);
149
+ if (distance < smallestDistance) {
150
+ smallestDistance = distance;
151
+ closestIndex = index;
152
+ }
153
+ });
154
+ return closestIndex;
155
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Slot — a React composition utility following the asChild pattern.
3
+ *
4
+ * When a component renders `<Slot {...ownProps}>{child}</Slot>`, the child
5
+ * element is cloned with ownProps merged in:
6
+ * - Event handlers are **composed** (child's handler runs first, then Slot's).
7
+ * - `style` objects are shallow-merged (child wins on collisions).
8
+ * - `className` strings are concatenated.
9
+ * - All other props default to the child's value, with Slot providing the
10
+ * fallback when the child doesn't specify one.
11
+ * - Refs from both sides are composed via {@link composeRefs}.
12
+ *
13
+ * This file is intentionally self-contained — no external dependencies
14
+ * beyond React itself.
15
+ */
16
+
17
+ import {
18
+ forwardRef,
19
+ isValidElement,
20
+ cloneElement,
21
+ Children,
22
+ RefObject,
23
+ } from "react";
24
+ import { SlotProps } from "./types";
25
+ import { AnyProps, PossibleRef } from "../types";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // composeRefs
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function setRef<T>(ref: PossibleRef<T>, value: T) {
32
+ if (typeof ref === "function") {
33
+ ref(value);
34
+ } else if (ref !== null && ref !== undefined) {
35
+ (ref as RefObject<T>).current = value;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Combines multiple refs into a single callback ref that sets all of them
41
+ * simultaneously. Handles function refs, object refs (`{ current }`), and
42
+ * `undefined` — any of which may be mixed freely.
43
+ *
44
+ * @example Compose an internal ref with a consumer-supplied external ref:
45
+ * ```tsx
46
+ * const internalRef = useRef<HTMLButtonElement>(null);
47
+ * const composedRef = externalRef
48
+ * ? composeRefs(internalRef, externalRef)
49
+ * : internalRef;
50
+ *
51
+ * return <button ref={composedRef} />;
52
+ * ```
53
+ */
54
+ export function composeRefs<T>(...refs: PossibleRef<T>[]) {
55
+ return (node: T) => refs.forEach((ref) => setRef(ref, node));
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // mergeProps — follows the asChild composition pattern rules exactly
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function getRef(element: React.ReactElement): PossibleRef<unknown> {
63
+ // React 19 exposes ref via props; React ≤18 via element.ref
64
+ const { props, ref } = element as React.ReactElement & {
65
+ ref?: PossibleRef<unknown>;
66
+ };
67
+ return (props as AnyProps).ref !== undefined
68
+ ? ((props as AnyProps).ref as PossibleRef<unknown>)
69
+ : ref;
70
+ }
71
+
72
+ function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
73
+ // Child props override by default; event handlers and style/className are
74
+ // special-cased.
75
+ const merged = { ...childProps };
76
+
77
+ for (const key in childProps) {
78
+ const slotVal = slotProps[key];
79
+ const childVal = childProps[key];
80
+ const isEventHandler = /^on[A-Z]/.test(key);
81
+
82
+ if (isEventHandler) {
83
+ if (slotVal && childVal) {
84
+ // Both sides provide a handler — compose: child first, then slot.
85
+ merged[key] = (...args: unknown[]) => {
86
+ (childVal as (...a: unknown[]) => unknown)(...args);
87
+ (slotVal as (...a: unknown[]) => unknown)(...args);
88
+ };
89
+ } else if (slotVal) {
90
+ merged[key] = slotVal;
91
+ }
92
+ } else if (key === "style") {
93
+ merged[key] = { ...(slotVal as object), ...(childVal as object) };
94
+ } else if (key === "className") {
95
+ merged[key] = [slotVal, childVal].filter(Boolean).join(" ");
96
+ }
97
+ }
98
+
99
+ return { ...slotProps, ...merged };
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Slot component
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Renders its single child element with the Slot's own props merged in.
108
+ *
109
+ * Used to implement the `asChild` pattern: a component that normally renders
110
+ * its own DOM element can instead delegate to a consumer-supplied element
111
+ * while preserving all of its own behaviour (ARIA attributes, event handlers,
112
+ * `ref`, etc.).
113
+ *
114
+ * **Prop-merging rules**:
115
+ * - **Event handlers** compose — child's handler fires first, then Slot's.
116
+ * - **`style`** is shallow-merged — child wins on key collisions.
117
+ * - **`className`** strings are concatenated (`slotClass childClass`).
118
+ * - **All other props** default to the child's value; Slot provides the
119
+ * fallback when the child doesn't specify one.
120
+ * - **Refs** from both sides are composed via {@link composeRefs}.
121
+ *
122
+ * **Constraints**
123
+ * - Exactly one React element child is required; Slot throws otherwise.
124
+ * - The child must accept a `ref` (i.e. a DOM element or a `forwardRef`
125
+ * component).
126
+ *
127
+ * **React version compatibility.** Slot reads the child's ref from
128
+ * `element.props.ref` (React 19+) with a fallback to `element.ref`
129
+ * (React ≤18) so both runtime versions compose refs correctly.
130
+ *
131
+ * @example
132
+ * ```tsx
133
+ * // Inside a component that normally renders <button>:
134
+ * if (asChild) {
135
+ * return <Slot {...buttonProps}>{children}</Slot>;
136
+ * }
137
+ * return <button {...buttonProps}>{children}</button>;
138
+ * ```
139
+ */
140
+ export const Slot = forwardRef<HTMLElement, SlotProps>(
141
+ ({ children, ...slotProps }, forwardedRef) => {
142
+ if (Children.count(children) !== 1 || !isValidElement(children)) {
143
+ throw new Error("Slot requires exactly one React element child.");
144
+ }
145
+
146
+ const childRef = getRef(children);
147
+ const composedRef = forwardedRef
148
+ ? composeRefs(forwardedRef, childRef)
149
+ : childRef;
150
+
151
+ return cloneElement(children, {
152
+ ...mergeProps(slotProps as AnyProps, children.props as AnyProps),
153
+ ref: composedRef,
154
+ } as AnyProps);
155
+ },
156
+ );
157
+
158
+ Slot.displayName = "Slot";
@@ -0,0 +1,163 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import React, { createRef } from "react";
4
+
5
+ import { Slot } from "../Slot";
6
+
7
+ describe("Slot tests", () => {
8
+ describe("error handling", () => {
9
+ it("throws when given zero children", () => {
10
+ // Arrange & Assert
11
+ expect(() => {
12
+ render(<Slot />);
13
+ }).toThrow("Slot requires exactly one React element child.");
14
+ });
15
+
16
+ it("throws when given a non-element child (string)", () => {
17
+ expect(() => {
18
+ // Arrange & Assert
19
+ render(<Slot>{"plain text"}</Slot>);
20
+ }).toThrow("Slot requires exactly one React element child.");
21
+ });
22
+ });
23
+
24
+ describe("mergeProps — event handlers", () => {
25
+ it("uses the slot handler when only the slot provides one (key absent from child)", async () => {
26
+ // Arrange
27
+ const user = userEvent.setup();
28
+ const slotClick = vi.fn();
29
+ render(
30
+ <Slot onClick={slotClick}>
31
+ {/* child has no onClick key at all */}
32
+ <button type="button">Click me</button>
33
+ </Slot>,
34
+ );
35
+
36
+ // Act
37
+ await user.click(screen.getByRole("button"));
38
+
39
+ // Assert
40
+ expect(slotClick).toHaveBeenCalledTimes(1);
41
+ });
42
+
43
+ it("uses the slot handler when child has the key but sets it to undefined", async () => {
44
+ // Arrange
45
+ const user = userEvent.setup();
46
+ const slotClick = vi.fn();
47
+ render(
48
+ <Slot onClick={slotClick}>
49
+ {/* child explicitly passes onClick={undefined} — key is present but falsy */}
50
+ <button type="button" onClick={undefined}>
51
+ Click me
52
+ </button>
53
+ </Slot>,
54
+ );
55
+
56
+ // Act
57
+ await user.click(screen.getByRole("button"));
58
+
59
+ // Assert
60
+ expect(slotClick).toHaveBeenCalledTimes(1);
61
+ });
62
+
63
+ it("preserves the child handler unchanged when the slot has no handler for that key", async () => {
64
+ // Arrange
65
+ const user = userEvent.setup();
66
+ const childClick = vi.fn();
67
+ render(
68
+ // Slot provides no onClick — child's handler should fire unaffected
69
+ <Slot>
70
+ <button type="button" onClick={childClick}>
71
+ Click me
72
+ </button>
73
+ </Slot>,
74
+ );
75
+
76
+ // Act
77
+ await user.click(screen.getByRole("button"));
78
+
79
+ // Assert
80
+ expect(childClick).toHaveBeenCalledTimes(1);
81
+ });
82
+ });
83
+
84
+ describe("mergeProps — style", () => {
85
+ it("shallow-merges style objects, child wins on collisions", () => {
86
+ // Arrange
87
+ const { container } = render(
88
+ <Slot style={{ color: "red", fontWeight: "bold" }}>
89
+ <span style={{ color: "blue" }}>text</span>
90
+ </Slot>,
91
+ );
92
+ const el = container.firstChild as HTMLElement;
93
+
94
+ // Assert
95
+ expect(el.style.color).toBe("blue"); // child wins on collision
96
+ expect(el.style.fontWeight).toBe("bold"); // slot contributes non-colliding prop
97
+ });
98
+ });
99
+
100
+ describe("getRef", () => {
101
+ it("reads ref from props.ref when present (React 19: child element carries its own ref)", () => {
102
+ // Arrange
103
+ // In React 19, a ref passed directly to a child element lands in props.ref.
104
+ // getRef must return it so composeRefs can wire it together with the Slot's
105
+ // own forwardedRef — otherwise the child's ref is silently dropped.
106
+ const childRef = createRef<HTMLDivElement>();
107
+ const slotRef = createRef<HTMLElement>();
108
+ const SlotWithRef = React.forwardRef<HTMLElement>((_props, ref) => (
109
+ // child element carries its own ref — this exercises the props.ref branch
110
+ <Slot ref={ref}>
111
+ <div ref={childRef}>text</div>
112
+ </Slot>
113
+ ));
114
+ render(<SlotWithRef ref={slotRef} />);
115
+
116
+ // Assert
117
+ // Both refs populated: childRef via props.ref (React 19 path in getRef),
118
+ // slotRef via the Slot's forwardedRef composed together.
119
+ expect(slotRef.current).not.toBeNull();
120
+ expect(childRef.current).not.toBeNull();
121
+ expect(childRef.current?.tagName).toBe("DIV");
122
+ });
123
+
124
+ it("falls back to element.ref when props.ref is absent (React ≤18 element structure)", () => {
125
+ // Arrange
126
+ // In React ≤18, ref is on element.ref and absent from element.props.
127
+ // getRef must fall back to element.ref so composeRefs can wire both refs.
128
+ const childRef = createRef<HTMLDivElement>();
129
+ const slotRef = createRef<HTMLElement>();
130
+ // Use a real element as the base so $$typeof matches the runtime, then
131
+ // overlay the React ≤18 shape: ref on the element object, absent from props.
132
+ const baseElement = React.createElement("div", null, "text");
133
+ const react18Element = {
134
+ ...baseElement,
135
+ ref: childRef, // React ≤18: ref lives here …
136
+ props: { children: "text" }, // … not in props
137
+ } as unknown as React.ReactElement;
138
+ const SlotWithRef = React.forwardRef<HTMLElement>((_props, ref) => (
139
+ <Slot ref={ref}>{react18Element}</Slot>
140
+ ));
141
+ render(<SlotWithRef ref={slotRef} />);
142
+
143
+ // Assert
144
+ expect(slotRef.current).not.toBeNull();
145
+ expect(childRef.current).not.toBeNull();
146
+ expect(childRef.current?.tagName).toBe("DIV");
147
+ });
148
+ });
149
+
150
+ describe("mergeProps — className", () => {
151
+ it("concatenates slot and child classNames", () => {
152
+ // Arrange
153
+ render(
154
+ <Slot className="slot-class">
155
+ <span className="child-class">text</span>
156
+ </Slot>,
157
+ );
158
+
159
+ // Assert
160
+ expect(screen.getByText("text")).toHaveClass("slot-class", "child-class");
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,74 @@
1
+ import { composeEventHandlers } from "../composeEventHandlers";
2
+
3
+ describe("composeEventHandlers", () => {
4
+ it("returns a function that calls the consumer's handler first, then ours", () => {
5
+ // Arrange
6
+ const calls: string[] = [];
7
+ const theirs = () => calls.push("theirs");
8
+ const ours = () => calls.push("ours");
9
+ const handler = composeEventHandlers(theirs, ours);
10
+
11
+ // Act
12
+ handler({} as Event);
13
+
14
+ // Assert
15
+ expect(calls).toEqual(["theirs", "ours"]);
16
+ });
17
+
18
+ it("passes the event through to both handlers unchanged", () => {
19
+ // Arrange
20
+ const theirs = vi.fn();
21
+ const ours = vi.fn();
22
+ const handler = composeEventHandlers(theirs, ours);
23
+ const event = { type: "click" } as unknown as Event;
24
+
25
+ // Act
26
+ handler(event);
27
+
28
+ // Assert
29
+ expect(theirs).toHaveBeenCalledWith(event);
30
+ expect(ours).toHaveBeenCalledWith(event);
31
+ });
32
+
33
+ it("still calls our handler when the consumer's handler is undefined", () => {
34
+ // Arrange
35
+ const ours = vi.fn();
36
+ const handler = composeEventHandlers<Event>(undefined, ours);
37
+
38
+ // Act
39
+ handler({} as Event);
40
+
41
+ // Assert
42
+ expect(ours).toHaveBeenCalledTimes(1);
43
+ });
44
+
45
+ it("skips our handler when the consumer called event.preventDefault()", () => {
46
+ // Arrange
47
+ const theirs = (event: Event) => event.preventDefault();
48
+ const ours = vi.fn();
49
+ const handler = composeEventHandlers(theirs, ours);
50
+ const event = new Event("click", { cancelable: true });
51
+
52
+ // Act
53
+ handler(event);
54
+
55
+ // Assert
56
+ expect(ours).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it("still runs our handler after preventDefault when checkForDefaultPrevented is false", () => {
60
+ // Arrange
61
+ const theirs = (event: Event) => event.preventDefault();
62
+ const ours = vi.fn();
63
+ const handler = composeEventHandlers(theirs, ours, {
64
+ checkForDefaultPrevented: false,
65
+ });
66
+ const event = new Event("click", { cancelable: true });
67
+
68
+ // Act
69
+ handler(event);
70
+
71
+ // Assert
72
+ expect(ours).toHaveBeenCalledTimes(1);
73
+ });
74
+ });