@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
@@ -1,50 +1,67 @@
1
1
  import { ChangeEventHandler, ComponentProps, ReactNode, Ref } from "react";
2
2
 
3
3
  /**
4
- * Props common to both controlled and uncontrolled `Switch.Root` modes — the
5
- * native `<input type="checkbox">` attributes (minus the ones the component
6
- * owns). `className` / `style` style the **track** (the visible control), not
7
- * the hidden input; everything else spreads onto the input, because
8
- * semantically the Root *is* the switch.
4
+ * Shared base for both {@link SwitchRootProps} variants — the native
5
+ * `<input type="checkbox">` attributes (minus the ones the component owns:
6
+ * `type`, `role`, `checked`, and `defaultChecked`), plus `children` and a
7
+ * typed `ref`.
8
+ *
9
+ * Prop routing: `className` / `style` style the **track** (the visible
10
+ * `<label>`, since the input is visually hidden); every other prop — `name`,
11
+ * `value`, `id`, `aria-*`, `required`, `disabled`, `ref`, ... — spreads onto the
12
+ * hidden `<input>`, because semantically the Root *is* the switch.
9
13
  */
10
14
  export type SwitchRootBaseProps = Omit<
11
15
  ComponentProps<"input">,
12
16
  "type" | "role" | "checked" | "defaultChecked"
13
17
  > & {
14
- /** Optional content (typically `Switch.Thumb`). */
18
+ /** Content of the switch — typically a single
19
+ * {@link SwitchThumbProps | `Switch.Thumb`}. */
15
20
  children?: ReactNode;
16
- /** Ref to the underlying native `<input>` element. */
21
+ /** Forwarded to the underlying native `HTMLInputElement` (the real,
22
+ * visually-hidden checkbox), not the visible track. */
17
23
  ref?: Ref<HTMLInputElement>;
18
24
  };
19
25
 
20
26
  /**
21
- * Props for `Switch.Root` in uncontrolled mode — the **browser** owns the
22
- * checked state. Pass `defaultChecked` to set the initial value; `checked` is
23
- * forbidden.
27
+ * Uncontrolled variant of {@link SwitchRootProps}: the **browser** owns the
28
+ * checked state, so the switch participates in native form submission and
29
+ * reset. Pass `defaultChecked` (or omit it); `checked` is forbidden.
24
30
  */
25
31
  export type SwitchRootUncontrolledProps = SwitchRootBaseProps & {
26
- /** Initial checked state when uncontrolled. */
32
+ /** Checked state on first render. Defaults to `false` when omitted.
33
+ * @default false */
27
34
  defaultChecked?: boolean;
28
- /** Forbidden in uncontrolled mode. */
35
+ /** Forbidden in uncontrolled mode — use `defaultChecked` instead. */
29
36
  checked?: never;
30
- /** Called whenever the checked state changes. */
37
+ /** Called with the new boolean state whenever the switch toggles. Optional
38
+ * in uncontrolled mode. */
31
39
  onCheckedChange?: (checked: boolean) => void;
32
40
  };
33
41
 
34
42
  /**
35
- * Props for `Switch.Root` in controlled mode — the parent owns the checked
36
- * value. Pass `checked` and `onCheckedChange` together.
43
+ * Controlled variant of {@link SwitchRootProps}: the parent owns the checked
44
+ * value. Pass `checked` and `onCheckedChange` together; `defaultChecked` is
45
+ * forbidden.
37
46
  */
38
47
  export type SwitchRootControlledProps = SwitchRootBaseProps & {
39
- /** Forbidden in controlled mode. */
48
+ /** Forbidden in controlled mode — use `checked` instead. */
40
49
  defaultChecked?: never;
41
- /** The controlled checked state. */
50
+ /** The controlled checked state. Must be kept in sync by the parent via
51
+ * `onCheckedChange`. */
42
52
  checked: boolean;
43
- /** Called whenever the component requests a checked-state change. */
53
+ /** Called with the new boolean state whenever the user requests a toggle.
54
+ * Required in controlled mode. */
44
55
  onCheckedChange: (checked: boolean) => void;
45
56
  };
