@primitiv-ui/react 0.1.27 → 0.1.29

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 (307) hide show
  1. package/README.md +4 -0
  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 +25 -4
  7. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  8. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  9. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  10. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  11. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  12. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  13. package/src/Accordion/types.ts +72 -21
  14. package/src/Alert/Alert.tsx +36 -7
  15. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  16. package/src/Alert/types.ts +18 -2
  17. package/src/Avatar/Avatar.tsx +99 -15
  18. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  19. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  20. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  21. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  22. package/src/Avatar/types.ts +15 -3
  23. package/src/Breadcrumb/Breadcrumb.tsx +136 -18
  24. package/src/Breadcrumb/README.md +47 -0
  25. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  27. package/src/Breadcrumb/types.ts +73 -7
  28. package/src/Button/Button.tsx +2 -0
  29. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  30. package/src/Button/types.ts +21 -0
  31. package/src/Carousel/Carousel.tsx +263 -15
  32. package/src/Carousel/README.md +749 -72
  33. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  34. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  35. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  36. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  37. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  38. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  39. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
  40. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  41. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  42. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  43. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  44. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  45. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  46. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  47. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  48. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  49. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  50. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  51. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  52. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  53. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  54. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  55. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  56. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  57. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  58. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  59. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  60. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  61. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  62. package/src/Carousel/hooks/index.ts +1 -0
  63. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  64. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  65. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  66. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  67. package/src/Carousel/loopEngine.ts +54 -0
  68. package/src/Carousel/types.ts +457 -35
  69. package/src/Checkbox/Checkbox.tsx +9 -1
  70. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  71. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  72. package/src/Checkbox/types.ts +19 -0
  73. package/src/CheckboxCard/CheckboxCard.tsx +13 -4
  74. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  75. package/src/CheckboxCard/types.ts +65 -17
  76. package/src/Collapsible/Collapsible.tsx +49 -3
  77. package/src/Collapsible/README.md +40 -0
  78. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  79. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  80. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  81. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  82. package/src/Collapsible/types.ts +89 -22
  83. package/src/ContextMenu/ContextMenu.tsx +270 -4
  84. package/src/ContextMenu/types.ts +148 -6
  85. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  86. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  87. package/src/DirectionProvider/types.ts +28 -3
  88. package/src/Divider/Divider.tsx +24 -10
  89. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  90. package/src/Divider/types.ts +10 -3
  91. package/src/Drawer/Drawer.tsx +315 -0
  92. package/src/Drawer/README.md +124 -0
  93. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  94. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  95. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  96. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  97. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  98. package/src/Drawer/index.ts +2 -0
  99. package/src/Drawer/types.ts +66 -0
  100. package/src/Dropdown/Dropdown.tsx +117 -0
  101. package/src/Dropdown/types.ts +241 -11
  102. package/src/EmptyState/EmptyState.tsx +74 -4
  103. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  104. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  105. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  106. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  107. package/src/EmptyState/types.ts +49 -6
  108. package/src/Field/Field.tsx +23 -0
  109. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  110. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  111. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  112. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  113. package/src/Field/types.ts +60 -12
  114. package/src/Fieldset/Fieldset.tsx +54 -18
  115. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  116. package/src/Fieldset/types.ts +20 -2
  117. package/src/Input/Input.tsx +14 -9
  118. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  119. package/src/Input/types.ts +19 -2
  120. package/src/InputGroup/InputGroup.tsx +11 -3
  121. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  122. package/src/InputGroup/types.ts +26 -5
  123. package/src/MillerColumns/MillerColumns.tsx +86 -19
  124. package/src/MillerColumns/types.ts +83 -27
  125. package/src/Modal/Modal.tsx +31 -4
  126. package/src/Modal/README.md +19 -10
  127. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  128. package/src/Modal/hooks/useModalContent.ts +32 -0
  129. package/src/Modal/types.ts +110 -29
  130. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  131. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  132. package/src/NavigationMenu/README.md +355 -0
  133. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  134. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  135. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  136. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  137. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  138. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  139. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  140. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  141. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  142. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  143. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  144. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  145. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  146. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  147. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  148. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  149. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  150. package/src/NavigationMenu/constants.ts +5 -0
  151. package/src/NavigationMenu/hooks/index.ts +7 -0
  152. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  153. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  154. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  155. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  156. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  157. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  158. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  159. package/src/NavigationMenu/index.ts +3 -0
  160. package/src/NavigationMenu/types.ts +259 -0
  161. package/src/NavigationMenu/utils.ts +89 -0
  162. package/src/Popover/Popover.tsx +427 -0
  163. package/src/Popover/PopoverContext.ts +13 -0
  164. package/src/Popover/README.md +106 -0
  165. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  166. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  167. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  168. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  169. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  170. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  171. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  172. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  173. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  174. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  175. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  176. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  177. package/src/Popover/hooks/index.ts +3 -0
  178. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  179. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  180. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  181. package/src/Popover/index.ts +2 -0
  182. package/src/Popover/types.ts +197 -0
  183. package/src/Portal/Portal.tsx +50 -10
  184. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  185. package/src/Portal/types.ts +5 -0
  186. package/src/Progress/Progress.tsx +33 -1
  187. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  188. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  189. package/src/Progress/types.ts +20 -4
  190. package/src/Radio/Radio.tsx +9 -1
  191. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  192. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  193. package/src/Radio/types.ts +20 -0
  194. package/src/RadioCard/RadioCard.tsx +11 -0
  195. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  196. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  197. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  198. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  199. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  200. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  201. package/src/RadioCard/types.ts +95 -22
  202. package/src/RadioGroup/RadioGroup.tsx +11 -0
  203. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  204. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  205. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  206. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  207. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  208. package/src/RadioGroup/types.ts +73 -9
  209. package/src/SegmentedControl/README.md +154 -0
  210. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  211. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  212. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  213. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  214. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  215. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  216. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  217. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  218. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  219. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  220. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  221. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  222. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  223. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  224. package/src/SegmentedControl/hooks/index.ts +2 -0
  225. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  226. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
  227. package/src/SegmentedControl/index.ts +2 -0
  228. package/src/SegmentedControl/types.ts +138 -0
  229. package/src/Select/README.md +122 -157
  230. package/src/Select/Select.tsx +595 -68
  231. package/src/Select/SelectContext.ts +38 -0
  232. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  233. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  234. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  235. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  236. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  237. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  238. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  239. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  240. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  241. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  242. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  243. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  244. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  245. package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
  246. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  247. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  248. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  249. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  250. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  251. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  252. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  253. package/src/Select/constants.ts +8 -0
  254. package/src/Select/hooks/index.ts +2 -0
  255. package/src/Select/hooks/useSelectContent.ts +149 -0
  256. package/src/Select/hooks/useSelectRoot.ts +149 -0
  257. package/src/Select/types.ts +199 -31
  258. package/src/SkipNav/SkipNav.tsx +69 -29
  259. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  260. package/src/SkipNav/types.ts +17 -9
  261. package/src/Slider/Slider.tsx +33 -1
  262. package/src/Slider/SliderContext.ts +28 -1
  263. package/src/Slider/types.ts +157 -9
  264. package/src/Slot/Slot.tsx +94 -19
  265. package/src/Slot/__tests__/Slot.test.tsx +110 -0
  266. package/src/Slot/composeEventHandlers.ts +26 -2
  267. package/src/Slot/types.ts +16 -0
  268. package/src/Status/Status.tsx +36 -15
  269. package/src/Status/__tests__/Status.test.tsx +5 -0
  270. package/src/Status/types.ts +12 -1
  271. package/src/Switch/Switch.tsx +15 -2
  272. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  273. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  274. package/src/Switch/types.ts +49 -22
  275. package/src/Table/Table.tsx +44 -10
  276. package/src/Table/types.ts +66 -17
  277. package/src/Tabs/Tabs.tsx +8 -0
  278. package/src/Tabs/types.ts +29 -4
  279. package/src/Textarea/Textarea.tsx +9 -1
  280. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  281. package/src/Textarea/types.ts +20 -3
  282. package/src/Toggle/Toggle.tsx +51 -13
  283. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  284. package/src/Toggle/types.ts +54 -4
  285. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  286. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  287. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  288. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  289. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  290. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  291. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  292. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  293. package/src/ToggleGroup/types.ts +55 -2
  294. package/src/Tooltip/Tooltip.tsx +98 -20
  295. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  296. package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
  297. package/src/Tooltip/types.ts +89 -8
  298. package/src/Tree/Tree.tsx +187 -33
  299. package/src/Tree/types.ts +54 -12
  300. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  301. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  302. package/src/VisuallyHidden/types.ts +11 -1
  303. package/src/index.ts +4 -0
  304. package/src/test/intersectionObserverPolyfill.ts +7 -0
  305. package/src/test/resizeObserverPolyfill.ts +80 -0
  306. package/src/utils/focusable.ts +15 -0
  307. package/src/utils/index.ts +1 -0
