@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
@@ -132,8 +132,8 @@ describe("Slot tests", () => {
132
132
  const baseElement = React.createElement("div", null, "text");
133
133
  const react18Element = {
134
134
  ...baseElement,
135
- ref: childRef, // React ≤18: ref lives here …
136
- props: { children: "text" }, // … not in props
135
+ ref: childRef, // React ≤18: ref lives here ...
136
+ props: { children: "text" }, // ... not in props
137
137
  } as unknown as React.ReactElement;
138
138
  const SlotWithRef = React.forwardRef<HTMLElement>((_props, ref) => (
139
139
  <Slot ref={ref}>{react18Element}</Slot>
@@ -159,5 +159,115 @@ describe("Slot tests", () => {
159
159
  // Assert
160
160
  expect(screen.getByText("text")).toHaveClass("slot-class", "child-class");
161
161
  });
162
+
163
+ it("drops a falsy className from either side (no stray whitespace)", () => {
164
+ // Arrange — child provides an empty className; the filter must remove it.
165
+ const { container } = render(
166
+ <Slot className="slot-class">
167
+ <span className="">text</span>
168
+ </Slot>,
169
+ );
170
+
171
+ // Assert — exactly "slot-class", not "slot-class " with a trailing space
172
+ expect((container.firstChild as HTMLElement).getAttribute("class")).toBe(
173
+ "slot-class",
174
+ );
175
+ });
176
+ });
177
+
178
+ describe("ref composition (exercised through the component)", () => {
179
+ it("calls a function ref forwarded through the Slot with the element", () => {
180
+ // Arrange
181
+ const fnRef = vi.fn();
182
+
183
+ // Act — a function ref on the Slot is composed onto the child element
184
+ render(
185
+ <Slot ref={fnRef}>
186
+ <button type="button">Click me</button>
187
+ </Slot>,
188
+ );
189
+
190
+ // Assert
191
+ expect(fnRef).toHaveBeenCalledWith(screen.getByRole("button"));
192
+ });
193
+
194
+ it("populates an object ref and tolerates a child with no ref of its own", () => {
195
+ // Arrange — the Slot's object ref composes with the child's (absent) ref;
196
+ // the absent side must be skipped, not dereferenced.
197
+ const objRef = createRef<HTMLElement>();
198
+
199
+ // Act
200
+ render(
201
+ <Slot ref={objRef}>
202
+ <button type="button">Click me</button>
203
+ </Slot>,
204
+ );
205
+
206
+ // Assert
207
+ expect(objRef.current).toBe(screen.getByRole("button"));
208
+ });
209
+ });
210
+
211
+ describe("mergeProps — event handlers (both sides)", () => {
212
+ it("composes handlers when both provide one — child first, then slot", async () => {
213
+ // Arrange
214
+ const user = userEvent.setup();
215
+ const calls: string[] = [];
216
+ const slotClick = vi.fn(() => calls.push("slot"));
217
+ const childClick = vi.fn(() => calls.push("child"));
218
+ render(
219
+ <Slot onClick={slotClick}>
220
+ <button type="button" onClick={childClick}>
221
+ Click me
222
+ </button>
223
+ </Slot>,
224
+ );
225
+
226
+ // Act
227
+ await user.click(screen.getByRole("button"));
228
+
229
+ // Assert — both fire, child's handler first
230
+ expect(childClick).toHaveBeenCalledTimes(1);
231
+ expect(slotClick).toHaveBeenCalledTimes(1);
232
+ expect(calls).toEqual(["child", "slot"]);
233
+ });
234
+ });
235
+
236
+ describe("mergeProps — other props", () => {
237
+ it("keeps non-special props, child winning over the slot on collision", () => {
238
+ // Arrange
239
+ const { container } = render(
240
+ <Slot id="slot-id" data-origin="slot">
241
+ <span id="child-id">text</span>
242
+ </Slot>,
243
+ );
244
+ const el = container.firstChild as HTMLElement;
245
+
246
+ // Assert — child's id wins; the slot-only prop still passes through
247
+ expect(el.id).toBe("child-id");
248
+ expect(el.getAttribute("data-origin")).toBe("slot");
249
+ });
250
+
251
+ it("does not treat a prop merely containing on[A-Z] as an event handler", () => {
252
+ // Arrange — "iconOnly" contains "onO" but is not a handler; only a
253
+ // start-anchored /^on[A-Z]/ correctly excludes it, so the child value must
254
+ // win by the default merge rather than being composed into a function.
255
+ // "iconOnly" is not a real DOM attribute, hence the spread casts.
256
+ const { container } = render(
257
+ <Slot {...({ iconOnly: "slot" } as object)}>
258
+ <span {...({ iconOnly: "child" } as object)}>text</span>
259
+ </Slot>,
260
+ );
261
+
262
+ // Assert
263
+ expect((container.firstChild as HTMLElement).getAttribute("icononly")).toBe(
264
+ "child",
265
+ );
266
+ });
267
+ });
268
+
269
+ it('sets displayName to "Slot" for React DevTools and stack traces', () => {
270
+ // Assert — an empty displayName would render it as an anonymous component.
271
+ expect(Slot.displayName).toBe("Slot");
162
272
  });
163
273
  });
