@primitiv-ui/react 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/README.md +19 -2
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -0,0 +1,79 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { SplitButton } from "../SplitButton";
5
+
6
+ describe("SplitButton — disabled", () => {
7
+ it("disables both halves when the group is disabled", () => {
8
+ render(
9
+ <SplitButton disabled>
10
+ <SplitButton.Action>Squash and merge</SplitButton.Action>
11
+ <SplitButton.Trigger aria-label="More merge options" />
12
+ <SplitButton.Menu />
13
+ </SplitButton>,
14
+ );
15
+
16
+ expect(screen.getByRole("group")).toHaveAttribute("data-disabled", "");
17
+ expect(
18
+ screen.getByRole("button", { name: "Squash and merge" }),
19
+ ).toBeDisabled();
20
+ expect(
21
+ screen.getByRole("button", { name: "More merge options" }),
22
+ ).toBeDisabled();
23
+ });
24
+
25
+ it("disables one half independently, leaving the other interactive", () => {
26
+ render(
27
+ <SplitButton>
28
+ <SplitButton.Action disabled>Squash and merge</SplitButton.Action>
29
+ <SplitButton.Trigger aria-label="More merge options" />
30
+ <SplitButton.Menu />
31
+ </SplitButton>,
32
+ );
33
+
34
+ expect(screen.getByRole("group")).not.toHaveAttribute("data-disabled");
35
+ expect(
36
+ screen.getByRole("button", { name: "Squash and merge" }),
37
+ ).toHaveAttribute("data-disabled", "");
38
+ expect(
39
+ screen.getByRole("button", { name: "More merge options" }),
40
+ ).toBeEnabled();
41
+ });
42
+
43
+ it("disables only the menu half when the trigger is disabled", () => {
44
+ render(
45
+ <SplitButton>
46
+ <SplitButton.Action>Squash and merge</SplitButton.Action>
47
+ <SplitButton.Trigger disabled aria-label="More merge options" />
48
+ <SplitButton.Menu />
49
+ </SplitButton>,
50
+ );
51
+
52
+ expect(
53
+ screen.getByRole("button", { name: "Squash and merge" }),
54
+ ).toBeEnabled();
55
+ expect(
56
+ screen.getByRole("button", { name: "More merge options" }),
57
+ ).toHaveAttribute("data-disabled", "");
58
+ });
59
+
60
+ it("re-propagates when the group's disabled flips after mount", () => {
61
+ const markup = (disabled: boolean) => (
62
+ <SplitButton disabled={disabled}>
63
+ <SplitButton.Action>Squash and merge</SplitButton.Action>
64
+ <SplitButton.Trigger aria-label="More merge options" />
65
+ <SplitButton.Menu />
66
+ </SplitButton>
67
+ );
68
+ const { rerender } = render(markup(false));
69
+
70
+ rerender(markup(true));
71
+
72
+ expect(
73
+ screen.getByRole("button", { name: "Squash and merge" }),
74
+ ).toBeDisabled();
75
+ expect(
76
+ screen.getByRole("button", { name: "More merge options" }),
77
+ ).toBeDisabled();
78
+ });
79
+ });
@@ -0,0 +1,29 @@
1
+ import { render } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { SplitButton } from "../SplitButton";
5
+
6
+ const outsideRoot =
7
+ "SplitButton sub-components must be rendered inside a <SplitButton.Root>.";
8
+
9
+ describe("SplitButton — error handling", () => {
10
+ beforeEach(() => {
11
+ vi.spyOn(console, "error").mockImplementation(() => {});
12
+ });
13
+
14
+ afterEach(() => {
15
+ vi.restoreAllMocks();
16
+ });
17
+
18
+ it("throws when SplitButton.Action is used outside SplitButton.Root", () => {
19
+ expect(() =>
20
+ render(<SplitButton.Action>Orphan</SplitButton.Action>),
21
+ ).toThrowError(outsideRoot);
22
+ });
23
+
24
+ it("throws when SplitButton.Trigger is used outside SplitButton.Root", () => {
25
+ expect(() => render(<SplitButton.Trigger aria-label="Orphan" />)).toThrowError(
26
+ outsideRoot,
27
+ );
28
+ });
29
+ });
@@ -0,0 +1,56 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { SplitButton } from "../SplitButton";
6
+
7
+ describe("SplitButton — keyboard interaction", () => {
8
+ it("opens the menu from the action with ArrowDown and focuses the first item", async () => {
9
+ const user = userEvent.setup();
10
+ render(
11
+ <SplitButton>
12
+ <SplitButton.Action>Squash and merge</SplitButton.Action>
13
+ <SplitButton.Trigger aria-label="More merge options" />
14
+ <SplitButton.Menu>
15
+ <SplitButton.Item>Create a merge commit</SplitButton.Item>
16
+ <SplitButton.Item>Rebase and merge</SplitButton.Item>
17
+ </SplitButton.Menu>
18
+ </SplitButton>,
19
+ );
20
+
21
+ await user.tab();
22
+ await user.keyboard("{ArrowDown}");
23
+
24
+ expect(
25
+ screen.getByRole("button", { name: "More merge options" }),
26
+ ).toHaveAttribute("aria-expanded", "true");
27
+ expect(
28
+ screen.getByRole("menuitem", {
29
+ name: "Create a merge commit",
30
+ hidden: true,
31
+ }),
32
+ ).toHaveFocus();
33
+ });
34
+
35
+ it("leaves the menu closed for other keys, and still runs the action's own handler", async () => {
36
+ const user = userEvent.setup();
37
+ const onKeyDown = vi.fn();
38
+ render(
39
+ <SplitButton>
40
+ <SplitButton.Action onKeyDown={onKeyDown}>
41
+ Squash and merge
42
+ </SplitButton.Action>
43
+ <SplitButton.Trigger aria-label="More merge options" />
44
+ <SplitButton.Menu>
45
+ <SplitButton.Item>Create a merge commit</SplitButton.Item>
46
+ </SplitButton.Menu>
47
+ </SplitButton>,
48
+ );
49
+
50
+ await user.tab();
51
+ await user.keyboard("{ArrowUp}");
52
+
53
+ expect(onKeyDown).toHaveBeenCalledOnce();
54
+ expect(screen.getByRole("group")).toHaveAttribute("data-state", "closed");
55
+ });
56
+ });
@@ -0,0 +1,68 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { Dropdown } from "../../Dropdown/index.ts";
6
+ import { SplitButton } from "../SplitButton";
7
+
8
+ describe("SplitButton — state modes", () => {
9
+ it("starts open in uncontrolled mode when defaultOpen is set", () => {
10
+ render(
11
+ <SplitButton defaultOpen>
12
+ <SplitButton.Action>Squash and merge</SplitButton.Action>
13
+ <SplitButton.Trigger aria-label="More merge options" />
14
+ <SplitButton.Menu>
15
+ <SplitButton.Item>Rebase and merge</SplitButton.Item>
16
+ </SplitButton.Menu>
17
+ </SplitButton>,
18
+ );
19
+
20
+ expect(screen.getByRole("group")).toHaveAttribute("data-state", "open");
21
+ });
22
+
23
+ it("defers to the parent in controlled mode", async () => {
24
+ const user = userEvent.setup();
25
+ const onOpenChange = vi.fn();
26
+ render(
27
+ <SplitButton open={false} onOpenChange={onOpenChange}>
28
+ <SplitButton.Action>Squash and merge</SplitButton.Action>
29
+ <SplitButton.Trigger aria-label="More merge options" />
30
+ <SplitButton.Menu>
31
+ <SplitButton.Item>Rebase and merge</SplitButton.Item>
32
+ </SplitButton.Menu>
33
+ </SplitButton>,
34
+ );
35
+
36
+ await user.click(screen.getByRole("button", { name: "More merge options" }));
37
+
38
+ expect(onOpenChange).toHaveBeenCalledWith(true);
39
+ expect(screen.getByRole("group")).toHaveAttribute("data-state", "closed");
40
+ });
41
+
42
+ it("passes the reading direction down to menus composed from Dropdown", async () => {
43
+ const user = userEvent.setup();
44
+ render(
45
+ <SplitButton dir="rtl" defaultOpen>
46
+ <SplitButton.Action>Squash and merge</SplitButton.Action>
47
+ <SplitButton.Trigger aria-label="More merge options" />
48
+ <SplitButton.Menu>
49
+ <Dropdown.Sub>
50
+ <Dropdown.SubTrigger>Merge strategies</Dropdown.SubTrigger>
51
+ <Dropdown.SubContent>
52
+ <SplitButton.Item>Rebase and merge</SplitButton.Item>
53
+ </Dropdown.SubContent>
54
+ </Dropdown.Sub>
55
+ </SplitButton.Menu>
56
+ </SplitButton>,
57
+ );
58
+
59
+ await user.keyboard("{ArrowLeft}");
60
+
61
+ expect(
62
+ screen.getByRole("menuitem", {
63
+ name: "Merge strategies",
64
+ hidden: true,
65
+ }),
66
+ ).toHaveAttribute("aria-expanded", "true");
67
+ });
68
+ });
@@ -0,0 +1,2 @@
1
+ export { SplitButton } from "./SplitButton";
2
+ export type * from "./types";
@@ -0,0 +1,186 @@
1
+ import { ComponentProps, ReactNode, Ref } from "react";
2
+
3
+ import type { Direction } from "../DirectionProvider/index.ts";
4
+ import type {
5
+ DropdownContentProps,
6
+ DropdownItemProps,
7
+ DropdownSeparatorProps,
8
+ DropdownTriggerProps,
9
+ } from "../Dropdown/types";
10
+
11
+ /**
12
+ * Shared base for both {@link SplitButtonRootProps} variants — the native
13
+ * `<div>` attributes (minus the component-owned `role` and the narrowed
14
+ * `dir`) plus the `asChild` escape hatch, reading direction, and the
15
+ * group-level `disabled`.
16
+ */
17
+ export type SplitButtonRootBaseProps = Omit<
18
+ ComponentProps<"div">,
19
+ "role" | "dir"
20
+ > & {
21
+ /**
22
+ * The widget's parts — typically one
23
+ * {@link SplitButtonActionProps | `SplitButton.Action`}, one
24
+ * {@link SplitButtonTriggerProps | `SplitButton.Trigger`}, and one
25
+ * {@link SplitButtonMenuProps | `SplitButton.Menu`}.
26
+ */
27
+ children?: ReactNode;
28
+ /** Forwarded to the underlying `HTMLDivElement` (or to the `asChild`
29
+ * element). */
30
+ ref?: Ref<HTMLDivElement>;
31
+ /**
32
+ * When `true`, Root delegates rendering to a single consumer-supplied
33
+ * element via the {@link Slot} pattern instead of the default `<div>`;
34
+ * `role="group"`, `data-state`, `data-disabled` and the remaining props are
35
+ * merged onto it.
36
+ * @default false
37
+ */
38
+ asChild?: boolean;
39
+ /**
40
+ * Reading direction for the menu. Only affects which arrow key opens or
41
+ * closes a submenu composed from `Dropdown.Sub` — `ArrowRight` opens in
42
+ * `"ltr"`, `ArrowLeft` in `"rtl"`. When omitted it is inherited from the
43
+ * nearest {@link DirectionProvider}, falling back to `"ltr"`.
44
+ */
45
+ dir?: Direction;
46
+ /**
47
+ * Disables the whole widget — both the primary action and the menu trigger
48
+ * become non-interactive, and `data-disabled=""` is set on the group. OR-ed
49
+ * with each half's own `disabled`, so a disabled group can never be
50
+ * overridden back to enabled by a part.
51
+ * @default false
52
+ */
53
+ disabled?: boolean;
54
+ };
55
+
56
+ /**
57
+ * Uncontrolled variant of {@link SplitButtonRootProps}: the component owns the
58
+ * menu's open state. Pass `defaultOpen` (or omit it); `onOpenChange` is
59
+ * optional and `open` is forbidden.
60
+ */
61
+ export type SplitButtonRootUncontrolledProps = SplitButtonRootBaseProps & {
62
+ /**
63
+ * Whether the menu is open on first render. Omit to start closed. The
64
+ * component owns the state thereafter.
65
+ * @default false
66
+ */
67
+ defaultOpen?: boolean;
68
+ /** Forbidden in uncontrolled mode — use `defaultOpen` instead. */
69
+ open?: never;
70
+ /**
71
+ * Called whenever a user-driven transition opens or closes the menu
72
+ * (trigger click, `ArrowDown` on the action, Escape, outside click,
73
+ * selection). Optional in uncontrolled mode.
74
+ */
75
+ onOpenChange?: (open: boolean) => void;
76
+ };
77
+
78
+ /**
79
+ * Controlled variant of {@link SplitButtonRootProps}: the parent owns the
80
+ * menu's open state. Pass `open` and `onOpenChange` together; `defaultOpen` is
81
+ * forbidden.
82
+ */
83
+ export type SplitButtonRootControlledProps = SplitButtonRootBaseProps & {
84
+ /** Forbidden in controlled mode — use `open` instead. */
85
+ defaultOpen?: never;
86
+ /**
87
+ * Whether the menu is currently open. Must be kept in sync by the parent via
88
+ * `onOpenChange`; the component never mutates it internally.
89
+ */
90
+ open: boolean;
91
+ /**
92
+ * Called whenever a user-driven transition would open or close the menu. The
93
+ * parent is responsible for reflecting the new value back into `open`.
94
+ * Required in controlled mode.
95
+ */
96
+ onOpenChange: (open: boolean) => void;
97
+ };
98
+
99
+ /**
100
+ * Props for {@link SplitButtonRoot | `SplitButton.Root`} — the
101
+ * controlled/uncontrolled discriminated union pairing
102
+ * {@link SplitButtonRootUncontrolledProps} with
103
+ * {@link SplitButtonRootControlledProps}, plus the shared group props.
104
+ */
105
+ export type SplitButtonRootProps =
106
+ | SplitButtonRootUncontrolledProps
107
+ | SplitButtonRootControlledProps;
108
+
109
+ /**
110
+ * Props for the internal frame component that renders the group element. Root
111
+ * splits it out so the frame sits *inside* the `Dropdown.Root` it renders and
112
+ * can therefore read the menu's open state.
113
+ *
114
+ * @internal
115
+ */
116
+ export type SplitButtonFrameProps = Omit<
117
+ SplitButtonRootBaseProps,
118
+ "dir" | "disabled"
119
+ > & {
120
+ /** Already defaulted by Root, so the frame receives a definite boolean. */
121
+ disabled: boolean;
122
+ };
123
+
124
+ /**
125
+ * Value published by `SplitButtonContext` — the derived ids that wire the
126
+ * trigger's accessible name to the action, plus the group's disabled flag.
127
+ *
128
+ * @internal
129
+ */
130
+ export type SplitButtonContextValue = {
131
+ /** DOM id applied to {@link SplitButtonActionProps | `SplitButton.Action`};
132
+ * referenced by the trigger's derived `aria-labelledby`. */
133
+ actionId: string;
134
+ /** DOM id applied to {@link SplitButtonTriggerProps | `SplitButton.Trigger`};
135
+ * the self-reference half of its derived `aria-labelledby`. */
136
+ triggerId: string;
137
+ /** Whether the whole widget is disabled (Root's `disabled` prop). */
138
+ disabled: boolean;
139
+ };
140
+
141
+ /**
142
+ * Props for {@link SplitButtonAction | `SplitButton.Action`} — the native
143
+ * `<button>` attributes minus `id`, which the component owns because the menu
144
+ * trigger's accessible name references it.
145
+ */
146
+ export type SplitButtonActionProps = Omit<ComponentProps<"button">, "id"> & {
147
+ /** The action's visible label. Keep it real text — the menu trigger borrows
148
+ * it for its own accessible name. */
149
+ children?: ReactNode;
150
+ /** Forwarded to the underlying `HTMLButtonElement` (or to the `asChild`
151
+ * element). */
152
+ ref?: Ref<HTMLButtonElement>;
153
+ /**
154
+ * When `true`, renders the consumer's element instead of `<button>` via the
155
+ * {@link Slot} pattern. `type="button"` is **not** forwarded in this mode —
156
+ * the child owns its own type semantics.
157
+ * @default false
158
+ */
159
+ asChild?: boolean;
160
+ };
161
+
162
+ /**
163
+ * Props for {@link SplitButtonTrigger | `SplitButton.Trigger`} —
164
+ * {@link DropdownTriggerProps} minus `id`, which the component owns because
165
+ * its derived `aria-labelledby` self-references it. Supply `aria-label` or
166
+ * `aria-labelledby` to opt out of the derived name.
167
+ */
168
+ export type SplitButtonTriggerProps = Omit<DropdownTriggerProps, "id">;
169
+
170
+ /**
171
+ * Props for {@link SplitButtonMenu | `SplitButton.Menu`} — identical to
172
+ * {@link DropdownContentProps}; the menu *is* a `Dropdown.Content`.
173
+ */
174
+ export type SplitButtonMenuProps = DropdownContentProps;
175
+
176
+ /**
177
+ * Props for {@link SplitButtonItem | `SplitButton.Item`} — identical to
178
+ * {@link DropdownItemProps}.
179
+ */
180
+ export type SplitButtonItemProps = DropdownItemProps;
181
+
182
+ /**
183
+ * Props for {@link SplitButtonSeparator | `SplitButton.Separator`} — identical
184
+ * to {@link DropdownSeparatorProps}.
185
+ */
186
+ export type SplitButtonSeparatorProps = DropdownSeparatorProps;
@@ -4,29 +4,50 @@ import { Slot } from "../Slot/index.ts";
4
4
  import { StatusProps } from "./types";