46
57
 
47
- /** Props for `Switch.Root` — discriminated controlled/uncontrolled union. */
58
+ /**
59
+ * Props for {@link SwitchRoot | `Switch.Root`}.
60
+ *
61
+ * Resolves to either {@link SwitchRootUncontrolledProps} or
62
+ * {@link SwitchRootControlledProps} — only one shape is accepted by TypeScript
63
+ * at a time.
64
+ */
48
65
  export type SwitchRootProps =
49
66
  | SwitchRootUncontrolledProps
50
67
  | SwitchRootControlledProps;
@@ -56,11 +73,21 @@ export type SwitchRootProps =
56
73
  */
57
74
  export type SwitchProps = SwitchRootProps;
58
75
 
59
- /** Props for `Switch.Thumb`, the sliding indicator inside the track. */
76
+ /**
77
+ * Props for {@link SwitchThumb | `Switch.Thumb`} — the decorative sliding
78
+ * indicator inside the track. All `HTMLSpanElement` attributes, plus the
79
+ * `asChild` escape hatch.
80
+ */
60
81
  export type SwitchThumbProps = ComponentProps<"span"> & {
61
- /** Optional thumb content. */
82
+ /** Optional thumb content (rarely needed — the thumb is usually styled
83
+ * purely with CSS). */
62
84
  children?: ReactNode;
63
- /** Render the child element instead of the default `<span>`. */
85
+ /**
86
+ * When `true`, render the consumer's own element as the thumb instead of the
87
+ * default `<span>`, merging `aria-hidden` and `data-state` onto it via the
88
+ * {@link Slot} pattern.
89
+ * @default false
90
+ */
64
91
  asChild?: boolean;
65
92
  };
66
93
 
@@ -51,6 +51,9 @@ import { Table } from "@primitiv-ui/react";
51
51
  | `Table.Cell` | `<td>` | Data cell. Accepts `colSpan` and `rowSpan`. |
