@primitiv-ui/react 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/README.md +19 -2
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -0,0 +1,238 @@
1
+ import type { HTMLAttributes, ReactNode, Ref } from "react";
2
+
3
+ /**
4
+ * Mode-independent props shared by every `Listbox.Root` variant — orientation,
5
+ * direction, selection-follows-focus, `asChild`, children, and the ref.
6
+ * Combined with a mode-specific arm to form {@link ListboxRootProps}.
7
+ */
8
+ export type ListboxRootBaseProps = Omit<
9
+ HTMLAttributes<HTMLDivElement>,
10
+ "defaultValue" | "dir"
11
+ > & {
12
+ /** Layout axis for cursor navigation. `"vertical"` binds ArrowUp/ArrowDown;
13
+ * `"horizontal"` binds ArrowLeft/ArrowRight and additionally emits
14
+ * `aria-orientation="horizontal"` (vertical is the ARIA default, so it is
15
+ * left implicit). Surfaces as `data-orientation` for styling; it does not
16
+ * itself apply any layout.
17
+ * @default "vertical" */
18
+ orientation?: "horizontal" | "vertical";
19
+ /** Reading direction. In `"rtl"` the horizontal arrow pair is mirrored so
20
+ * the cursor follows visual order — meaningful only when
21
+ * {@link ListboxRootBaseProps.orientation} is `"horizontal"`. Inherited from
22
+ * the nearest {@link DirectionProvider} when omitted, falling back to
23
+ * `"ltr"`. */
24
+ dir?: "ltr" | "rtl";
25
+ /** When `true`, moving the cursor with the arrow / Home / End keys or via
26
+ * typeahead also selects the option it lands on — the behaviour of APG's
27
+ * single-select listbox example. Left `false`, arrowing only moves the
28
+ * highlight and the user commits with Enter or Space, which is what a
29
+ * command palette or search-suggestion list needs.
30
+ * @default false */
31
+ selectionFollowsFocus?: boolean;
32
+ /** Render a single consumer-supplied element in place of the native `<div>`,
33
+ * with the listbox's `role`, `tabIndex`, ARIA state, `data-orientation`, ref
34
+ * and keyboard handlers merged onto it via the {@link Slot} pattern.
35
+ * @default false */
36
+ asChild?: boolean;
37
+ /** The {@link ListboxOptionProps | `Listbox.Option`} and
38
+ * {@link ListboxGroupProps | `Listbox.Group`} elements that make up the
39
+ * list. */
40
+ children?: ReactNode;
41
+ /** Forwarded to the underlying `HTMLDivElement`. */
42
+ ref?: Ref<HTMLDivElement>;
43
+ };
44
+
45
+ /**
46
+ * Single-selection mode, uncontrolled — at most one option selected, with the
47
+ * initial choice supplied via `defaultValue`.
48
+ */
49
+ export type ListboxSingleUncontrolledProps = {
50
+ /** Selects single-selection semantics: at most one option selected at a
51
+ * time, and re-selecting the current option is a no-op rather than a
52
+ * deselect. */
53
+ type: "single";
54
+ /** Value of the option selected on first render. Omit to start with nothing
55
+ * selected. */
56
+ defaultValue?: string;
57
+ /** Forbidden in uncontrolled mode — use
58
+ * {@link ListboxSingleUncontrolledProps.defaultValue}. */
59
+ value?: never;
60
+ /** Called with the newly selected option's value whenever the user commits a
61
+ * choice. */
62
+ onValueChange?: (value: string) => void;
63
+ };
64
+
65
+ /**
66
+ * Single-selection mode, controlled — the selected value is owned by the
67
+ * caller via `value` / `onValueChange`.
68
+ */
69
+ export type ListboxSingleControlledProps = {
70
+ /** Selects single-selection semantics: at most one option selected at a
71
+ * time, and re-selecting the current option is a no-op rather than a
72
+ * deselect. */
73
+ type: "single";
74
+ /** The currently selected option's value. Must be kept in sync by the caller
75
+ * via {@link ListboxSingleControlledProps.onValueChange}. */
76
+ value: string;
77
+ /** Called with the value the user is asking to select. */
78
+ onValueChange?: (value: string) => void;
79
+ /** Forbidden in controlled mode — use
80
+ * {@link ListboxSingleControlledProps.value}. */
81
+ defaultValue?: never;
82
+ };
83
+
84
+ /**
85
+ * Multiple-selection mode, uncontrolled — options toggle independently, with
86
+ * the initial set supplied via `defaultValue`.
87
+ */
88
+ export type ListboxMultipleUncontrolledProps = {
89
+ /** Selects multiple-selection semantics: any number of options can be
90
+ * selected and each toggles independently. Sets `aria-multiselectable` on
91
+ * the root. */
92
+ type: "multiple";
93
+ /** Values of the options selected on first render. Omit to start with
94
+ * nothing selected. */
95
+ defaultValue?: string[];
96
+ /** Forbidden in uncontrolled mode — use
97
+ * {@link ListboxMultipleUncontrolledProps.defaultValue}. */
98
+ value?: never;
99
+ /** Called with the complete next set of selected values whenever the user
100
+ * toggles any option. */
101
+ onValueChange?: (value: string[]) => void;
102
+ };
103
+
104
+ /**
105
+ * Multiple-selection mode, controlled — the set of selected values is owned by
106
+ * the caller via `value` / `onValueChange`.
107
+ */
108
+ export type ListboxMultipleControlledProps = {
109
+ /** Selects multiple-selection semantics: any number of options can be
110
+ * selected and each toggles independently. Sets `aria-multiselectable` on
111
+ * the root. */
112
+ type: "multiple";
113
+ /** The full set of currently selected values. Must be kept in sync by the
114
+ * caller via {@link ListboxMultipleControlledProps.onValueChange}. */
115
+ value: string[];
116
+ /** Called with the complete next set of selected values whenever the user
117
+ * toggles any option. */
118
+ onValueChange?: (value: string[]) => void;
119
+ /** Forbidden in controlled mode — use
120
+ * {@link ListboxMultipleControlledProps.value}. */
121
+ defaultValue?: never;
122
+ };
123
+
124
+ /**
125
+ * Props for {@link ListboxRoot | `Listbox.Root`}. Combines the shared
126
+ * {@link ListboxRootBaseProps} with one mode-specific arm — single or
127
+ * multiple, controlled or uncontrolled — so the `value` / `defaultValue` /
128
+ * `onValueChange` shape is enforced per `type`.
129
+ */
130
+ export type ListboxRootProps = ListboxRootBaseProps &
131
+ (
132
+ | ListboxSingleUncontrolledProps
133
+ | ListboxSingleControlledProps
134
+ | ListboxMultipleUncontrolledProps
135
+ | ListboxMultipleControlledProps
136
+ );
137
+
138
+ /**
139
+ * Props for {@link ListboxOption | `Listbox.Option`} — one selectable choice.
140
+ * `value` identifies it within the listbox; `children` supplies the label that
141
+ * typeahead matches against.
142
+ */
143
+ export type ListboxOptionProps = HTMLAttributes<HTMLDivElement> & {
144
+ /** Identifies this option within the listbox. It is this string — not the
145
+ * visible label — that is compared against the root's selected `value` /
146
+ * `defaultValue`; use `children` for the label. */
147
+ value: string;
148
+ /** Removes the option from cursor navigation, focus seeding and typeahead,
149
+ * and makes it unselectable by click or key. The option stays in the DOM and
150
+ * in the accessibility tree, marked `aria-disabled`. Also sets
151
+ * `data-disabled=""` for CSS targeting.
152
+ * @default false */
153
+ disabled?: boolean;
154
+ /** Render a single consumer-supplied element in place of the native `<div>`,
155
+ * with the option's `role`, `id`, `aria-selected`, `data-highlighted` and
156
+ * click handler merged onto it via the {@link Slot} pattern. The child must
157
+ * accept a `ref`.
158
+ * @default false */
159
+ asChild?: boolean;
160
+ /** Forwarded to the underlying `HTMLDivElement`. */
161
+ ref?: Ref<HTMLDivElement>;
162
+ };
163
+
164
+ /**
165
+ * Props for {@link ListboxGroup | `Listbox.Group`} — a named cluster of
166
+ * options. Grouping is presentational; it does not partition navigation.
167
+ */
168
+ export type ListboxGroupProps = HTMLAttributes<HTMLDivElement> & {
169
+ /** Accessible name for the group, applied as `aria-label`. APG requires
170
+ * every option group to carry a name — supply it either with this prop (an
171
+ * invisible name) or by rendering a
172
+ * {@link ListboxGroupLabelProps | `Listbox.GroupLabel`} heading inside the
173
+ * group, which names it via `aria-labelledby` instead. A rendered
174
+ * `GroupLabel` takes precedence. */
175
+ label?: string;
176
+ /** Render a single consumer-supplied element in place of the native `<div>`,
177
+ * with the group's `role` and `aria-label` merged onto it via the
178
+ * {@link Slot} pattern.
179
+ * @default false */
180
+ asChild?: boolean;
181
+ /** Forwarded to the underlying `HTMLDivElement`. */
182
+ ref?: Ref<HTMLDivElement>;
183
+ };
184
+
185
+ /**
186
+ * Props for {@link ListboxGroupLabel | `Listbox.GroupLabel`} — the visible
187
+ * heading that names its enclosing {@link ListboxGroup | `Listbox.Group`}.
188
+ */
189
+ export type ListboxGroupLabelProps = HTMLAttributes<HTMLDivElement> & {
190
+ /** Render a single consumer-supplied element in place of the native
191
+ * `<div>`, with the heading's `id` and `role="presentation"` merged onto it
192
+ * via the {@link Slot} pattern.
193
+ * @default false */
194
+ asChild?: boolean;
195
+ /** Forwarded to the underlying `HTMLDivElement`. */
196
+ ref?: Ref<HTMLDivElement>;
197
+ };
198
+
199
+ /**
200
+ * Context shared from {@link ListboxGroup | `Listbox.Group`} to its label —
201
+ * the id the heading must adopt so the group can point `aria-labelledby` at
202
+ * it, and the registrar telling the group a heading is present.
203
+ */
204
+ export type ListboxGroupContextValue = {
205
+ /** The DOM id the {@link ListboxGroupLabel} must render with. */
206
+ labelId: string;
207
+ /** Called by the label on mount/unmount so the group knows whether to emit
208
+ * `aria-labelledby`. */
209
+ registerLabel: (present: boolean) => void;
210
+ };
211
+
212
+ /**
213
+ * Context shared from {@link ListboxRoot | `Listbox.Root`} to its options —
214
+ * the selected set, the select action, the virtual-focus cursor, option
215
+ * registration, and the id derivation that wires `aria-activedescendant` to
216
+ * each option's `id`.
217
+ */
218
+ export type ListboxContextValue = {
219
+ /** Every currently selected option value. Single mode holds at most one. */
220
+ selectedValues: string[];
221
+ /** Selects (single) or toggles (multiple) the given option. */
222
+ select: (optionValue: string) => void;
223
+ /** The option the cursor is on, or `undefined` before the root is focused. */
224
+ activeValue: string | undefined;
225
+ /** Moves the cursor onto the given option and scrolls it into view. Used by
226
+ * `Listbox.Option` so a click leaves the cursor where the user clicked. */
227
+ moveCursor: (optionValue: string) => void;
228
+ /** Registers a mounted option's element and disabled state; pass `null` as
229
+ * the element to unregister. */
230
+ registerOption: (
231
+ optionValue: string,
232
+ element: HTMLElement | null,
233
+ disabled?: boolean,
234
+ ) => void;
235
+ /** Derives the stable DOM `id` for an option, shared by the option itself
236
+ * and the root's `aria-activedescendant`. */
237
+ getOptionId: (optionValue: string) => string;
238
+ };
@@ -18,7 +18,7 @@ import {
18
18
  useMillerColumnsRoot,
19
19
  } from "./hooks/index.ts";