5
5
 
6
6
  /**
7
- * A polite live region for low-priority, non-urgent status messages.
7
+ * A polite live region for low-priority, non-urgent status messages,
8
+ * implementing the WAI-ARIA `status` role.
8
9
  *
9
- * Renders a `<div role="status">`. The `status` role carries an
10
- * implicit `aria-live="polite"` and `aria-atomic="true"`, so assistive
11
- * technology announces the message once the user is idle, without
12
- * interrupting them — use it for confirmations, counts, and background
13
- * progress. For errors the user must see immediately, reach for
14
- * `Alert` instead.
10
+ * Renders a `<div role="status">`. The `status` role carries an implicit
11
+ * `aria-live="polite"` and `aria-atomic="true"`, so assistive technology
12
+ * announces the message once the user is idle, without interrupting
13
+ * them — use it for confirmations, counts, and background progress. For
14
+ * errors the user must see immediately, reach for `Alert` instead.
15
15
  *
16
- * Render the `Status` conditionally, or update its text content while
17
- * it stays mounted: the live region announces content that changes
18
- * *after* it is already in the DOM.
16
+ * **Live region timing.** A live region only announces content that
17
+ * changes *after* the region is already in the DOM. Either render
18
+ * `Status` conditionally (mount when a message is ready), or keep it
19
+ * mounted and update its text:
19
20
  *
20
- * **`asChild` composition.** Renders the consumer's element instead of
21
- * a `<div>`, merging `role="status"` and all other props in via the
22
- * {@link Slot} utility.
21
+ * ```tsx
22
+ * // Conditional mount — announces on first appearance
23
+ * {saved && <Status>All changes saved.</Status>}
24
+ *
25
+ * // Persistent — announces on every content change
26
+ * <Status>{count} items in your cart</Status>
27
+ * ```
28
+ *
29
+ * **`asChild` composition.** Pass `asChild` to render the consumer's own
30
+ * element instead of a `<div>`, merging `role="status"` and all other
31
+ * props onto it via the {@link Slot} utility. Event handlers compose with
32
+ * the child's own handlers (child runs first).
33
+ *
34
+ * @extends HTMLDivElement
23
35
  *
24
36
  * @example Confirmation message
25
37
  * ```tsx
26
- * {saved && <Status>All changes saved.</Status>}
38
+ * import { Status } from "@primitiv-ui/react";
39
+ *
40
+ * function SaveIndicator({ saved }: { saved: boolean }) {
41
+ * return saved ? <Status>All changes saved.</Status> : null;
42
+ * }
43
+ * ```
44
+ *
45
+ * @example Cart count — persistent live region
46
+ * ```tsx
47
+ * <Status>{count} items in your cart</Status>
27
48
  * ```
28
49
  *
29
- * @example asChild — keep semantic markup
50
+ * @example asChild — apply the status role to a semantic element
30
51
  * ```tsx
31
52
  * <Status asChild>
32
53
  * <output>{count} items in your cart</output>
@@ -25,4 +25,9 @@ describe("Status component", () => {
25
25
  expect(status.tagName).toBe("SECTION");
26
26
  expect(status).toHaveTextContent("Saved");
27
27
  });
28
+
29
+ it('sets displayName to "Status" for React DevTools and stack traces', () => {
30
+ // Assert — an empty displayName would render it as an anonymous component.
31
+ expect(Status.displayName).toBe("Status");
32
+ });
28
33
  });
@@ -1,9 +1,20 @@
1
- import { ComponentProps } from "react";
1
+ import { ComponentProps, ReactNode } from "react";
2
2
 
3
3
  /**
4
4
  * Props for {@link Status} — all native `<div>` attributes plus the
5
5
  * `asChild` escape hatch.
6
6
  */