@@ -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);
@@ -46,6 +46,8 @@ import { ToggleGroupItemProps, ToggleGroupRootProps } from "./types";
46
46
  * element in place of the native `<div>`, with the group's `role`,
47
47
  * `data-orientation`, and ref merged in.
48
48
  *
49
+ * @extends HTMLDivElement
50
+ *
49
51
  * @example Single mode — text alignment
50
52
  * ```tsx
51
53
  * <ToggleGroup.Root type="single" defaultValue="left" aria-label="Alignment">
@@ -140,6 +142,7 @@ export function ToggleGroupRoot({
140
142
  }
141
143
 
142
144
  /** @internal */
145
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
143
146
  ToggleGroupRoot.displayName = "ToggleGroupRoot";
144
147
 
145
148
  /**
@@ -165,9 +168,19 @@ ToggleGroupRoot.displayName = "ToggleGroupRoot";
165
168
  *
166
169
  * **`asChild` prop.** Pass `asChild` to render any consumer element in
167
170
  * place of the native `<button>`, with the Item's `aria-pressed`,
168
- * `data-state`, `tabIndex`, and event handlers merged onto it.
171
+ * `data-state`, `tabIndex`, and event handlers merged onto it. The child
172
+ * must accept a `ref`. Useful for a link or a styled control that needs
173
+ * toggle-button semantics.
169
174
  *
170
175
  * @throws if rendered outside a `ToggleGroup.Root`.
176
+ *
177
+ * @extends HTMLButtonElement
178
+ *
179
+ * @example
180
+ * ```tsx
181
+ * <ToggleGroup.Item value="bold">Bold</ToggleGroup.Item>
182
+ * <ToggleGroup.Item value="strike" disabled>Strikethrough</ToggleGroup.Item>
183
+ * ```
171
184
  */
172
185
  export function ToggleGroupItem({
173
186
  value,
@@ -62,4 +62,24 @@ describe("ToggleGroup asChild composition", () => {
62
62
  expect(consumerClick).toHaveBeenCalledOnce();
63
63
  expect(item).toHaveAttribute("aria-pressed", "true");
64
64
  });
65
+
66
+ it("Item asChild activates on Space through the keyboard handler", async () => {
67
+ // A non-button element fires no native click on Space, so toggling can
68
+ // only come from the roving handler's activate mapping — the guarantee a
69
+ // native <button> would otherwise mask.
70
+ const user = userEvent.setup();
71
+ render(
72
+ <ToggleGroup.Root type="single" aria-label="Alignment">
73
+ <ToggleGroup.Item value="left" asChild>
74
+ <div>Left</div>
75
+ </ToggleGroup.Item>
76
+ </ToggleGroup.Root>,
77
+ );
78
+ const item = screen
79
+ .getByLabelText("Alignment")
80
+ .querySelector<HTMLElement>("[data-state]")!;
81
+ item.focus();
82
+ await user.keyboard(" ");
83
+ expect(item).toHaveAttribute("aria-pressed", "true");
84
+ });
65
85
  });
@@ -47,4 +47,13 @@ describe("ToggleGroup basic rendering", () => {
47
47
  "horizontal",
48
48
  );
49
49
  });
50
+
51
+ it("sets a displayName on the compound and Item", () => {
52
+ // Assert — empty displayNames would render each as anonymous in DevTools.
53
+ // `ToggleGroup`, `ToggleGroup.Root`, and the underlying function are one
54
+ // object (Object.assign compound), so the compound's "ToggleGroup" alias is
55
+ // the observable Root displayName; only Item is a distinct object.
56
+ expect(ToggleGroup.displayName).toBe("ToggleGroup");
57
+ expect(ToggleGroup.Item.displayName).toBe("ToggleGroupItem");
58
+ });
50
59
  });
@@ -0,0 +1,70 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { ToggleGroup } from "../ToggleGroup";
5
+
6
+ function ItemsFixture({ items }: { items: string[] }) {
7
+ return (
8
+ <ToggleGroup.Root type="single" aria-label="Alignment">
9
+ {items.map((v) => (
10
+ <ToggleGroup.Item key={v} value={v}>
11
+ {v}
12
+ </ToggleGroup.Item>
13
+ ))}
14
+ </ToggleGroup.Root>
15
+ );
16
+ }
17
+
18
+ function DisabledFixture({ disabledItem }: { disabledItem?: string }) {
19
+ return (
20
+ <ToggleGroup.Root type="single" aria-label="Alignment">
21
+ <ToggleGroup.Item value="left" disabled={disabledItem === "left"}>
22
+ Left
23
+ </ToggleGroup.Item>
24
+ <ToggleGroup.Item value="center" disabled={disabledItem === "center"}>
25
+ Center
26
+ </ToggleGroup.Item>
27
+ <ToggleGroup.Item value="right" disabled={disabledItem === "right"}>
28
+ Right
29
+ </ToggleGroup.Item>
30
+ </ToggleGroup.Root>
31
+ );
32
+ }
33
+
34
+ describe("ToggleGroup dynamic items — registration lifecycle", () => {
35
+ it("unregisters an unmounted item so the tab stop moves to the survivor", () => {
36
+ // Arrange — "a" is the first enabled item, so it holds the tab stop.
37
+ const { rerender } = render(<ItemsFixture items={["a", "b"]} />);
38
+ expect(screen.getByRole("button", { name: "a" })).toHaveAttribute(
39
+ "tabindex",
40
+ "0",
41
+ );
42
+
43
+ // Act — remove "a"; its effect cleanup must unregister it.
44
+ rerender(<ItemsFixture items={["b"]} />);
45
+
46
+ // Assert — with "a" gone from the registry, "b" is now the tab stop. A
47
+ // no-op cleanup would leave "a" registered ahead of "b", stranding the
48
+ // tab stop on the detached element and leaving "b" at tabindex -1.
49
+ expect(screen.getByRole("button", { name: "b" })).toHaveAttribute(
50
+ "tabindex",
51
+ "0",
52
+ );
53
+ });
54
+
55
+ it("re-registers an item's disabled flag when it changes after mount", async () => {
56
+ // Arrange — all three enabled; focus can travel Left → Center → Right.
57
+ const user = userEvent.setup();
58
+ const { rerender } = render(<DisabledFixture />);
59
+
60
+ // Act — disable Center after mount, then navigate rightward from Left.
61
+ rerender(<DisabledFixture disabledItem="center" />);
62
+ screen.getByRole("button", { name: "Left" }).focus();
63
+ await user.keyboard("{ArrowRight}");
64
+
65
+ // Assert — the registrar effect re-ran, so Center is skipped and focus
66
+ // lands on Right. A stale registration would keep Center navigable and
67
+ // strand focus (a disabled button cannot receive it).
68
+ expect(screen.getByRole("button", { name: "Right" })).toHaveFocus();
69
+ });
70
+ });
@@ -0,0 +1,23 @@
1
+ import { render } from "@testing-library/react";
2
+
3
+ import { ToggleGroup } from "../ToggleGroup";
4
+ import { ToggleGroupContext } from "../ToggleGroupContext";
5
+
6
+ describe("ToggleGroup error handling", () => {
7
+ it("throws a helpful error when ToggleGroup.Item is rendered outside Root", () => {
8
+ // Arrange
9
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
10
+
11
+ // Assert
12
+ expect(() =>
13
+ render(<ToggleGroup.Item value="left">Left</ToggleGroup.Item>),
14
+ ).toThrow(/ToggleGroup\.Item must be rendered inside a ToggleGroup\.Root/);
15
+
16
+ error.mockRestore();
17
+ });
18
+
19
+ it("names the context for React DevTools", () => {
20
+ // Assert — an empty displayName would render the provider as anonymous.
21
+ expect(ToggleGroupContext.displayName).toBe("ToggleGroupContext");
22
+ });
23
+ });
@@ -1,4 +1,4 @@
1
- import { render, screen } from "@testing-library/react";
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
 
4
4
  import { ToggleGroup } from "../ToggleGroup";
@@ -45,6 +45,28 @@ describe("ToggleGroup keyboard — roving tabindex", () => {
45
45
  "-1",
46
46
  );
47
47
  });
48
+
49
+ it("moves the tab stop to the focused item (onFocus updates the home base)", () => {
50
+ // Arrange — nothing selected, so the first item is the initial tab stop.
51
+ render(<SingleFixture />);
52
+ expect(screen.getByRole("button", { name: "Left" })).toHaveAttribute(
53
+ "tabindex",
54
+ "0",
55
+ );
56
+
57
+ // Act — focusing another item should hand it the tab stop.
58
+ fireEvent.focus(screen.getByRole("button", { name: "Right" }));
59
+
60
+ // Assert — Right is now the sole tab stop; Left drops out of the sequence.
61
+ expect(screen.getByRole("button", { name: "Right" })).toHaveAttribute(
62
+ "tabindex",
63
+ "0",
64
+ );
65
+ expect(screen.getByRole("button", { name: "Left" })).toHaveAttribute(
66
+ "tabindex",
67
+ "-1",
68
+ );
69
+ });
48
70
  });
49
71
 
50
72
  describe("ToggleGroup keyboard — arrow key navigation (no auto-select)", () => {
@@ -0,0 +1,58 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { ToggleGroup } from "../ToggleGroup";
4
+
5
+ describe("ToggleGroup.Item ref forwarding", () => {
6
+ it("forwards the underlying button element to a consumer function ref", () => {
7
+ // Arrange
8
+ const received: (HTMLButtonElement | null)[] = [];
9
+ const functionRef = (node: HTMLButtonElement | null) => {
10
+ received.push(node);
11
+ };
12
+
13
+ // Act
14
+ render(
15
+ <ToggleGroup.Root type="single" aria-label="Alignment">
16
+ <ToggleGroup.Item value="left" ref={functionRef}>
17
+ Left
18
+ </ToggleGroup.Item>
19
+ </ToggleGroup.Root>,
20
+ );
21
+
22
+ // Assert
23
+ expect(received).toContain(screen.getByRole("button", { name: "Left" }));
24
+ });
25
+
26
+ it("re-attaches to a replacement ref when the ref prop changes", () => {
27
+ // Arrange — the item's ref memo must depend on `ref`, or a changed ref
28
+ // callback is never invoked with the element.
29
+ const firstReceived: (HTMLButtonElement | null)[] = [];
30
+ const secondReceived: (HTMLButtonElement | null)[] = [];
31
+ const firstRef = (node: HTMLButtonElement | null) => {
32
+ firstReceived.push(node);
33
+ };
34
+ const secondRef = (node: HTMLButtonElement | null) => {
35
+ secondReceived.push(node);
36
+ };
37
+
38
+ // Act — mount with the first ref, then re-render with a different one.
39
+ const { rerender } = render(
40
+ <ToggleGroup.Root type="single" aria-label="Alignment">
41
+ <ToggleGroup.Item value="left" ref={firstRef}>
42
+ Left
43
+ </ToggleGroup.Item>
44
+ </ToggleGroup.Root>,
45
+ );
46
+ rerender(
47
+ <ToggleGroup.Root type="single" aria-label="Alignment">
48
+ <ToggleGroup.Item value="left" ref={secondRef}>
49
+ Left
50
+ </ToggleGroup.Item>
51
+ </ToggleGroup.Root>,
52
+ );
53
+
54
+ // Assert — the replacement ref received the button.
55
+ const button = screen.getByRole("button", { name: "Left" });
56
+ expect(secondReceived).toContain(button);
57
+ });
58
+ });
@@ -42,7 +42,16 @@ export function useToggleGroupRoot({
42
42
  (itemValue: string) => {
43
43
  const isSelected = value.includes(itemValue);
44
44
  if (type === "single") {
45
- const next = isSelected ? [] : [itemValue];
45
+ // Deselecting clears the selection to the empty array. Stryker's
46
+ // ArrayDeclaration mutant fills this with a phantom value, but since
47
+ // that value can never equal a registered item's `value` — and single
48
+ // mode always overwrites the whole array on the next selection — no
49
+ // item ever reads it as pressed. The mutation is observationally
50
+ // identical, so it is isolated here to keep the disable off the tested
51
+ // `[itemValue]` literal below.
52
+ // Stryker disable next-line ArrayDeclaration
53
+ const cleared: string[] = [];
54
+ const next = isSelected ? cleared : [itemValue];
46
55
  setValue(next);
47
56
  (onValueChange as ((v: string | undefined) => void) | undefined)?.(
48
57
  isSelected ? undefined : itemValue,
@@ -68,6 +77,9 @@ export function useToggleGroupRoot({
68
77
  (itemValue: string, element: HTMLButtonElement | null, disabled = false) => {
69
78
  registerBase(itemValue, element ? { element, disabled } : null);
70
79
  },
80
+ // `registerBase` is a stable useCollection callback (its own deps never
81
+ // change), so emptying this array yields the identical memoised function.
82
+ // Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
71
83
  [registerBase],
72
84
  );
73
85
 
@@ -83,8 +95,16 @@ export function useToggleGroupRoot({
83
95
 
84
96
  const focusItem = useCallback(
85
97
  (itemValue: string) => {
98
+ // `itemValue` is always sourced from `navigable` (the enabled subset of
99
+ // `itemValues`), and useCollection's register() writes `itemsRef` and the
100
+ // `itemValues` state together — so a value that reaches here is guaranteed
101
+ // to have a live entry in the map.
102
+ // Stryker disable next-line OptionalChaining: unreachable given that invariant.
86
103
  itemsRef.current.get(itemValue)?.element.focus();
87
104
  },
105
+ // `itemsRef` is a stable RefObject, so emptying this array yields the
106
+ // identical memoised function.
107
+ // Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
88
108
  [itemsRef],
89
109
  );
90
110
 
@@ -7,12 +7,27 @@ import { HTMLAttributes, ButtonHTMLAttributes, ReactNode, Ref } from "react";
7
7
  */
8
8
  export type ToggleGroupRootBaseProps = Omit<
9
9
  HTMLAttributes<HTMLDivElement>,
10
- "dir"
10
+ "dir" | "defaultValue"
11
11
  > & {
12
+ /** Layout axis for keyboard navigation. `"horizontal"` binds
13
+ * ArrowLeft/ArrowRight; `"vertical"` binds ArrowUp/ArrowDown. Surfaces as
14
+ * `data-orientation` on the root for styling; it does not itself apply any
15
+ * flex/grid layout.
16
+ * @default "horizontal" */
12
17
  orientation?: "horizontal" | "vertical";
18
+ /** Reading direction. In `"rtl"` the horizontal arrow keys are mirrored so
19
+ * focus follows the visual order. Inherited from the nearest
20
+ * {@link DirectionProvider} when omitted, falling back to `"ltr"`. */
13
21
  dir?: "ltr" | "rtl";
22
+ /** Render a single consumer-supplied element in place of the native
23
+ * `<div>`, with the group's `role="group"`, `data-orientation`, and `ref`
24
+ * merged onto it via the {@link Slot} pattern.
25
+ * @default false */
14
26
  asChild?: boolean;
27
+ /** The {@link ToggleGroupItemProps | `ToggleGroup.Item`} elements that make
28
+ * up the segmented control. */
15
29
  children?: ReactNode;
30
+ /** Forwarded to the underlying `HTMLDivElement`. */
16
31
  ref?: Ref<HTMLDivElement>;
17
32
  };
18
33
 
@@ -21,9 +36,15 @@ export type ToggleGroupRootBaseProps = Omit<
21
36
  * initial value supplied via `defaultValue`.
22
37
  */
23
38
  export type SingleUncontrolledProps = {
39
+ /** Selects single-selection semantics: at most one item pressed at a time,
40
+ * and pressing the active item again clears the selection. */
24
41
  type: "single";
42
+ /** Value of the item pressed on first render. Omit to start with nothing
43
+ * pressed. */
25
44
  defaultValue?: string;
45
+ /** Forbidden in uncontrolled mode — use `defaultValue`. */
26
46
  value?: never;
47
+ /** Forbidden in uncontrolled mode. */
27
48
  onValueChange?: never;
28
49
  };
29
50
 
@@ -32,9 +53,16 @@ export type SingleUncontrolledProps = {
32
53
  * caller via `value` / `onValueChange`.
33
54
  */
34
55
  export type SingleControlledProps = {
56
+ /** Selects single-selection semantics: at most one item pressed at a time,
57
+ * and pressing the active item again clears the selection. */
35
58
  type: "single";
59
+ /** The currently pressed item's value, or `undefined` when none is pressed.
60
+ * Must be kept in sync by the caller via `onValueChange`. */
36
61
  value: string | undefined;
62
+ /** Called with the requested next value — the pressed item's value, or
63
+ * `undefined` when the active item is pressed again to deselect it. */
37
64
  onValueChange: (value: string | undefined) => void;
65
+ /** Forbidden in controlled mode — use `value`. */
38
66
  defaultValue?: never;
39
67
  };
40
68
 
@@ -43,9 +71,15 @@ export type SingleControlledProps = {
43
71
  * the initial set supplied via `defaultValue`.
44
72
  */
45
73
  export type MultipleUncontrolledProps = {
74
+ /** Selects multiple-selection semantics: any number of items can be pressed
75
+ * simultaneously and each toggles independently. */
46
76
  type: "multiple";
77
+ /** Values of the items pressed on first render. Omit to start with nothing
78
+ * pressed. */
47
79
  defaultValue?: string[];
80
+ /** Forbidden in uncontrolled mode — use `defaultValue`. */
48
81
  value?: never;
82
+ /** Forbidden in uncontrolled mode. */
49
83
  onValueChange?: never;
50
84
  };
51
85
 
@@ -54,9 +88,16 @@ export type MultipleUncontrolledProps = {
54
88
  * by the caller via `value` / `onValueChange`.
55
89
  */
56
90
  export type MultipleControlledProps = {
91
+ /** Selects multiple-selection semantics: any number of items can be pressed
92
+ * simultaneously and each toggles independently. */
57
93
  type: "multiple";
94
+ /** The full set of currently pressed item values. Must be kept in sync by
95
+ * the caller via `onValueChange`. */
58
96
  value: string[];
97
+ /** Called with the complete next array of pressed values whenever the user
98
+ * toggles any item. */
59
99
  onValueChange: (value: string[]) => void;
100
+ /** Forbidden in controlled mode — use `value`. */
60
101
  defaultValue?: never;
61
102
  };
62
103
 
@@ -81,11 +122,23 @@ export type ToggleGroupRootProps = ToggleGroupRootBaseProps &
81
122
  */
82
123
  export type ToggleGroupItemProps = Omit<
83
124
  ButtonHTMLAttributes<HTMLButtonElement>,
84
- "type"
125
+ "type" | "value"
85
126
  > & {
127
+ /** Identifies this item within the group. It is this string — not the
128
+ * visible label — that is compared against the group's pressed
129
+ * `value` / `defaultValue`; use `children` for the label. */
86
130
  value: string;
131
+ /** Forwards the native `disabled` attribute and removes the item from the
132
+ * roving tab order so arrow-key navigation skips it. Also sets
133
+ * `data-disabled=""` for CSS targeting.
134
+ * @default false */
87
135
  disabled?: boolean;
136
+ /** Render a single consumer-supplied element in place of the native
137
+ * `<button>`, with the item's `aria-pressed`, `data-state`, `tabIndex`, and
138
+ * event handlers merged onto it via the {@link Slot} pattern.
139
+ * @default false */
88
140
  asChild?: boolean;
141
+ /** Forwarded to the underlying `HTMLButtonElement`. */
89
142
  ref?: Ref<HTMLButtonElement>;
90
143
  };
91
144