20
20
 
21
- import { partitionItemChildren } from "./utils";
21
+ import { partitionAriaProps, partitionItemChildren } from "./utils";
22
22
 
23
23
  import type {
24
24
  MillerColumnsRootProps,
@@ -33,11 +33,60 @@ import type {
33
33
  * The root of a Miller Columns widget — a horizontal strip of vertical
34
34
  * lists where selecting a node reveals its children in the next column.
35
35
  *
36
- * Renders the strip container (`role="tree"`) into which every
37
- * `MillerColumns.Column` projects itself via a portal. Authoring the tree
38
- * is recursive: an `Item` declares its child column as a *nested*
39
- * `<MillerColumns.Column>`, and the strip flattens those nested columns
40
- * into a single left-to-right row.
36
+ * Renders **two** elements: an outer strip container
37
+ * (`data-miller-columns-strip`, `data-orientation="horizontal"`) that the
38
+ * consumer lays out and scrolls, and an inner `role="tree"` widget
39
+ * (`data-miller-columns-tree`) holding only the columns. The split exists
40
+ * because a tree may own nothing but `treeitem` and `group` — keeping the
41
+ * {@link MillerColumnsPreviewPanel | `PreviewPanel`} outside it means the
42
+ * panel is not an illegal child of the tree.
43
+ *
44
+ * Consequently **`aria-*` props land on the tree widget** (an `aria-label`
45
+ * must name the tree, not its scroll container) while every other prop —
46
+ * `className`, `id`, `style`, `data-*` — lands on the strip container.
47
+ *
48
+ * Every {@link MillerColumnsColumn | `MillerColumns.Column`}
49
+ * projects itself into the tree via a portal. Authoring the tree is **recursive** and
50
+ * data-less: there is no `items` prop — an {@link MillerColumnsItem | `Item`}
51
+ * declares its child column as a *nested* `<MillerColumns.Column>`, and the
52
+ * strip flattens those nested columns into a single left-to-right row by
53
+ * rendering one `display: contents` slot per visible depth and portalling
54
+ * each column into its slot. This fixes left-to-right DOM order by slot
55
+ * order rather than by portal commit order.
56
+ *
57
+ * **Controlled vs uncontrolled.** State is the *selection path* — an array
58
+ * of item ids, root column first (`["docs", "guides"]`). The two modes are
59
+ * statically discriminated at the type level:
60
+ *
61
+ * - **Uncontrolled** — pass {@link MillerColumnsRootUncontrolledProps.defaultValue | `defaultValue`}
62
+ * (or omit it to start empty). The component owns the path.
63
+ * - **Controlled** — pass {@link MillerColumnsRootControlledProps.value | `value`}
64
+ * and {@link MillerColumnsRootControlledProps.onValueChange | `onValueChange`}
65
+ * together; every selection defers back through the callback.
66
+ *
67
+ * **Roving tabindex.** The whole tree has exactly **one** Tab stop
68
+ * (`tabIndex={0}`), not one per column. It follows the last-focused item;
69
+ * before any focus it defaults to the deepest selected item, falling back
70
+ * to the first enabled root item. Within a column, Up/Down/Home/End move
71
+ * focus; ArrowRight opens (and steps into) a branch item's child column,
72
+ * ArrowLeft returns to the parent item. Under
73
+ * {@link MillerColumnsRootBaseProps.dir | `dir="rtl"`} that horizontal pair
74
+ * is mirrored, so the step-in key always points at the child column.
75
+ *
76
+ * **Typeahead.** Printable characters move focus to the first item whose
77
+ * label starts with the accumulated query, wrapping, and cycling through
78
+ * matches when the same character repeats. The query is owned by `Root` —
79
+ * so it survives the focus move a match causes — but resolves against the
80
+ * **focused item's own column**, each column being an independent list.
81
+ *
82
+ * **Context.** Root establishes {@link MillerColumnsContext} (selection
83
+ * path, portal slots, item registry, column widths, and focus commands);
84
+ * each Column provides {@link MillerColumnsColumnContext} (its depth) and
85
+ * each Item {@link MillerColumnsItemContext} (`selected` / `hasChildren`).
86
+ * Read the current selection from consumer content with
87
+ * {@link useMillerColumnsSelection}.
88
+ *
89
+ * @extends HTMLDivElement
41
90
  *
42
91
  * @example
43
92
  * ```tsx
@@ -53,28 +102,32 @@ export function MillerColumnsRoot({
53
102
  defaultValue,
54
103
  value,
55
104
  onValueChange,
105
+ dir,
56
106
  ...rest
57
107
  }: MillerColumnsRootProps): ReactElement {
58
- const { contextValue, columnCount, registerSlotRef, stripRef } =
59
- useMillerColumnsRoot(value, defaultValue, onValueChange);
108
+ const { contextValue, columnCount, registerSlotRef, stripRef, resolvedDir } =
109
+ useMillerColumnsRoot(value, defaultValue, onValueChange, dir);
110
+ const { aria, rest: containerProps } = partitionAriaProps(rest);
60
111
 
61
112
  return (
62
113
  <MillerColumnsContext.Provider value={contextValue}>
63
114
  <div
64
115
  ref={stripRef}
65
- role="tree"
116
+ dir={resolvedDir}
66
117
  data-miller-columns-strip=""
67
118
  data-orientation="horizontal"
68
- {...rest}
119
+ {...containerProps}
69
120
  >
70
- {Array.from({ length: columnCount }, (_, depth) => (
71
- <div
72
- key={depth}
73
- data-miller-columns-slot=""
74
- style={{ display: "contents" }}
75
- ref={registerSlotRef(depth)}
76
- />
77
- ))}
121
+ <div role="tree" data-miller-columns-tree="" {...aria}>
122
+ {Array.from({ length: columnCount }, (_, depth) => (
123
+ <div
124
+ key={depth}
125
+ data-miller-columns-slot=""
126
+ style={{ display: "contents" }}
127
+ ref={registerSlotRef(depth)}
128
+ />
129
+ ))}
130
+ </div>
78
131
  {children}
79
132
  </div>
80
133
  </MillerColumnsContext.Provider>
@@ -86,9 +139,24 @@ MillerColumnsRoot.displayName = "MillerColumnsRoot";
86
139
 
87
140
  /**
88
141
  * A single vertical list of items within the strip. Renders a
89
- * `<div role="group">` that is portal-projected into the `Root` strip, so
90
- * a `Column` nested inside an `Item` still appears side-by-side with its
91
- * ancestors rather than in document flow.
142
+ * `<div role="group">` (carrying `data-depth`) that is **portal-projected**
143
+ * into the {@link MillerColumnsRoot | `Root`} strip's depth slot, so a
144
+ * `Column` nested inside an `Item` still appears side-by-side with its
145
+ * ancestors rather than in document flow. Until its slot has mounted the
146
+ * component renders `null`.
147
+ *
148
+ * A column holding no registered items carries **`data-empty`** — selecting
149
+ * a childless branch legitimately opens an empty column, and the styling
150
+ * layer needs to be able to say so rather than show a void.
151
+ *
152
+ * It reads its depth from {@link MillerColumnsContext} and re-provides it to
153
+ * descendant {@link MillerColumnsItem | Items} via
154
+ * {@link MillerColumnsColumnContext}. A width set by a
155
+ * {@link MillerColumnsResizeHandle | `ResizeHandle`} in this column is
156
+ * applied here as an inline `width` style (consumer `style` still wins for
157
+ * every other property).
158
+ *
159
+ * @extends HTMLDivElement
92
160
  *
93
161
  * @example
94
162
  * ```tsx
@@ -102,7 +170,8 @@ export function MillerColumnsColumn({
102
170
  style,
103
171
  ...rest
104
172
  }: MillerColumnsColumnProps): ReactPortal | null {
105
- const { slot, depth, width, columnContextValue } = useMillerColumnsColumn();
173
+ const { slot, depth, width, empty, columnContextValue } =
174
+ useMillerColumnsColumn();
106
175
 
107
176
  if (!slot) {
108
177
  return null;
@@ -114,6 +183,7 @@ export function MillerColumnsColumn({
114
183
  role="group"
115
184
  data-miller-columns-column=""
116
185
  data-depth={depth}
186
+ {...(empty ? { "data-empty": "" } : {})}
117
187
  style={{ ...style, ...(width !== undefined ? { width } : {}) }}
118
188
  {...rest}
119
189
  >
@@ -128,27 +198,51 @@ export function MillerColumnsColumn({
128
198
  MillerColumnsColumn.displayName = "MillerColumnsColumn";
129
199
 
130
200
  /**
131
- * A single selectable node within a `Column`. Renders a
132
- * `<div role="treeitem">` for its cell content.
201
+ * A single selectable node within a {@link MillerColumnsColumn | `Column`}.
202
+ * Renders a `<div role="treeitem">` for its cell content, with
203
+ * `aria-selected`, `aria-level` (1-based depth), and `data-state` /
204
+ * `data-depth` styling hooks.
205
+ *
206
+ * **Terminal vs ancestor.** Every item on the active path is
207
+ * `data-state="selected"`, including the ancestors the path merely passes
208
+ * through. The deepest one — the item the user actually landed on — also
209
+ * carries **`data-terminal`**. Style the two differently or the strip loses
210
+ * its depth cue: with a uniform selected state there is no way to tell
211
+ * which of four highlighted rows was clicked.
133
212
  *
134
213
  * The {@link MillerColumnsItemProps.value | `value`} prop is the stable
135
214
  * identifier used to match this item against the active selection path.
136
215
  *
137
- * An `Item` becomes a branch by nesting a `<MillerColumns.Column>` among
138
- * its children. That child column is mounted (and projected into the
139
- * strip) only while the item is selected; an item with no nested column
140
- * is a leaf.
216
+ * **Branch vs leaf.** An `Item` becomes a *branch* by nesting a
217
+ * `<MillerColumns.Column>` among its children — `partitionItemChildren`
218
+ * splits the children into the cell content and that single optional
219
+ * nested column (matched however deeply it is wrapped in fragments; a
220
+ * second nested column throws). A branch carries `aria-expanded` and
221
+ * mounts its child column (projecting it into the strip) only while the
222
+ * item is selected; an item with no nested column is a *leaf*.
223
+ *
224
+ * **Keyboard & selection.** Clicking or activating (Enter/Space) selects
225
+ * the item, truncating the path to this depth. ArrowRight on a branch opens
226
+ * and steps into the child column; ArrowLeft returns focus to the parent
227
+ * item. The item participates in the tree-wide single roving Tab stop.
228
+ *
229
+ * **Disabled.** Pass `disabled` to render `aria-disabled="true"` /
230
+ * `data-disabled`; the item is skipped by roving navigation and cannot be
231
+ * selected, focused, or activated.
141
232
  *
142
233
  * **`asChild` prop.** Pass `asChild` to render the cell as a
143
234
  * consumer-supplied element (e.g. an `<a>`) instead of the default
144
235
  * `<div>`. All treeitem ARIA attributes, event handlers, and the internal
145
- * ref are merged onto that child. A nested `<MillerColumns.Column>` is
146
- * still declared as a sibling of the cell element.
236
+ * ref are merged onto that child via the {@link Slot} pattern. A nested
237
+ * `<MillerColumns.Column>` is still declared as a sibling of the cell
238
+ * element.
147
239
  *
148
240
  * **Ref forwarding.** A `ref` prop (React 19 ref-as-prop style) is
149
241
  * forwarded to the rendered element and composed with the library's
150
242
  * internal ref.
151
243
  *
244
+ * @extends HTMLDivElement
245
+ *
152
246
  * @example Leaf
153
247
  * ```tsx
154
248
  * <MillerColumns.Item value="guides">Guides</MillerColumns.Item>
@@ -204,12 +298,14 @@ MillerColumnsItem.displayName = "MillerColumnsItem";
204
298
  * - `data-has-children` — always present (the indicator only renders
205
299
  * for branch items).
206
300
  *
301
+ * @extends HTMLSpanElement
302
+ *
207
303
  * @example
208
304
  * ```tsx
209
305
  * <MillerColumns.Item value="docs">
210
306
  * Docs
211
307
  * <MillerColumns.ItemIndicator>▸</MillerColumns.ItemIndicator>
212
- * <MillerColumns.Column>…</MillerColumns.Column>
308
+ * <MillerColumns.Column>...</MillerColumns.Column>
213
309
  * </MillerColumns.Item>
214
310
  * ```
215
311
  */
@@ -239,18 +335,43 @@ export function MillerColumnsItemIndicator({
239
335
  MillerColumnsItemIndicator.displayName = "MillerColumnsItemIndicator";
240
336
 
241
337
  /**
242
- * A drag-to-resize affordance for the column it is rendered in. Renders a
243
- * `<div role="separator" aria-orientation="vertical">` that, while
244
- * pointer-dragged, drives that column's width as state on the `Root`.
338
+ * A resize affordance for the column it is rendered in — the WAI-ARIA
339
+ * window-splitter pattern. Renders a focusable
340
+ * `<div role="separator" aria-orientation="vertical" tabIndex={0}>` that
341
+ * drives its column's width as state on the `Root`.
342
+ *
343
+ * **Pointer and keyboard.** Drag it, or focus it and use the arrow keys:
344
+ * the inline-end arrow widens by
345
+ * {@link MillerColumnsResizeHandleProps.step | `step`} and the inline-start
346
+ * arrow narrows (mirrored under `dir="rtl"`), with Home and End jumping to
347
+ * {@link MillerColumnsResizeHandleProps.minWidth | `minWidth`} /
348
+ * {@link MillerColumnsResizeHandleProps.maxWidth | `maxWidth`}. Both paths
349
+ * clamp to the same bounds. End is a no-op while `maxWidth` is unbounded.
245
350
  *
246
- * Must be rendered among a `MillerColumns.Column`'s children — it reads
247
- * the column's depth from context. Position it with CSS (typically
351
+ * **Value semantics.** `aria-valuenow` tracks the column's real rendered
352
+ * width — measured with a `ResizeObserver`, so it is truthful before the
353
+ * first resize and follows CSS- or container-driven changes too.
354
+ * `aria-valuemin` always renders; `aria-valuemax` renders only when
355
+ * `maxWidth` is finite.
356
+ *
357
+ * **Give it an accessible name.** A splitter announces as an unnamed
358
+ * separator otherwise — pass `aria-label` (e.g. `"Resize column"`).
359
+ *
360
+ * Must be rendered among a {@link MillerColumnsColumn | `MillerColumns.Column`}'s
361
+ * children — it reads the column's depth from
362
+ * {@link MillerColumnsColumnContext}. Position it with CSS (typically
248
363
  * absolutely, against the column's trailing edge).
249
364
  *
365
+ * @extends HTMLDivElement
366
+ *
250
367
  * @example
251
368
  * ```tsx
252
369
  * <MillerColumns.Column>
253
- * <MillerColumns.ResizeHandle />
370
+ * <MillerColumns.ResizeHandle
371
+ * aria-label="Resize column"
372
+ * minWidth={120}
373
+ * maxWidth={400}
374
+ * />
254
375
  * <MillerColumns.Item value="a">A</MillerColumns.Item>
255
376
  * </MillerColumns.Column>
256
377
  * ```
@@ -271,13 +392,22 @@ MillerColumnsResizeHandle.displayName = "MillerColumnsResizeHandle";
271
392
  * macOS-Finder-style preview pane.
272
393
  *
273
394
  * Renders a plain `<div data-miller-columns-preview>` as the last child
274
- * of the strip, sitting to the right of the columns. The component is
275
- * deliberately content-agnostic: it does not know how to preview an
276
- * item, so the consumer supplies whatever the panel should show.
395
+ * of the strip — *outside* the inner `role="tree"` widget, since a tree may
396
+ * own only `treeitem` and `group`. The component is deliberately
397
+ * content-agnostic: it does not know how to preview an item, so the
398
+ * consumer supplies whatever the panel should show.
399
+ *
400
+ * While nothing is selected the panel carries **`data-empty`**, so the
401
+ * styling layer can render a resting state without the consumer having to
402
+ * re-derive the selection.
277
403
  *
278
404
  * Pair it with {@link useMillerColumnsSelection} to render content for
279
405
  * the current selection. Author it as the **last child** of `Root`, a
280
- * sibling of the root `Column`.
406
+ * sibling of the root `Column`. It subscribes to
407
+ * {@link MillerColumnsContext} (so it must live inside a `Root`) but does
408
+ * not portal — it stays in document flow after the projected columns.
409
+ *
410
+ * @extends HTMLDivElement
281
411
  *
282
412
  * @example
283
413
  * ```tsx
@@ -296,12 +426,23 @@ MillerColumnsResizeHandle.displayName = "MillerColumnsResizeHandle";
296
426
  */
297
427
  export function MillerColumnsPreviewPanel({
298
428
  children,
429
+ forceMount = false,
299
430
  ...rest
300
- }: MillerColumnsPreviewPanelProps): ReactElement {
301
- useMillerColumnsContext();
431
+ }: MillerColumnsPreviewPanelProps): ReactElement | null {
432
+ const { activePath, terminalIsLeaf } = useMillerColumnsContext();
433
+
434
+ // Finder's rule: the preview occupies the slot *after* the selected item, so
435
+ // it exists only where that item has no children to open there instead.
436
+ if (!forceMount && !terminalIsLeaf) {
437
+ return null;
438
+ }
302
439
 
303
440
  return (
304
- <div data-miller-columns-preview="" {...rest}>
441
+ <div
442
+ data-miller-columns-preview=""
443
+ {...(activePath.length === 0 ? { "data-empty": "" } : {})}
444
+ {...rest}
445
+ >
305
446
  {children}
306
447
  </div>
307
448
  );
@@ -329,7 +470,7 @@ type MillerColumnsCompound = typeof MillerColumnsRoot & {
329
470
  /**
330
471
  * MillerColumns — a horizontal strip of vertical lists where selecting a
331
472
  * node reveals its children in the next column. Use as a namespace
332
- * (`MillerColumns.Root`, `MillerColumns.Column`, `MillerColumns.Item`, …);
473
+ * (`MillerColumns.Root`, `MillerColumns.Column`, `MillerColumns.Item`, ...);
333
474
  * the default export is also callable as the Root.
334
475
  */
335
476
  const MillerColumnsCompound: MillerColumnsCompound = Object.assign(