7
7
  export type StatusProps = ComponentProps<"div"> & {
8
+ /**
9
+ * Renders the child element instead of a native `<div>`, merging
10
+ * `role="status"` and all other props onto it via {@link Slot}.
11
+ * Event handlers compose with the child's own handlers (child runs
12
+ * first). Use this to apply the live-region role to a semantically
13
+ * appropriate element (e.g. `<output>`).
14
+ * @default false
15
+ */
8
16
  asChild?: boolean;
17
+ /** Content to announce. A live region announces changes that occur
18
+ * after the region is already present in the DOM. */
19
+ children?: ReactNode;
9
20
  };
@@ -45,7 +45,7 @@ Pass `checked` and `onCheckedChange` together. The parent owns the value.
45
45
  ```tsx
46
46
  const [enabled, setEnabled] = useState(false);
47
47
 
48
- <Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="…">
48
+ <Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
49
49
  <Switch.Thumb />
50
50
  </Switch.Root>
51
51
  ```
@@ -25,7 +25,7 @@ function dataStateOf(checked: boolean) {
25
25
  *
26
26
  * Props routing: `className` / `style` style the **track** (the `<label>` you
27
27
  * see — the input is hidden); every other prop (`name`, `value`, `id`,
28
- * `aria-*`, `required`, `disabled`, `ref`, …) spreads onto the input, because
28
+ * `aria-*`, `required`, `disabled`, `ref`, ...) spreads onto the input, because
29
29
  * semantically the Root *is* the switch.