52
52
  | `Table.ScrollArea` | `<div>` | Horizontal-scroll wrapper — see [Responsive scrolling](#responsive-scrolling). |
53
53
  | `Table.Caption` | `<caption>` | Visible table label — see [Caption](#caption). |
54
+ | `Table.Expandable` | *nothing* | Pairs a row with its detail row — see [Expandable rows](#expandable-rows). |
55
+ | `Table.ExpandTrigger` | `<button>` | The row's disclosure control; owns `aria-expanded` / `aria-controls`. |
56
+ | `Table.DetailRow` | `<tr>` | The revealed panel, as a `<td colSpan>`. `gutter` offsets it to the first data column. |
54
57
 
55
58
  ## Accessible headers
56
59
 
@@ -96,14 +99,14 @@ the table via the CSS `caption-side` property. Defaults to `"bottom"`.
96
99
  /* Caption below table (default) */
97
100
  }
98
101
  <Table.Root>
99
- <Table.Caption>Q1 sales by region</Table.Caption>…
102
+ <Table.Caption>Q1 sales by region</Table.Caption>...
100
103
  </Table.Root>;
101
104
 
102
105
  {
103
106
  /* Caption above table */
104
107
  }
105
108
  <Table.Root>
106
- <Table.Caption captionSide="top">Q1 sales by region</Table.Caption>…
109
+ <Table.Caption captionSide="top">Q1 sales by region</Table.Caption>...
107
110
  </Table.Root>;
108
111
  ```
109
112
 
@@ -111,7 +114,7 @@ When a visible caption is not desirable, use `aria-label` directly on
111
114
  `Table.Root` instead:
112
115
 
113
116
  ```tsx
114
- <Table.Root aria-label="Q1 sales by region">…</Table.Root>
117
+ <Table.Root aria-label="Q1 sales by region">...</Table.Root>
115
118
  ```
116
119
 
117
120
  ## Responsive scrolling
@@ -124,7 +127,7 @@ narrow viewports instead of overflowing or compressing columns.
124
127
 
125
128
  ```tsx
126
129
  <Table.ScrollArea>
127
- <Table.Root>…</Table.Root>
130
+ <Table.Root>...</Table.Root>
128
131
  </Table.ScrollArea>
129
132
  ```
130
133
 
@@ -133,7 +136,7 @@ base scroll styles, so you can layer additional styles freely:
133
136
 
134
137
  ```tsx
135
138
  <Table.ScrollArea style={{ borderRadius: "8px" }}>
136
- <Table.Root>…</Table.Root>
139
+ <Table.Root>...</Table.Root>
137
140
  </Table.ScrollArea>
138
141
  ```
139
142
 
@@ -144,14 +147,14 @@ separating logical row groups within a single table:
144
147
 
145
148
  ```tsx
146
149
  <Table.Root>
147
- <Table.Head>…</Table.Head>
150
+ <Table.Head>...</Table.Head>
148
151
  <Table.Body>
149
152
  {/* Group 1 */}
150
- <Table.Row>…</Table.Row>
153
+ <Table.Row>...</Table.Row>
151
154
  </Table.Body>
152
155
  <Table.Body>
153
156
  {/* Group 2 */}
154
- <Table.Row>…</Table.Row>
157
+ <Table.Row>...</Table.Row>
155
158
  </Table.Body>
156
159
  </Table.Root>
157
160
  ```
@@ -203,3 +206,102 @@ caption {
203
206
  ---
204
207
 
205
208
  [Back to @primitiv-ui/react](../../README.md)
209
+
210
+ ## Expandable rows
211
+
212
+ A row can open onto a detail panel. This is a **disclosure**, not a
213
+ [treegrid](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/): expanding into
214
+ hierarchical child *rows* is a different pattern with a full cell-navigation
215
+ keyboard model, and is deliberately out of scope here.
216
+
217
+ `Table.Expandable` renders no DOM of its own — it is a context provider, so both
218
+ `<tr>`s stay direct children of the `<tbody>` and the table's structure is
219
+ untouched.
220
+
221
+ ```tsx
222
+ <Table.Body>
223
+ <Table.Expandable expanded={open} onExpandedChange={setOpen}>
224
+ <Table.Row>
225
+ <Table.Cell>
226
+ <Table.ExpandTrigger>
227
+ <ChevronIcon aria-hidden="true" />
228
+ <span className="sr-only">Show details for Ada Lovelace</span>
229
+ </Table.ExpandTrigger>
230
+ </Table.Cell>
231
+ <Table.Cell>Ada Lovelace</Table.Cell>
232
+ </Table.Row>
233
+
234
+ <Table.DetailRow colSpan={2}>
235
+ <ContributorDetail />
236
+ </Table.DetailRow>
237
+ </Table.Expandable>
238
+ </Table.Body>
239
+ ```
240
+
241
+ Controlled via `expanded` + `onExpandedChange` (what an external table engine
242
+ drives), or uncontrolled with `defaultExpanded`.
243
+
244
+ ### Why the state is on the button
245
+
246
+ `role="row"` does list `aria-expanded` among its supported properties, but a
247
+ **button's** expanded state is announced universally while a row's is only
248
+ reliably announced inside a `role="treegrid"`. So the button owns
249
+ `aria-expanded` and `aria-controls`, and the detail row publishes the matching
250
+ `id`.
251
+
252
+ Give the trigger a name that identifies **its row**. A column of buttons all
253
+ called "Expand" is the same defect as a column of checkboxes all called
254
+ "Select": obvious on screen, useless in a screen reader's element list.
255
+
256
+ ### `colSpan` is required
257
+
258
+ It must cover every column the table renders, control columns included. It is a
259
+ prop rather than a count taken from context on purpose: counting would mean the
260
+ table registering its own columns, and an external engine already knows the
261
+ number (TanStack Table: `table.getVisibleLeafColumns().length`).
262
+
263
+ ### `gutter` aligns the panel with the first data column
264
+
265
+ A panel that starts at the table's edge, under the checkbox, reads as a new
266
+ section of the table rather than as an opened row. `gutter` is how many leading
267
+ columns to leave empty — the control columns in front of the data:
268
+
269
+ ```tsx
270
+ {/* select + expand columns, then eight data columns */}
271
+ <Table.DetailRow colSpan={10} gutter={2}>
272
+ ```
273
+
274
+ The panel spans what remains, so `colSpan` keeps its meaning: the table's full
275
+ column count, gutter included.
276
+
277
+ It renders **one** empty `<td colSpan={gutter}>`, not `gutter` of them — a
278
+ spanning cell takes the full width of the columns it covers, so the alignment is
279
+ identical and the row gains a single blank cell instead of several.
280
+
281
+ Aligning by empty cell rather than by a CSS indent is the point: the browser's
282
+ own table layout resolves the width, so it stays exact across every size,
283
+ density and control-column set. Indenting instead means summing control widths
284
+ and paddings by hand, which is right at one size and wrong at the other four —
285
+ measured drift of 7-21px across xs...xl before this existed.
286
+
287
+ ### `forceMount` and the row count
288
+
289
+ By default a collapsed detail row is `hidden`, so it is absent from the
290
+ accessibility tree — which matters more here than on a `<div>`-based disclosure,
291
+ because a mounted `<tr>` is still a **row**: 24 data rows each with a
292
+ force-mounted detail row would announce 48.
293
+
294
+ Pass `forceMount` to keep it mounted so CSS can transition it. While collapsed it
295
+ then carries `aria-hidden="true"`, so the announced row count stays truthful
296
+ either way.
297
+
298
+ ### Styling hooks
299
+
300
+ `data-state="open" | "closed"` on both the trigger and the detail row.
301
+
302
+ Note that the `display: grid` 0fr↔1fr open/close technique used by Accordion and
303
+ Collapsible **cannot** go on the `<tr>` or the `<td>` — `display: grid` on either
304
+ takes it out of the table layout algorithm and column alignment collapses with
305
+ it. Put the animated wrapper inside the cell, and zero the cell's block padding
306
+ so a closed row leaves no padded band. The registry `data-table` component does
307
+ this for you.
@@ -1,9 +1,19 @@
1
- import type { ReactElement } from "react";
1
+ import { useId, useMemo, type MouseEvent, type ReactElement } from "react";
2
2
 
3
+ import { useControllableState } from "../hooks/index.ts";
4
+ import { Slot } from "../Slot/index.ts";
5
+ import { deriveId } from "../utils/index.ts";
6
+ import {
7
+ TableExpandableContext,
8
+ useTableExpandableContext,
9
+ } from "./TableExpandableContext";
3
10
  import {
4
11
  TableBodyProps,
5
12
  TableCaptionProps,
6
13
  TableCellProps,
14
+ TableDetailRowProps,
15
+ TableExpandableProps,
16
+ TableExpandTriggerProps,
7
17
  TableFooterProps,
8
18
  TableHeaderProps,
9
19
  TableHeadProps,
@@ -16,13 +26,28 @@ import {
16
26
  * The root of a Table widget — renders a plain `<table>` element and passes
17
27
  * all `TableHTMLAttributes` through to the DOM.
18
28
  *
29
+ * Structural: every part below renders exactly what its native HTML element
30
+ * provides, with no context and no `data-*` attributes of its own (verified by
31
+ * `Table.contract.test.tsx` against the registry contract).
32
+ *
33
+ * The one exception is the expandable-row trio —
34
+ * {@link TableExpandable | `Table.Expandable`},
35
+ * {@link TableExpandTrigger | `Table.ExpandTrigger`} and
36
+ * {@link TableDetailRow | `Table.DetailRow`} — which share a context so the
37
+ * trigger's `aria-controls` can reach the detail row. They are opt-in: a table
38
+ * that does not use them is still entirely static.
39
+ *
19
40
  * The `<table>` element carries an implicit `role="table"` in the accessibility
20
41
  * tree. Assistive technology will announce it as a table and report the number
21
42
  * of rows and columns to the user.
22
43
  *
23
- * Always pair column headers (`Table.Header`) with the correct
24
- * {@link TableHeaderProps.scope | `scope`} attribute so screen readers can
25
- * associate data cells with their headers.
44
+ * Always pair column headers ({@link TableHeader | `Table.Header`}) with the
45
+ * correct {@link TableHeaderProps.scope | `scope`} attribute so screen
46
+ * readers can associate data cells with their headers. Wrap the whole table
47
+ * in {@link TableScrollArea | `Table.ScrollArea`} when it has many columns
48
+ * and needs to scroll horizontally on narrow viewports.
49
+ *
50
+ * @extends HTMLTableElement
26
51
  *
27
52
  * @example
28
53
  * ```tsx
@@ -47,6 +72,10 @@ export function TableRoot({ children, ...rest }: TableRootProps): ReactElement {
47
72
  }
48
73
 
49
74
  /** @internal */
75
+ // `TableRoot` and the exported `Table` compound are the same object
76
+ // (`Object.assign`), and the compound re-assigns this displayName at the bottom
77
+ // of the file — so this assignment is unobservable and any value survives.
78
+ // Stryker disable next-line StringLiteral: equivalent — overwritten by the compound.
50
79
  TableRoot.displayName = "Table";
51
80
 
52
81
  /**
@@ -54,7 +83,12 @@ TableRoot.displayName = "Table";
54
83
  *
55
84
  * Browsers and assistive technology treat rows inside `<thead>` as column
56
85
  * headers. When a table is printed across multiple pages, the browser may
57
- * repeat the `<thead>` content at the top of each page.
86
+ * repeat the `<thead>` content at the top of each page. Renders as a direct
87
+ * child of {@link TableRoot | `Table.Root`}'s `<table>`, typically
88
+ * containing a single {@link TableRow | `Table.Row`} of
89
+ * {@link TableHeader | `Table.Header`}s.
90
+ *
91
+ * @extends HTMLTableSectionElement
58
92
  *
59
93
  * @example
60
94
  * ```tsx
@@ -79,7 +113,9 @@ TableHead.displayName = "TableHead";
79
113
  * Using `<tbody>` explicitly improves accessibility and allows browsers to
80
114
  * scroll the body independently of a fixed header. Multiple `<tbody>` elements
81
115
  * are valid and useful for visually separating logical row groups within a
82
- * single table.
116
+ * single table — render more than one `Table.Body` as siblings for that case.
117
+ *
118
+ * @extends HTMLTableSectionElement
83
119
  *
84
120
  * @example
85
121
  * ```tsx
@@ -103,7 +139,11 @@ TableBody.displayName = "TableBody";
103
139
  *
104
140
  * Typically used for summary rows — totals, averages, counts. When a table is
105
141
  * printed across multiple pages some browsers repeat the `<tfoot>` content at
106
- * the bottom of each page.
142
+ * the bottom of each page. Renders as a sibling of
143
+ * {@link TableHead | `Table.Head`} and {@link TableBody | `Table.Body`}
144
+ * inside {@link TableRoot | `Table.Root`}.
145
+ *
146
+ * @extends HTMLTableSectionElement
107
147
  *
108
148
  * @example
109
149
  * ```tsx
@@ -128,9 +168,13 @@ TableFooter.displayName = "TableFooter";
128
168
  /**
129
169
  * An individual table row — renders a `<tr>` element.
130
170
  *
131
- * May contain `Table.Header` (`<th>`) or `Table.Cell` (`<td>`) children, or a
132
- * mix of both when the row contains both header and data cells (e.g. the first
133
- * column of each row is a row header).
171
+ * May contain {@link TableHeader | `Table.Header`} (`<th>`) or
172
+ * {@link TableCell | `Table.Cell`} (`<td>`) children, or a mix of both when
173
+ * the row contains both header and data cells (e.g. the first column of each
174
+ * row is a row header). Lives inside {@link TableHead | `Table.Head`},
175
+ * {@link TableBody | `Table.Body`}, or {@link TableFooter | `Table.Footer`}.
176
+ *
177
+ * @extends HTMLTableRowElement
134
178
  *
135
179
  * @example
136
180
  * ```tsx
@@ -166,6 +210,8 @@ TableRow.displayName = "TableRow";
166
210
  * All `ThHTMLAttributes` (including `colSpan`, `rowSpan`, `abbr`) pass
167
211
  * through to the DOM.
168
212
  *
213
+ * @extends HTMLTableCellElement
214
+ *
169
215
  * @example Column header
170
216
  * ```tsx
171
217
  * <Table.Header scope="col">Name</Table.Header>
@@ -190,7 +236,11 @@ TableHeader.displayName = "TableHeader";
190
236
  * A data cell — renders a `<td>` element.
191
237
  *
192
238
  * All `TdHTMLAttributes` pass through to the DOM, including `colSpan` and
193
- * `rowSpan` for spanning multiple columns or rows.
239
+ * `rowSpan` for spanning multiple columns or rows. Use
240
+ * {@link TableHeader | `Table.Header`} instead when a cell's content is a
241
+ * label for a row or column rather than data.
242
+ *
243
+ * @extends HTMLTableCellElement
194
244
  *
195
245
  * @example Basic cell
196
246
  * ```tsx
@@ -224,15 +274,17 @@ TableCell.displayName = "TableCell";
224
274
  *
225
275
  * ```tsx
226
276
  * <Table.ScrollArea style={{ borderRadius: "8px" }}>
227
- * …
277
+ * ...
228
278
  * </Table.ScrollArea>
229
279
  * ```
230
280
  *
281
+ * @extends HTMLDivElement
282
+ *
231
283
  * @example
232
284
  * ```tsx
233
285
  * <Table.ScrollArea>
234
286
  * <Table.Root>
235
- * …
287
+ * ...
236
288
  * </Table.Root>
237
289
  * </Table.ScrollArea>
238
290
  * ```
@@ -277,11 +329,13 @@ TableScrollArea.displayName = "TableScrollArea";
277
329
  * | `"bottom"` (default) | Below the table |
278
330
  * | `"top"` | Above the table |
279
331
  *
332
+ * @extends HTMLTableCaptionElement
333
+ *
280
334
  * @example Default (caption below table)
281
335
  * ```tsx
282
336
  * <Table.Root>
283
337
  * <Table.Caption>Q1 sales figures by region</Table.Caption>
284
- * …
338
+ * ...
285
339
  * </Table.Root>
286
340
  * ```
287
341
  *
@@ -289,7 +343,7 @@ TableScrollArea.displayName = "TableScrollArea";
289
343
  * ```tsx
290
344
  * <Table.Root>
291
345
  * <Table.Caption captionSide="top">Q1 sales figures by region</Table.Caption>
292
- * …
346
+ * ...
293
347
  * </Table.Root>
294
348
  * ```
295
349
  */
@@ -308,11 +362,184 @@ export function TableCaption({
308
362
  /** @internal */
309
363
  TableCaption.displayName = "TableCaption";
310
364
 
365
+ /**
366
+ * Pairs one expandable {@link TableRow | `Table.Row`} with its
367
+ * {@link TableDetailRow | `Table.DetailRow`}, sharing the id that wires the
368
+ * trigger's `aria-controls` to the detail row.
369
+ *
370
+ * Renders **no DOM of its own** — it is a context provider, so the two `<tr>`
371
+ * elements stay direct children of the `<tbody>` and the table's structure is
372
+ * untouched.
373
+ */
374
+ export function TableExpandable({
375
+ children,
376
+ expanded: expandedProp,
377
+ defaultExpanded = false,
378
+ onExpandedChange,
379
+ }: TableExpandableProps): ReactElement {
380
+ const reactId = useId();
381
+ const [expanded, setExpanded] = useControllableState(
382
+ expandedProp,
383
+ defaultExpanded,
384
+ onExpandedChange,
385
+ );
386
+
387
+ const value = useMemo(
388
+ () => ({
389
+ expanded,
390
+ // The id is opaque: the only contracts are that the trigger's
391
+ // aria-controls matches the detail row's id, and that two Expandables on
392
+ // one page differ — both of which `useId` alone guarantees. The suffixes
393
+ // are for legibility when inspecting the DOM, so emptying them yields an
394
+ // equally valid id.
395
+ // Stryker disable next-line StringLiteral: equivalent — see above.
396
+ detailId: deriveId(reactId, "detail", "row"),
397
+ toggle: () => setExpanded(!expanded),
398
+ }),
399
+ [expanded, reactId, setExpanded],
400
+ );
401
+
402
+ return (
403
+ <TableExpandableContext.Provider value={value}>
404
+ {children}
405
+ </TableExpandableContext.Provider>
406
+ );
407
+ }
408
+
409
+ /** @internal */
410
+ TableExpandable.displayName = "TableExpandable";
411
+
412
+ /**
413
+ * The disclosure control for an expandable row — renders a `<button>` inside
414
+ * one of the row's cells.
415
+ *
416
+ * The **button** carries the state, not the row. `role="row"` does list
417
+ * `aria-expanded` among its supported properties, but a button's expanded state
418
+ * is announced universally while a row's is only reliably announced inside a
419
+ * `role="treegrid"` — so the button owns `aria-expanded` and `aria-controls`.
420
+ *
421
+ * Give it a name that identifies its row ("Show details for Ada Lovelace", or
422
+ * visually-hidden text beside the glyph). A column of buttons all called
423
+ * "Expand" is the same defect as a column of checkboxes all called "Select":
424
+ * obvious on screen, useless in a screen reader's element list.
425
+ *
426
+ * @extends HTMLButtonElement
427
+ */
428
+ export function TableExpandTrigger({
429
+ children,
430
+ asChild = false,
431
+ onClick,
432
+ ...rest
433
+ }: TableExpandTriggerProps): ReactElement {
434
+ const { expanded, detailId, toggle } = useTableExpandableContext();
435
+
436
+ const wiring = {
437
+ "aria-expanded": expanded,
438
+ "aria-controls": detailId,
439
+ "data-state": expanded ? "open" : "closed",
440
+ onClick: (event: MouseEvent<HTMLButtonElement>) => {
441
+ onClick?.(event);
442
+ toggle();
443
+ },
444
+ ...rest,
445
+ };
446
+
447
+ // `type` is set only on the native button: a Slot child may not be a
448
+ // <button> at all, and `type` on a non-button element is invalid HTML.
449
+ return asChild ? (
450
+ <Slot {...wiring}>{children}</Slot>
451
+ ) : (
452
+ <button type="button" {...wiring}>
453
+ {children}
454
+ </button>
455
+ );
456
+ }
457
+
458
+ /** @internal */
459
+ TableExpandTrigger.displayName = "TableExpandTrigger";
460
+
461
+ /**
462
+ * The revealed panel for an expandable row — a `<tr>` holding a `<td colSpan>`,
463
+ * and the target of its {@link TableExpandTrigger | `Table.ExpandTrigger`}'s
464
+ * `aria-controls`. Must be rendered inside a
465
+ * {@link TableExpandable | `Table.Expandable`}, as the sibling of the data row.
466
+ *
467
+ * This is a **disclosure**, not a treegrid: the panel holds detail *about* the
468
+ * row (a summary, a form, a chart), not child rows. Expanding into hierarchical
469
+ * child rows is a different pattern with a different role and keyboard model.
470
+ *
471
+ * Two things it leaves to you, both because only the caller can know them:
472
+ * {@link TableDetailRowProps.colSpan | `colSpan`}, since counting columns would
473
+ * mean the table registering its own, and
474
+ * {@link TableDetailRowProps.gutter | `gutter`}, which aligns the panel with the
475
+ * first data column by spanning the control columns in front of it.
476
+ *
477
+ * Collapsed, the row is `hidden` — so it cannot be reached, and cannot inflate
478
+ * the table's announced row count. Pass
479
+ * {@link TableDetailRowProps.forceMount | `forceMount`} when you are animating
480
+ * it: `display: none` cannot transition, so the row stays in the layout and
481
+ * takes `aria-hidden` instead, which keeps the row count honest either way.
482
+ *
483
+ * @extends HTMLTableRowElement
484
+ *
485
+ * @example
486
+ * ```tsx
487
+ * <Table.Expandable>
488
+ * <Table.Row>
489
+ * <Table.Cell>
490
+ * <Table.ExpandTrigger aria-label="Show details for harmoni-engine">
491
+ * ▸
492
+ * </Table.ExpandTrigger>
493
+ * </Table.Cell>
494
+ * <Table.Cell>harmoni-engine</Table.Cell>
495
+ * </Table.Row>
496
+ * <Table.DetailRow colSpan={2} gutter={1}>
497
+ * <DeploymentSummary />
498
+ * </Table.DetailRow>
499
+ * </Table.Expandable>
500
+ * ```
501
+ */
502
+ export function TableDetailRow({
503
+ children,
504
+ colSpan,
505
+ gutter = 0,
506
+ forceMount = false,
507
+ cellProps,
508
+ ...rest
509
+ }: TableDetailRowProps): ReactElement | null {
510
+ const { expanded, detailId } = useTableExpandableContext();
511
+ const collapsed = !expanded;
512
+
513
+ return (
514
+ <tr
515
+ id={detailId}
516
+ data-state={collapsed ? "closed" : "open"}
517
+ hidden={collapsed && !forceMount}
518
+ aria-hidden={collapsed && forceMount ? true : undefined}
519
+ {...rest}
520
+ >
521
+ {/* ONE spanning cell rather than `gutter` empty ones: a spanning cell
522
+ takes the full width of the columns it covers, so the alignment is
523
+ identical and the row gains a single blank cell instead of several. */}
524
+ {gutter > 0 ? <td colSpan={gutter} /> : null}
525
+ <td colSpan={colSpan - gutter} {...cellProps}>
526
+ {children}
527
+ </td>
528
+ </tr>
529
+ );
530
+ }
531
+
532
+ /** @internal */
533
+ TableDetailRow.displayName = "TableDetailRow";
534
+
311
535
  /**
312
536
  * The shape of the exported `Table` value — callable as `Table.Root` and
313
537
  * carrying every sub-component as a static property.
314
538
  */
315
539
  export type TableCompound = typeof TableRoot & {
540
+ Expandable: typeof TableExpandable;
541
+ ExpandTrigger: typeof TableExpandTrigger;
542
+ DetailRow: typeof TableDetailRow;
316
543
  Root: typeof TableRoot;
317
544
  Head: typeof TableHead;
318
545
  Body: typeof TableBody;
@@ -370,7 +597,7 @@ export type TableCompound = typeof TableRoot & {
370
597
  * @example Responsive — horizontal scroll on narrow viewports
371
598
  * ```tsx
372
599
  * <Table.ScrollArea>
373
- * <Table.Root>…</Table.Root>
600
+ * <Table.Root>...</Table.Root>
374
601
  * </Table.ScrollArea>
375
602
  * ```
376
603
  *
@@ -389,6 +616,9 @@ export type TableCompound = typeof TableRoot & {
389
616
  * @see {@link TableScrollArea} for the horizontal-scroll style caveat.
390
617
  */
391
618
  const TableCompound: TableCompound = Object.assign(TableRoot, {
619
+ Expandable: TableExpandable,
620
+ ExpandTrigger: TableExpandTrigger,
621
+ DetailRow: TableDetailRow,
392
622
  Root: TableRoot,
393
623
  Head: TableHead,
394
624
  Body: TableBody,
@@ -0,0 +1,7 @@
1
+ import { createStrictContext } from "../utils/index.ts";
2
+ import { TableExpandableContextValue } from "./types";
3
+
4
+ export const [TableExpandableContext, useTableExpandableContext] =
5
+ createStrictContext<TableExpandableContextValue>(
6
+ "Table.ExpandTrigger and Table.DetailRow must be rendered inside a <Table.Expandable>.",
7
+ );