@@ -5,8 +5,25 @@
5
5
  *
6
6
  * This is the standard composition pattern used throughout headless
7
7
  * components to let consumers attach their own listeners onto a
8
- * sub-component without clobbering the component's own behaviour, while
9
- * still giving them an opt-out via `preventDefault()`.
8
+ * sub-component's *own* native event props (e.g. `Tabs.Trigger`'s
9
+ * `onClick`, `Modal.Overlay`'s `onClick`) without clobbering the
10
+ * component's own behaviour, while still giving them an opt-out via
11
+ * `preventDefault()`. Wrap it around the component's internal handler at
12
+ * the point the prop is consumed:
13
+ *
14
+ * ```tsx
15
+ * const handleClick = composeEventHandlers(onClick, () => {
16
+ * // the component's own behaviour, e.g. activating a tab
17
+ * });
18
+ * ```
19
+ *
20
+ * **Not the same mechanism as {@link Slot}.** `Slot`'s own prop-merging
21
+ * (used for the `asChild` pattern) also composes event handlers, but with
22
+ * the *opposite* order (child fires first, then Slot) and *no*
23
+ * `preventDefault()` opt-out — both handlers always run there. Reach for
24
+ * `composeEventHandlers` when a component owns a native DOM prop directly;
25
+ * reach for `Slot` when delegating rendering to a consumer-supplied
26
+ * element via `asChild`. The two are never used together on the same prop.
10
27
  *
11
28
  * @example Consumer logs every click; component still closes the modal
12
29
  * afterwards:
@@ -20,6 +37,13 @@
20
37
  * if (formIsDirty) e.preventDefault(); // don't close
21
38
  * }} />
