@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
@@ -28,6 +28,64 @@ describe("Activation mode tests", () => {
28
28
  expect(secondTabPanel).not.toHaveAttribute("hidden");
29
29
  });
30
30
 
31
+ it("does not activate a tab on arrow navigation in manual mode", async () => {
32
+ // Directly asserts the manual guard: arrow moves focus without activating,
33
+ // so Tab 1 stays selected and Tab 2 does not.
34
+ const user = userEvent.setup();
35
+ render(
36
+ <Tabs.Root defaultValue="tab1" activationMode="manual">
37
+ <Tabs.List label="Test tabs">
38
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
39
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
40
+ </Tabs.List>
41
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
42
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
43
+ </Tabs.Root>,
44
+ );
45
+
46
+ await user.tab();
47
+ await user.keyboard("{ArrowRight}");
48
+
49
+ expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveAttribute(
50
+ "aria-selected",
51
+ "false",
52
+ );
53
+ expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveAttribute(
54
+ "aria-selected",
55
+ "true",
56
+ );
57
+ });
58
+
59
+ it("activates a non-native asChild tab via the roving Enter handler (manual)", async () => {
60
+ // A plain <div> fires no synthetic click on Enter, so activation can only
61
+ // come from the roving hook's includeActivate mapping — the guarantee a
62
+ // native <button> would mask.
63
+ const user = userEvent.setup();
64
+ render(
65
+ <Tabs.Root defaultValue="tab1" activationMode="manual">
66
+ <Tabs.List label="Test tabs">
67
+ <Tabs.Trigger asChild value="tab1">
68
+ <div>Tab 1</div>
69
+ </Tabs.Trigger>
70
+ <Tabs.Trigger asChild value="tab2">
71
+ <div>Tab 2</div>
72
+ </Tabs.Trigger>
73
+ </Tabs.List>
74
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
75
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
76
+ </Tabs.Root>,
77
+ );
78
+
79
+ await user.tab();
80
+ await user.keyboard("{ArrowRight}");
81
+ await user.keyboard("{Enter}");
82
+
83
+ expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveAttribute(
84
+ "aria-selected",
85
+ "true",
86
+ );
87
+ });
88
+
31
89
  it("should not activate the second tab when focusing its tab in manual mode", async () => {
32
90
  // Arrange
33
91
  const user = userEvent.setup();
@@ -1,6 +1,21 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
 
3
3
  import { Tabs, TabsOrientation } from "../index.ts";
4
+ import { TabsContext } from "../TabsContext";
5
+
6
+ describe("Tabs displayNames", () => {
7
+ it("sets a displayName on the compound, each sub-component, and the context", () => {
8
+ // Assert — empty displayNames would render each as anonymous in DevTools.
9
+ // `Tabs`, `Tabs.Root`, and the underlying component are one object
10
+ // (Object.assign compound), so the compound's "Tabs" alias is the
11
+ // observable Root displayName; the sub-components are distinct objects.
12
+ expect(Tabs.displayName).toBe("Tabs");
13
+ expect(Tabs.List.displayName).toBe("TabsList");
14
+ expect(Tabs.Trigger.displayName).toBe("TabsTrigger");
15
+ expect(Tabs.Content.displayName).toBe("TabsContent");
16
+ expect(TabsContext.displayName).toBe("TabsContext");
17
+ });
18
+ });
4
19
 
5
20
  describe("Tabs basic rendering tests", () => {
6
21
  describe("Tabs.Root", () => {
@@ -110,6 +110,30 @@ describe('Tabs change event callbacks', () => {
110
110
  expect(handleChange).not.toHaveBeenCalled();
111
111
  });
112
112
 
113
+ it('does not re-fire onChange when re-clicking a tab activated after mount', async () => {
114
+ // No defaultValue: the guard reads an activeValue that changes after the
115
+ // first click, so a stale activateTab closure would re-fire on the second.
116
+ const user = userEvent.setup();
117
+ const handleChange = vi.fn();
118
+
119
+ render(
120
+ <Tabs.Root onChange={handleChange}>
121
+ <Tabs.List label="Test tabs">
122
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
123
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
124
+ </Tabs.List>
125
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
126
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
127
+ </Tabs.Root>,
128
+ );
129
+
130
+ const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
131
+ await user.click(tab2);
132
+ await user.click(tab2);
133
+
134
+ expect(handleChange).toHaveBeenCalledTimes(1);
135
+ });
136
+
113
137
  it('should not call onValueChange if clicking already active tab in controlled mode', async () => {
114
138
  const user = userEvent.setup();
115
139
  const handleValueChange = vi.fn();
@@ -2,6 +2,42 @@ import { render, screen } from "@testing-library/react";
2
2
  import { Tabs } from "../Tabs";
3
3
  import userEvent from "@testing-library/user-event";
4
4
 
5
+ describe("Tabs disabled re-registration", () => {
6
+ it("re-registers a trigger's disabled flag when it changes after mount", async () => {
7
+ // Arrange — automatic mode: arrowing onto a tab activates it unless the
8
+ // trigger is registered as disabled. Tab 2 starts enabled, then disables.
9
+ const user = userEvent.setup();
10
+ function Fixture({ tab2Disabled }: { tab2Disabled: boolean }) {
11
+ return (
12
+ <Tabs.Root defaultValue="tab1">
13
+ <Tabs.List label="Test tabs">
14
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
15
+ <Tabs.Trigger value="tab2" disabled={tab2Disabled}>
16
+ Tab 2
17
+ </Tabs.Trigger>
18
+ </Tabs.List>
19
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
20
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
21
+ </Tabs.Root>
22
+ );
23
+ }
24
+ const { rerender } = render(<Fixture tab2Disabled={false} />);
25
+
26
+ // Act — disable Tab 2 after mount, then arrow onto it.
27
+ rerender(<Fixture tab2Disabled />);
28
+ await user.tab();
29
+ await user.keyboard("{ArrowRight}");
30
+
31
+ // Assert — the registrar effect re-ran, so Tab 2 is focused but NOT
32
+ // activated. A stale registration would treat it as enabled and activate
33
+ // it on arrow (automatic mode).
34
+ expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveAttribute(
35
+ "aria-selected",
36
+ "false",
37
+ );
38
+ });
39
+ });
40
+
5
41
  describe("Tabs disabled tabs tests", () => {
6
42
  it("should have the aria-disabled attribute set to false by default", () => {
7
43
  // Arrange
@@ -26,6 +26,23 @@ describe("Tabs lazyMount", () => {
26
26
  expect(screen.getByText("Content 3")).toBeInTheDocument();
27
27
  });
28
28
 
29
+ it("defaults lazyMount to off when the prop is omitted (eager render)", () => {
30
+ // Omitting the prop must fall back to eager mounting — a default of `true`
31
+ // would hide inactive panel children on mount.
32
+ render(
33
+ <Tabs.Root defaultValue="tab1">
34
+ <Tabs.List label="Test tabs">
35
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
36
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
37
+ </Tabs.List>
38
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
39
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
40
+ </Tabs.Root>,
41
+ );
42
+
43
+ expect(screen.getByText("Content 2")).toBeInTheDocument();
44
+ });
45
+
29
46
  it("should not render inactive panel children on initial mount when lazyMount is true", () => {
30
47
  renderTabs(true);
31
48
 
@@ -14,6 +14,11 @@ export function useTabsContent({ value }: Pick<TabsContentProps, "value">) {
14
14
 
15
15
  // Track whether this panel has ever been active. Starts true for the
16
16
  // default active panel; flips once on first activation for all others.
17
+ // The mount effect below sets `activated` for the active panel within the
18
+ // same commit, so the initialiser's value is only ever observable as a
19
+ // pre-effect first-paint flash (not reproducible in jsdom); for inactive
20
+ // panels the initialiser's `false` and a mutated `undefined` are both falsy.
21
+ // Stryker disable next-line ArrowFunction: equivalent under the test env — see above.
17
22
  const [activated, setActivated] = useState(() => isActive);
18
23
  useEffect(() => {
19
24
  if (isActive) setActivated(true);
@@ -18,15 +18,23 @@ type TriggerMeta = {
18
18
 
19
19
  export function useTabsRoot(
20
20
  {
21
- orientation = "horizontal",
22
- dir = "ltr",
23
- activationMode = "automatic",
21
+ orientation,
22
+ dir,
23
+ activationMode,
24
24
  defaultValue,
25
25
  value,
26
26
  onValueChange,
27
27
  onChange,
28
- lazyMount = false,
29
- }: Omit<TabsRootProps, "className">,
28
+ lazyMount,
29
+ }: // `TabsRoot` always resolves these four before calling the hook, so they
30
+ // are required here (a default would be dead code the tests never reach).
31
+ Omit<TabsRootProps, "className"> &
32
+ Required<
33
+ Pick<
34
+ TabsRootProps,
35
+ "orientation" | "dir" | "activationMode" | "lazyMount"
36
+ >
37
+ >,
30
38
  ref: Ref<TabsImperativeApi>,
31
39
  ) {
32
40
  const tabsId = useId();
@@ -47,6 +55,10 @@ export function useTabsRoot(
47
55
  (
48
56
  triggerValue: string,
49
57
  element: HTMLButtonElement | null,
58
+ // The default is only reached by the cleanup call `registerTrigger(value,
59
+ // null)`, where `element` is null and `disabled` is discarded — so its
60
+ // value is never observable.
61
+ // Stryker disable next-line BooleanLiteral
50
62
  disabled = false,
51
63
  ) => {
52
64
  registerTriggerBase(
@@ -54,6 +66,9 @@ export function useTabsRoot(
54
66
  element ? { element, disabled } : null,
55
67
  );
56
68
  },
69
+ // `registerTriggerBase` is a stable useCollection callback, so emptying this
70
+ // array yields the identical memoised function.
71
+ // Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
57
72
  [registerTriggerBase],
58
73
  );
59
74
 
@@ -71,8 +86,14 @@ export function useTabsRoot(
71
86
 
72
87
  const focusTrigger = useCallback(
73
88
  (triggerValue: string) => {
89
+ // `triggerValue` is always sourced from the navigable set (registered
90
+ // triggers), so a value that reaches here has a live entry.
91
+ // Stryker disable next-line OptionalChaining: unreachable given that invariant.
74
92
  triggersRef.current.get(triggerValue)?.element.focus();
75
93
  },
94
+ // `triggersRef` is a stable RefObject, so emptying this array yields the
95
+ // identical memoised function.
96
+ // Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
76
97
  [triggersRef],
77
98
  );
78
99
 
@@ -100,6 +121,9 @@ export function useTabsRoot(
100
121
  }
101
122
 
102
123
  if (isControlled) {
124
+ // Controlled mode requires `onValueChange` (discriminated-union prop
125
+ // type), so the optional call can never no-op in valid usage.
126
+ // Stryker disable next-line OptionalChaining: unreachable — controlled requires onValueChange.
103
127
  onValueChange?.(newValue);
104
128
  } else {
105
129
  setActiveValue(newValue);
@@ -51,6 +51,9 @@ export function useTabsTrigger({
51
51
  (newValue: string, index: number) => {
52
52
  if (activeValue === newValue) return;
53
53
  if (isControlled) {
54
+ // Controlled mode requires `onValueChange` (discriminated-union prop
55
+ // type), so the optional call can never no-op in valid usage.
56
+ // Stryker disable next-line OptionalChaining: unreachable — controlled requires onValueChange.
54
57
  onValueChange?.(newValue);
55
58
  } else {
56
59
  setActiveValue(newValue);
@@ -87,10 +90,12 @@ export function useTabsTrigger({
87
90
  includeActivate: true,
88
91
  onNavigate: (targetValue, action) => {
89
92
  const isDisabled = disabledTriggerValues.has(targetValue);
93
+ // Automatic mode activates on any navigation; manual mode activates only
94
+ // on an explicit activation key (Enter/Space). Since the mode is always
95
+ // "automatic" or "manual", `automatic || isActivationKey` captures both.
90
96
  const shouldActivate =
91
97
  !isDisabled &&
92
- (activationMode === "automatic" ||
93
- (activationMode === "manual" && action === "activate"));
98
+ (activationMode === "automatic" || action === "activate");
94
99
  if (shouldActivate) {
95
100
  activateTab(targetValue, triggerValues.indexOf(targetValue));
96
101
  }
package/src/Tabs/types.ts CHANGED
@@ -60,9 +60,18 @@ export type ControlledTabsRootProps = {
60
60
  * the `HTMLDivElement` `ref` left in, a styled wrapper that spreads these props
61
61
  * straight back into `Tabs.Root` (`<Tabs.Root {...props} />`) would fail to
62
62
  * type-check on incompatible `ref` types (D58). */
63
- export type TabsRootProps = Omit<ComponentProps<"div">, "onChange" | "ref"> & {
63
+ export type TabsRootProps = Omit<
64
+ ComponentProps<"div">,
65
+ "onChange" | "ref" | "dir" | "defaultValue"
66
+ > & {
67
+ /** Layout axis; see {@link TabsOrientation}.
68
+ * @default "horizontal" */
64
69
  orientation?: TabsOrientation;
70
+ /** Reading direction; see {@link TabsReadingDirection}. Inherited from the
71
+ * nearest `DirectionProvider` when omitted, falling back to `"ltr"`. */
65
72
  dir?: TabsReadingDirection;
73
+ /** When a focused trigger becomes active; see {@link TabsActivationMode}.
74
+ * @default "automatic" */
66
75
  activationMode?: TabsActivationMode;
67
76
  /** When `true`, a panel's children are not rendered until that tab is first
68
77
  * activated. Once mounted they remain in the DOM across subsequent tab
@@ -105,8 +114,19 @@ export type TabsListProps = Omit<
105
114
  "label" | "aria-labelledby"
106
115
  > &
107
116
  (
108
- | { label: string; ariaLabelledBy?: never }
109
- | { label?: never; ariaLabelledBy: string }
117
+ | {
118
+ /** Accessible name for the tablist, announced as `aria-label`. Pick a
119
+ * short, human-readable description of the set (e.g. `"Account
120
+ * sections"`, not `"Tabs"`). Mutually exclusive with `ariaLabelledBy`. */
121
+ label: string;
122
+ ariaLabelledBy?: never;
123
+ }
124
+ | {
125
+ label?: never;
126
+ /** Id of an existing element to use as the tablist's accessible name,
127
+ * set as `aria-labelledby`. Mutually exclusive with `label`. */
128
+ ariaLabelledBy: string;
129
+ }
110
130
  );
111
131
  /** Props for `Tabs.Trigger` — an individual `role="tab"` button. Identified by
112
132
  * its required `value`, which links it to the `Tabs.Content` of the same value.
@@ -114,9 +134,14 @@ export type TabsListProps = Omit<
114
134
  * `asChild` and the `ref` type parameter. */
115
135
  export type TabsTriggerProps<T extends HTMLElement = HTMLButtonElement> = Omit<
116
136
  ComponentProps<"button">,
117
- "ref"
137
+ "ref" | "value"
118
138
  > & {
139
+ /** Removes the trigger from the roving tab order and marks it
140
+ * `aria-disabled`/`data-disabled`; arrow-key navigation skips it.
141
+ * @default false */
119
142
  disabled?: boolean;
143
+ /** Identifies this trigger, linking it to the `Tabs.Content` of the same
144
+ * value. Not the visible label — use `children` for that. */
120
145
  value: string;
121
146
  /** Render the child element instead of the default `<button>`. All tab
122
147
  * ARIA attributes and event handlers are merged onto the child. The child
@@ -39,10 +39,18 @@ import { TextareaProps } from "./types";
39
39
  * textarea[data-disabled] { opacity: 0.5; cursor: not-allowed; }
40
40
  * ```
41
41
  *
42
+ * **Styling hooks.**
43
+ * - `data-disabled=""` — present when `disabled` is `true`; omitted otherwise.
44
+ * Target with `[data-disabled]` rather than the `:disabled` pseudo-class so
45
+ * styles work identically in both native and `asChild` modes.
46
+ *
42
47
  * **`asChild` composition.** Renders the consumer's element instead of
43
48
  * `<textarea>`, merging all props (aria-*, data-*, event handlers, ref)
44
49
  * via the {@link Slot} utility. Use it to wrap an autosizing textarea
45
- * implementation while keeping this component's prop contract.
50
+ * implementation while keeping this component's prop contract. Event handlers
51
+ * compose with the child's own handlers (child runs first).
52
+ *
53
+ * @extends HTMLTextAreaElement
46
54
  *
47
55
  * @example Basic usage
48
56
  * ```tsx
@@ -104,4 +104,9 @@ describe("Textarea basic rendering", () => {
104
104
  // Assert
105
105
  expect(onChange).toHaveBeenCalledTimes(2);
106
106
  });
107
+
108
+ it('sets displayName to "Textarea" for React DevTools and stack traces', () => {
109
+ // Assert — an empty displayName would render it as an anonymous component.
110
+ expect(Textarea.displayName).toBe("Textarea");
111
+ });
107
112
  });
@@ -1,11 +1,28 @@
1
1
  import { ComponentProps, ReactNode, Ref } from "react";
2
2
 
3
3
  /**
4
- * Props for {@link Textarea} — all native `<textarea>` attributes plus the
5
- * `asChild` escape hatch and a typed `ref`.
4
+ * Props for {@link Textarea} — all native `<textarea>` attributes (with `ref`
5
+ * typed as `Ref<HTMLTextAreaElement>`) plus the `asChild` escape hatch.
6
6
  */
7
- export type TextareaProps = ComponentProps<"textarea"> & {
7
+ export type TextareaProps = Omit<ComponentProps<"textarea">, "ref"> & {
8
+ /**
9
+ * Renders the child element instead of a native `<textarea>`, merging all
10
+ * props — aria-*, data-*, event handlers, ref — onto it via {@link Slot}.
11
+ * Event handlers compose (child runs first). Use this to wrap a third-party
12
+ * autosizing textarea while keeping this component's prop contract.
13
+ * @default false
14
+ */
8
15
  asChild?: boolean;
16
+ /**
17
+ * Forwarded to the underlying `HTMLTextAreaElement`. Under `asChild`, merged
18
+ * onto the rendered child via {@link Slot}.
19
+ */
9
20
  ref?: Ref<HTMLTextAreaElement>;
21
+ /**
22
+ * Child content. Under `asChild`, must be a single React element that
23
+ * {@link Slot} merges props onto. Not used when rendering a native
24
+ * `<textarea>` (the element's content is controlled by `value` /
25
+ * `defaultValue`, not children).
26
+ */
10
27
  children?: ReactNode;
11
28
  };
@@ -11,24 +11,50 @@ import { ToggleProps } from "./types";
11
11
  * with the `aria-pressed` attribute. Renders a native
12
12
  * `<button type="button">` by default.
13
13
  *
14
- * Supports two state modes, statically discriminated at the type level:
14
+ * **Controlled vs uncontrolled.** Two state modes, statically discriminated at
15
+ * the type level so TypeScript rejects mixing them:
15
16
  *
16
17
  * - **Uncontrolled** — pass
17
- * {@link ToggleProps.defaultPressed | `defaultPressed`} (or omit for
18
- * unpressed on mount). The component owns the value internally.
19
- * - **Controlled** — pass
20
- * {@link ToggleProps.pressed | `pressed`} *and*
21
- * {@link ToggleProps.onPressedChange | `onPressedChange`} together.
22
- * The parent owns the value; every click defers back through the callback.
18
+ * {@link ToggleProps.defaultPressed | `defaultPressed`} (or omit entirely
19
+ * for unpressed on mount). The component owns the pressed value internally.
20
+ * - **Controlled** — pass {@link ToggleProps.pressed | `pressed`} *and*
21
+ * {@link ToggleProps.onPressedChange | `onPressedChange`} together. The
22
+ * parent owns the value; every click defers back through the callback.
23
23
  *
24
- * **ARIA.** `aria-pressed` is set automatically to reflect the pressed state.
24
+ * **Keyboard support.** Keyboard activation is provided natively by the
25
+ * underlying `<button>` element — no custom `keydown` listeners are needed:
25
26
  *
26
- * **Styling hooks.** `data-state="on" | "off"` on the element, plus
27
- * `data-disabled=""` when disabled.
27
+ * | Key | Behaviour |
28
+ * | ------- | -------------------------------------- |
29
+ * | `Space` | Toggles the button (native `<button>`) |
30
+ * | `Enter` | Toggles the button (native `<button>`) |
28
31
  *
29
- * **`asChild` prop.** Pass `asChild` to render any consumer-supplied element
30
- * with the toggle's `aria-pressed`, `data-state`, composed `onClick`, and
31
- * `ref` merged in. The native `<button>` is dropped.
32
+ * **Styling hooks.**
33
+ * - `data-state="on" | "off"` — mirrors the pressed state; use
34
+ * `[data-state="on"]` to style the active appearance.
35
+ * - `data-disabled=""` — set when `disabled` is `true`; lets CSS target
36
+ * `[data-disabled]` without relying on the `:disabled` pseudo-class.
37
+ *
38
+ * **`asChild` composition.** Pass `asChild` to render any consumer-supplied
39
+ * element instead of the default `<button>`. The toggle's `aria-pressed`,
40
+ * `data-state`, composed `onClick`, and `ref` are merged onto the child via
41
+ * {@link Slot}. The native `<button>` is dropped; the consumer is responsible
42
+ * for making the element focusable (e.g. `tabIndex={0}`, `role="button"`).
43
+ *
44
+ * **Ref forwarding.** Pass a `ref` prop to access the underlying
45
+ * `HTMLButtonElement` directly (or the `asChild` element when `asChild` is
46
+ * set):
47
+ *
48
+ * ```tsx
49
+ * const ref = useRef<HTMLButtonElement>(null);
50
+ * <Toggle ref={ref} aria-label="Bold">B</Toggle>
51
+ * ```
52
+ *
53
+ * **Disabled.** Sets the native `disabled` attribute (removing the button
54
+ * from the tab order and suppressing clicks) **and** sets `data-disabled=""`
55
+ * so CSS can target `[data-disabled]` without relying on `:disabled`.
56
+ *
57
+ * @extends HTMLButtonElement
32
58
  *
33
59
  * @example Uncontrolled
34
60
  * ```tsx
@@ -41,6 +67,18 @@ import { ToggleProps } from "./types";
41
67
  *
42
68
  * <Toggle pressed={bold} onPressedChange={setBold} aria-label="Bold">B</Toggle>
43
69
  * ```
70
+ *
71
+ * @example Disabled
72
+ * ```tsx
73
+ * <Toggle disabled aria-label="Bold (unavailable)">B</Toggle>
74
+ * ```
75
+ *
76
+ * @example asChild — render a custom element with toggle semantics
77
+ * ```tsx
78
+ * <Toggle asChild aria-label="Bold">
79
+ * <div role="button" tabIndex={0}>B</div>
80
+ * </Toggle>
81
+ * ```
44
82
  */
45
83
  function Toggle({
46
84
  defaultPressed,
@@ -25,4 +25,9 @@ describe("Toggle basic rendering", () => {
25
25
  "off",
26
26
  );
27
27
  });
28
+
29
+ it('sets displayName to "Toggle" for React DevTools and stack traces', () => {
30
+ // Assert — an empty displayName would render it as an anonymous component.
31
+ expect(Toggle.displayName).toBe("Toggle");
32
+ });
28
33
  });
@@ -1,27 +1,77 @@
1
1
  import { ButtonHTMLAttributes, ReactNode, Ref } from "react";
2
2
 
3
- /** Uncontrolled `Toggle` props: seed with `defaultPressed`; `pressed`/`onPressedChange` are disallowed. */
3
+ /**
4
+ * Uncontrolled {@link Toggle} props: seed the initial pressed state with
5
+ * `defaultPressed`; `pressed` and `onPressedChange` are disallowed in this
6
+ * mode. The component owns the pressed value internally.
7
+ */
4
8
  export type UncontrolledToggleProps = {
9
+ /**
10
+ * The pressed state on first render. Omit to start unpressed.
11
+ * Mutually exclusive with {@link ControlledToggleProps.pressed | `pressed`}.
12
+ * @default false
13
+ */
5
14
  defaultPressed?: boolean;
6
15
  pressed?: never;
7
16
  onPressedChange?: never;
8
17
  };
9
18
 
10
- /** Controlled `Toggle` props: drive with `pressed` and `onPressedChange`; `defaultPressed` is disallowed. */
19
+ /**
20
+ * Controlled {@link Toggle} props: drive the pressed state with `pressed` and
21
+ * respond to toggle requests via `onPressedChange`; `defaultPressed` is
22
+ * disallowed in this mode. The parent owns the value.
23
+ */
11
24
  export type ControlledToggleProps = {
25
+ /**
26
+ * The current pressed state, owned by the caller. Every click fires
27
+ * {@link ControlledToggleProps.onPressedChange | `onPressedChange`} with the
28
+ * next value — the component does not update state itself.
29
+ */
12
30
  pressed: boolean;
31
+ /**
32
+ * Called with the next pressed value when the user clicks the toggle.
33
+ * The caller is responsible for updating the `pressed` prop accordingly.
34
+ */
13
35
  onPressedChange: (pressed: boolean) => void;
14
36
  defaultPressed?: never;
15
37
  };
16
38
 
17
- /** Props common to both `Toggle` modes: `disabled`, `asChild`, `children`, `ref`, and the native `<button>` attributes (minus `type`). */
39
+ /**
40
+ * Props common to both controlled and uncontrolled {@link Toggle} modes:
41
+ * `disabled`, `asChild`, `children`, `ref`, and all native `<button>`
42
+ * attributes except `type` (which is fixed to `"button"` by the component).
43
+ */
18
44
  export type ToggleBaseProps = {
45
+ /**
46
+ * When `true`, sets the native `disabled` attribute (removing the button
47
+ * from the tab order and suppressing clicks) and adds `data-disabled=""`
48
+ * so CSS can target `[data-disabled]` without relying on `:disabled`.
49
+ */
19
50
  disabled?: boolean;
51
+ /**
52
+ * Renders the child element instead of the default `<button>`, merging
53
+ * `aria-pressed`, `data-state`, the composed `onClick`, and `ref` onto
54
+ * it via {@link Slot}. The child must be focusable (e.g. carry
55
+ * `tabIndex={0}` and `role="button"`).
56
+ * @default false
57
+ */
20
58
  asChild?: boolean;
59
+ /** Toggle content — text, icons, or any React node. */
21
60
  children?: ReactNode;
61
+ /**
62
+ * Forwarded to the underlying `HTMLButtonElement`, or — under
63
+ * `asChild` — merged onto the rendered child element.
64
+ */
22
65
  ref?: Ref<HTMLButtonElement>;
23
66
  } & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type">;
24
67
 
25
- /** Props for the `Toggle` button — base props combined with the controlled/uncontrolled discriminated union. */
68
+ /**
69
+ * Props for the {@link Toggle} button — {@link ToggleBaseProps} combined with
70
+ * the controlled/uncontrolled discriminated union
71
+ * ({@link ControlledToggleProps} | {@link UncontrolledToggleProps}).
72
+ *
73
+ * TypeScript statically rejects mixing controlled and uncontrolled props
74
+ * (e.g. passing both `pressed` and `defaultPressed`).
75
+ */
26
76
  export type ToggleProps = ToggleBaseProps &
27
77
  (UncontrolledToggleProps | ControlledToggleProps);
@@ -30,13 +30,13 @@ At most one item is pressed at a time. Pressing the active item again
30
30
  ```tsx
31
31
  // Uncontrolled
32
32
  <ToggleGroup.Root type="single" defaultValue="center" aria-label="Alignment">
33
- …
33
+ ...
34
34
  </ToggleGroup.Root>
35
35
 
36
36
  // Controlled — onValueChange receives string | undefined
37
37
  const [align, setAlign] = useState<string | undefined>("center");
38
38
  <ToggleGroup.Root type="single" value={align} onValueChange={setAlign} aria-label="Alignment">
39
- …
39
+ ...
40
40
  </ToggleGroup.Root>
41
41
  ```
42
42
 
@@ -48,13 +48,13 @@ independently.
48
48
  ```tsx
49
49
  // Uncontrolled
50
50
  <ToggleGroup.Root type="multiple" defaultValue={["bold"]} aria-label="Formatting">
51
- …
51
+ ...
52
52
  </ToggleGroup.Root>
53
53
 
54
54
  // Controlled — onValueChange receives string[]
55
55
  const [formats, setFormats] = useState<string[]>([]);
56
56
  <ToggleGroup.Root type="multiple" value={formats} onValueChange={setFormats} aria-label="Formatting">
57
- …
57
+ ...
58
58
  </ToggleGroup.Root>
59
59
  ```
60
60