30
30
  *
31
31
  * Supports two state modes, statically discriminated at the type level:
@@ -42,6 +42,12 @@ function dataStateOf(checked: boolean) {
42
42
  * **Keyboard.** Toggles on `Space` (the native checkbox behaviour); `Enter`
43
43
  * does not toggle a checkbox-based control.
44
44
  *
45
+ * **`onChange` composition.** The consumer's raw `onChange` fires first; if it
46
+ * calls `event.preventDefault()`, the internal mirror update and
47
+ * `onCheckedChange` are skipped for that event. See {@link SwitchRootBaseProps}.
48
+ *
49
+ * @extends HTMLInputElement
50
+ *
45
51
  * @example Uncontrolled
46
52
  * ```tsx
47
53
  * <Switch.Root name="notify" value="on" defaultChecked aria-label="Enable notifications">
@@ -53,7 +59,7 @@ function dataStateOf(checked: boolean) {
53
59
  * ```tsx
54
60
  * const [enabled, setEnabled] = useState(false);
55
61
  *
56
- * <Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="…">
62
+ * <Switch.Root checked={enabled} onCheckedChange={setEnabled} aria-label="...">
57
63
  * <Switch.Thumb />
58
64
  * </Switch.Root>
59
65
  * ```
@@ -102,7 +108,11 @@ export function SwitchRoot({
102
108
  );
103
109
  }
104
110
 
105
- /** @internal */
111
+ // Runtime-dead: the compound alias below (same object via Object.assign)
112
+ // overwrites this to "Switch" at load, so the value is never observable. The
113
+ // assignment stays because it declares `displayName` on `typeof SwitchRoot`,
114
+ // which TSwitchCompound extends.
115
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
106
116
  SwitchRoot.displayName = "SwitchRoot";
107
117
 
108
118
  /**
@@ -114,7 +124,10 @@ SwitchRoot.displayName = "SwitchRoot";
114
124
  * **Styling hook.** Mirrors the root's `data-state="checked" | "unchecked"`.
115
125
  *
116
126
  * **`asChild` prop.** Pass `asChild` to render the consumer's own element as
117
- * the thumb, with `aria-hidden` and `data-state` merged in.
127
+ * the thumb, with `aria-hidden` and `data-state` merged in via the
128
+ * {@link Slot} pattern. See {@link SwitchThumbProps}.
129
+ *
130
+ * @extends HTMLSpanElement
118
131
  *
119
132
  * @example
120
133
  * ```tsx
@@ -92,4 +92,11 @@ describe("Switch basic rendering", () => {
92
92
  expect(thumb).toHaveAttribute("aria-hidden", "true");
93
93
  expect(thumb).toHaveAttribute("data-state", "unchecked");
94
94
  });
95
+
96
+ it("sets a displayName on the compound and the Thumb", () => {
97
+ // Assert — empty displayNames would render each as anonymous in DevTools.
98
+ // Root aliases the compound (Object.assign), so its name is "Switch".
99
+ expect(Switch.displayName).toBe("Switch");
100
+ expect(Switch.Thumb.displayName).toBe("SwitchThumb");
101
+ });
95
102
  });
@@ -34,6 +34,9 @@ export function useSwitchRoot({
34
34
  onChange?.(event);
35
35
  if (event.defaultPrevented) return;
36
36
  const next = event.target.checked;
37
+ // Stryker disable next-line ConditionalExpression: in controlled mode
38
+ // `checked` reads controlledChecked, never `mirror`, so setting mirror here
39
+ // (the mutant's always-run form) has no observable effect — equivalent.
37
40
  if (!isControlled) setMirror(next);
38
41
  onCheckedChange?.(next);
39
42
  };