22
39
  * ```
40
+ *
41
+ * @example Opting out of the veto — always run our handler too
42
+ * ```tsx
43
+ * const handleClick = composeEventHandlers(onClick, ourHandler, {
44
+ * checkForDefaultPrevented: false,
45
+ * });
46
+ * ```
23
47
  */
24
48
  export function composeEventHandlers<E>(
25
49
  theirHandler: ((event: E) => void) | undefined,
package/src/Slot/types.ts CHANGED
@@ -1,5 +1,21 @@
1
1
  import { HTMLAttributes, ReactNode } from "react";
2
2
 
3
+ /**
4
+ * Props for {@link Slot} — all native `HTMLAttributes` (event handlers,
5
+ * `style`, `className`, `aria-*`, `data-*`, ...), merged onto the single
6
+ * child element rather than rendered onto an element `Slot` creates
7
+ * itself. See {@link Slot} for the exact prop-merging rules (event
8
+ * handlers compose child-first, `style` shallow-merges child-wins,
9
+ * `className` concatenates, everything else defaults to the child).
10
+ *
11
+ * @extends HTMLElement
12
+ */
3
13
  export type SlotProps = HTMLAttributes<HTMLElement> & {
14
+ /**
15
+ * The element to clone `Slot`'s props onto. Must be exactly one valid
16
+ * React element that accepts a `ref` — passing zero children, more than
17
+ * one, or a non-element (e.g. a plain string) throws
18
+ * `"Slot requires exactly one React element child."`.
19
+ */
4
20
  children?: ReactNode;
5
21
  };
@@ -0,0 +1,225 @@
1
+ # SplitButton
2
+
3
+ One primary action paired with a menu of related alternatives, bound into
4
+ a single `role="group"` widget. Zero styles ship.
5
+
6
+ ```tsx
7
+ import { SplitButton, VisuallyHidden } from "@primitiv-ui/react";
8
+
9
+ <SplitButton>
10
+ <SplitButton.Action onClick={squashAndMerge}>
11
+ Squash and merge
12
+ </SplitButton.Action>
13
+ <SplitButton.Trigger>
14
+ <ChevronDown aria-hidden="true" />
15
+ <VisuallyHidden>More merge options</VisuallyHidden>
16
+ </SplitButton.Trigger>
17
+ <SplitButton.Menu>
18
+ <SplitButton.Item onSelect={mergeCommit}>
19
+ Create a merge commit
20
+ </SplitButton.Item>
21
+ <SplitButton.Item onSelect={rebase}>Rebase and merge</SplitButton.Item>
22
+ </SplitButton.Menu>
23
+ </SplitButton>;
24
+ ```
25
+
26
+ ## Split button, not select
27
+
28
+ A `Select` answers *"which option is currently selected?"* — picking an
29
+ item changes a **value**. A split button is an **actions** control: the
30
+ left half runs the default action immediately, and the right half opens a
31
+ menu of alternatives. Those alternatives may run their own action, or
32
+ re-point what the primary half does next; either way nothing about the
33
+ widget models a selected value.
34
+
35
+ If you want a value picker, use [`Select`](../Select/README.md). If you
36
+ want a plain menu button with no default action, use
37
+ [`Dropdown`](../Dropdown/README.md) on its own.
38
+
39
+ ## Sub-components
40
+
41
+ | Export | Element | Role | Notes |
42
+ | ------------------------ | -------------------------- | ------------ | ----------------------------------------------------------------------- |
43
+ | `SplitButton` / `.Root` | `<div>` | `group` | Owns the menu's open state; uncontrolled (`defaultOpen`) or controlled |
44
+ | `SplitButton.Action` | `<button type="button">` | `button` | The primary action; `ArrowDown` opens the menu |
45
+ | `SplitButton.Trigger` | `<button type="button">` | `button` | `aria-haspopup="menu"`; name derived from the action |
46
+ | `SplitButton.Menu` | `<menu popover="auto">` | `menu` | The panel of alternatives |
47
+ | `SplitButton.Item` | `<div>` | `menuitem` | One alternative action, with `onSelect` |
48
+ | `SplitButton.Separator` | `<div>` | `separator` | Divider between groups of alternatives |
49
+
50
+ `Root`, `Action` and the group accept `asChild`; `Trigger`, `Menu`,
51
+ `Item` and `Separator` inherit `asChild` from the Dropdown parts they
52
+ delegate to.
53
+
54
+ ## Anatomy and styling hooks
55
+
56
+ | Part | Hook |
57
+ | ------------- | ------------------------------------------------------ |
58
+ | Root | `data-split-button=""`, `data-state="open" \| "closed"` |
59
+ | Action | `data-split-button-action=""` |
60
+ | Trigger | `data-split-button-trigger=""` |
61
+ | Menu | `data-split-button-menu=""` |
62
+ | Any disabled | `data-disabled=""` |
63
+
64
+ `data-state` on the **root** tracks the menu, which is what makes the
65
+ seam between the two halves styleable while the menu is open (a pressed
66
+ chevron, a joined border) without reaching for `:has()`.
67
+
68
+ ## What this component adds over `Dropdown`
69
+
70
+ The menu half *is* a `Dropdown` — Root renders a `Dropdown.Root`, and
71
+ `Menu` / `Item` / `Separator` delegate to `Dropdown.Content` / `.Item` /
72
+ `.Separator`. All the arrow-key navigation, typeahead, Escape handling,
73
+ light dismiss and focus restoration are Dropdown's, tested once over
74
+ there.
75
+
76
+ What `SplitButton` owns is the coordination **between the two halves**,
77
+ which is the whole reason it exists as a primitive rather than a styled
78
+ recipe:
79
+
80
+ 1. **The `role="group"` boundary** — assistive technology reads two
81
+ buttons as one widget rather than two unrelated commands.
82
+ 2. **The trigger's accessible name, derived from the action** — see
83
+ below. This is the thing hand-rolled split buttons get wrong most
84
+ often.
85
+ 3. **`disabled` propagation** — one prop on the group disables both
86
+ halves; it is OR-ed with each half's own `disabled`, so a part can
87
+ never override the group back to enabled.
88
+ 4. **`ArrowDown` on the action opens the menu** — the action is *not*
89
+ the menu trigger, so nothing in `Dropdown` wires this. Note this
90
+ affordance is specific to `SplitButton`; `Dropdown.Trigger` itself
91
+ opens on click/Enter/Space only.
92
+
93
+ ## The derived accessible name
94
+
95
+ A chevron-only control has no text of its own. Unless you pass
96
+ `aria-label` or `aria-labelledby`, `SplitButton.Trigger` defaults to:
97
+
98
+ ```
99
+ aria-labelledby="<the trigger's own id> <the action's id>"
100
+ ```
101
+
102
+ Self-reference is valid in `aria-labelledby` and resolves to the
103
+ element's own contents, so putting visually-hidden text inside the
104
+ trigger produces *"More merge options, Squash and merge"*:
105
+
106
+ ```tsx
107
+ <SplitButton.Trigger>
108
+ <ChevronDown aria-hidden="true" />
109
+ <VisuallyHidden>More merge options</VisuallyHidden>
110
+ </SplitButton.Trigger>
111
+ ```
112
+
113
+ With no text inside the trigger at all, the name falls back to just the
114
+ action's label — ambiguous, but still better than an unlabelled button.
115
+ Passing either ARIA attribute yourself opts out of the derivation
116
+ completely:
117
+
118
+ ```tsx
119
+ <SplitButton.Trigger aria-label="More merge options">
120
+ <ChevronDown aria-hidden="true" />
121
+ </SplitButton.Trigger>
122
+ ```
123
+
124
+ Because the derivation depends on both ids, **`id` is component-owned on
125
+ `Action` and `Trigger`** and is `Omit`-ted from their prop types. Each
126
+ id is derived from the root's `useId()` with a stable `-split-button-action`
127
+ / `-split-button-trigger` suffix.
128
+
129
+ ## Keyboard interaction
130
+
131
+ | Key | On | Behaviour |
132
+ | ----------------------- | ----------------- | ------------------------------------------------ |
133
+ | `Enter` / `Space` | Action | Runs the primary action (native button) |
134
+ | `ArrowDown` | Action | Opens the menu, focusing its first item |
135
+ | `Enter` / `Space` | Trigger | Toggles the menu |
136
+ | `ArrowDown` / `ArrowUp` | Menu | Moves between items (wraps) |
137
+ | `Home` / `End` | Menu | First / last enabled item |
138
+ | printable characters | Menu | Typeahead |
139
+ | `Escape` | Menu | Closes and returns focus to the trigger |
140
+
141
+ **Both halves are separate tab stops.** They are two genuinely distinct
142
+ commands, so each is independently tabbable — this is deliberately *not*
143
+ a roving-tabindex widget, and shouldn't become one. (Contrast
144
+ `NavigationMenu`, which uses the roving-tabindex hook for its keymap only,
145
+ for the same underlying reason.)
146
+
147
+ ## State modes
148
+
149
+ Identical to `Dropdown.Root`'s, and statically discriminated at the type
150
+ level:
151
+
152
+ - **Uncontrolled** — pass `defaultOpen` (or omit it to start closed).
153
+ Optional `onOpenChange` observes user-driven transitions.
154
+ - **Controlled** — pass `open` and `onOpenChange` together.
155
+
156
+ ```tsx
157
+ const [open, setOpen] = useState(false);
158
+
159
+ <SplitButton open={open} onOpenChange={setOpen}>
160
+ ...
161
+ </SplitButton>;
162
+ ```
163
+
164
+ ## Disabled
165
+
166
+ ```tsx
167
+ {/* Both halves disabled */}
168
+ <SplitButton disabled>...</SplitButton>
169
+
170
+ {/* Only the action — alternatives stay reachable */}
171
+ <SplitButton.Action disabled>Squash and merge</SplitButton.Action>
172
+
173
+ {/* Only the menu — the default action still works */}
174
+ <SplitButton.Trigger disabled aria-label="More merge options" />
175
+ ```
176
+
177
+ ## Richer menus
178
+
179
+ `SplitButton` re-exports only the two menu parts a split button almost
180
+ always needs. Root provides the same Dropdown context, so every other
181
+ `Dropdown` part composes inside `SplitButton.Menu` directly:
182
+
183
+ ```tsx
184
+ import { Dropdown, SplitButton } from "@primitiv-ui/react";
185
+
186
+ <SplitButton.Menu>
187
+ <Dropdown.Group>
188
+ <Dropdown.Label>Merge strategy</Dropdown.Label>
189
+ <SplitButton.Item onSelect={mergeCommit}>
190
+ Create a merge commit
191
+ </SplitButton.Item>
192
+ </Dropdown.Group>
193
+ <SplitButton.Separator />
194
+ <Dropdown.Sub>
195
+ <Dropdown.SubTrigger>Advanced</Dropdown.SubTrigger>
196
+ <Dropdown.SubContent>
197
+ <SplitButton.Item onSelect={rebase}>Rebase and merge</SplitButton.Item>
198
+ </Dropdown.SubContent>
199
+ </Dropdown.Sub>
200
+ </SplitButton.Menu>;
201
+ ```
202
+
203
+ `dir` on `SplitButton.Root` is forwarded to the Dropdown, so submenus
204
+ invert their open/close arrow keys in RTL.
205
+
206
+ ## `asChild`
207
+
208
+ ```tsx
209
+ {/* The group is a <section> */}
210
+ <SplitButton asChild>
211
+ <section aria-label="Merge">...</section>
212
+ </SplitButton>
213
+
214
+ {/* The primary action navigates instead of firing a handler.
215
+ type="button" is not forwarded in asChild mode. */}
216
+ <SplitButton.Action asChild>
217
+ <a href="/merge">Squash and merge</a>
218
+ </SplitButton.Action>
219
+ ```
220
+
221
+ ## Positioning
222
+
223
+ Like `Dropdown`, the menu is a native `popover="auto"` element in the top
224
+ layer — placement is the consumer's CSS concern (CSS anchor positioning,
225
+ as the registry `dropdown` component does).