@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
@@ -61,7 +61,7 @@ are ever rendered.
61
61
  | `MillerColumns.Column` | List | A vertical list of items, projected into the strip as `role="group"` |
62
62
  | `MillerColumns.Item` | Tree node | A `role="treeitem"`; branch when it nests a `Column`. Supports `disabled`, `asChild`, `ref` |
63
63
  | `MillerColumns.ItemIndicator` | Icon wrapper | Decorative `aria-hidden` icon, rendered only for branch items |
64
- | `MillerColumns.ResizeHandle` | Resize grip | A `role="separator"` drag handle that sets its column's width |
64
+ | `MillerColumns.ResizeHandle` | Resize grip | A focusable `role="separator"` window splitter — drag or arrow-key it to set its column's width |
65
65
  | `MillerColumns.PreviewPanel` | Preview pane | A content-agnostic trailing panel; pair with `useMillerColumnsSelection` |
66
66
 
67
67
  ## Selection model
@@ -80,7 +80,20 @@ column deeper than _d_ closes.
80
80
  The two shapes are discriminated at the type level: passing
81
81
  `defaultValue` alongside `value` is a type error.
82
82
 
83
- There is no multi-select — only one path is active at a time.
83
+ **Single path only, by decision.** There is no multi-select and there is
84
+ not going to be one — this is settled, not a v1 shortfall awaiting a
85
+ follow-up. A Miller strip's whole premise is that the selection *is* the
86
+ path: column N+1 exists because exactly one item in column N is chosen.
87
+ Two selections in a column have no defined answer for what the next
88
+ column should show. A consumer needing to mark many nodes should keep
89
+ that set themselves and render it as content inside each `Item` (a
90
+ checkbox, a badge), leaving the path to mean navigation.
91
+
92
+ Every item on the path renders `data-state="selected"`, including the
93
+ ancestors the path merely passes through. The **deepest** one also gets
94
+ `data-terminal`. Style the two apart — with a uniform selected state
95
+ there is no way to tell which of several highlighted rows was actually
96
+ clicked.
84
97
 
85
98
  ## Keyboard interaction
86
99
 
@@ -92,6 +105,18 @@ There is no multi-select — only one path is active at a time.
92
105
  | `ArrowRight` | Branch: select it and move focus to its child column's first item; leaf: no-op |
93
106
  | `ArrowLeft` | Move focus to the selected item of the parent column |
94
107
  | `Tab` | Move into / out of the whole tree (single tabstop) |
108
+ | Printable character | Typeahead — focus the first item in the column whose label matches |
109
+
110
+ Under `dir="rtl"` the horizontal pair is mirrored: `ArrowLeft` steps
111
+ *into* a branch's child column and `ArrowRight` walks back out, so the
112
+ step-in key always points at the column it opens. `dir` is inherited
113
+ from a `DirectionProvider` ancestor when omitted.
114
+
115
+ Typeahead accumulates keystrokes into a prefix query that resets after
116
+ 500 ms of inactivity; repeating one character cycles through the items
117
+ starting with it. The query is scoped to the **focused item's own
118
+ column** — each column is an independent list, so typing never jumps the
119
+ cursor sideways.
95
120
 
96
121
  The strip is a single roving-tabindex widget: exactly one item is
97
122
  tabbable at a time. The tabstop follows the last-focused item and
@@ -109,7 +134,20 @@ effect when the strip is itself an overflow container; give
109
134
 
110
135
  ## ARIA
111
136
 
112
- - The strip is `role="tree"`.
137
+ `Root` renders **two** elements: an outer strip container you lay out and
138
+ scroll, and an inner `role="tree"` widget holding only the columns. A
139
+ tree may own nothing but `treeitem` and `group`, so the `PreviewPanel`
140
+ sits outside it.
141
+
142
+ Because of that split, **`aria-*` props go to the tree widget** and every
143
+ other prop (`className`, `id`, `style`, `data-*`) goes to the strip
144
+ container:
145
+
146
+ ```tsx
147
+ <MillerColumns.Root aria-label="Files" className="strip">
148
+ ```
149
+
150
+ - The tree widget is `role="tree"`.
113
151
  - Each `Column` is `role="group"`.
114
152
  - Each `Item` is `role="treeitem"` with `aria-level` (1-based column
115
153
  depth), `aria-selected`, and — on branch items — `aria-expanded`.
@@ -156,10 +194,42 @@ it to the column's trailing edge yourself:
156
194
  ```
157
195
 
158
196
  The first drag measures the column's current rendered width and
159
- resizes from there; later drags resume from the last resized width. A
160
- width is clamped so it can never be dragged below zero — use CSS
161
- `min-width` for any larger floor. Resize is pointer-only; the handle
162
- is not keyboard-operable.
197
+ resizes from there; later drags resume from the last resized width.
198
+
199
+ The handle is a full WAI-ARIA **window splitter**: it is focusable, and
200
+ arrow keys resize it. Pass `minWidth` / `maxWidth` / `step` to bound and
201
+ pace that (they clamp pointer drags too, so CSS `min-width` is no longer
202
+ the only floor).
203
+
204
+ | Prop | Default | Effect |
205
+ | ---------- | ---------- | -------------------------------------------------- |
206
+ | `minWidth` | `0` | Floor for drag and keys; published as `aria-valuemin` |
207
+ | `maxWidth` | `Infinity` | Ceiling; published as `aria-valuemax` only when finite |
208
+ | `step` | `10` | Pixels moved per arrow-key press |
209
+
210
+ | Key | Behaviour |
211
+ | ------------------------- | ---------------------------------- |
212
+ | Inline-end arrow | Widen the column by `step` |
213
+ | Inline-start arrow | Narrow the column by `step` |
214
+ | `Home` / `End` | Jump to `minWidth` / `maxWidth` |
215
+
216
+ The arrow pair mirrors under `dir="rtl"`. `End` does nothing while
217
+ `maxWidth` is unbounded — there is no largest width to jump to.
218
+
219
+ `aria-valuenow` tracks the column's real rendered width via a
220
+ `ResizeObserver`, so it is correct before the first resize and follows
221
+ CSS- or container-driven changes as well.
222
+
223
+ **Give the handle an accessible name** — without one it announces as an
224
+ unnamed separator:
225
+
226
+ ```tsx
227
+ <MillerColumns.ResizeHandle
228
+ aria-label="Resize column"
229
+ minWidth={120}
230
+ maxWidth={400}
231
+ />
232
+ ```
163
233
 
164
234
  ## Preview panel
165
235
 
@@ -199,10 +269,11 @@ function FilePreview() {
199
269
  both controlled and uncontrolled roots, and throws if called outside
200
270
  `MillerColumns.Root`.
201
271
 
202
- The panel ships with no ARIA role. The strip is a `role="tree"`, whose
203
- conforming children are `treeitem`s and `group`s — so give the panel
204
- content its own labelled landmark (`role`, `aria-label`, …) through
205
- props if the preview warrants being announced.
272
+ The panel ships with no ARIA role, and renders *outside* the inner
273
+ `role="tree"` widget — a tree may own only `treeitem`s and `group`s, so
274
+ the panel could not legally sit inside it. Give the panel content its
275
+ own labelled landmark (`role`, `aria-label`, ...) through props if the
276
+ preview warrants being announced.
206
277
 
207
278
  ## `asChild` composition
208
279
 
@@ -216,7 +287,7 @@ element:
216
287
  ```tsx
217
288
  <MillerColumns.Item<HTMLAnchorElement> asChild value="docs">
218
289
  <a href="#docs">Docs</a>
219
- <MillerColumns.Column>…</MillerColumns.Column>
290
+ <MillerColumns.Column>...</MillerColumns.Column>
220
291
  </MillerColumns.Item>
221
292
  ```
222
293
 
@@ -229,10 +300,11 @@ Zero styles ship with the component. Style it through `data-*` hooks;
229
300
  typically the strip is `display: flex` and each column scrolls
230
301
  vertically.
231
302
 
232
- Inside the strip, each column is wrapped in a transparent
303
+ Inside the tree element, each column is wrapped in a transparent
233
304
  (`display: contents`) slot element that fixes left-to-right order — so
234
305
  target columns with `[data-miller-columns-column]`, not a direct-child
235
- selector like `[data-miller-columns-strip] > *`.
306
+ selector like `[data-miller-columns-tree] > *`. Note the strip's direct
307
+ children are the tree element and the `PreviewPanel`, not the columns.
236
308
 
237
309
  ```css
238
310
  [data-miller-columns-strip] {
@@ -258,11 +330,12 @@ selector like `[data-miller-columns-strip] > *`.
258
330
  | Element | Attributes |
259
331
  | --------------- | --------------------------------------------------------------------------- |
260
332
  | Strip (`Root`) | `data-miller-columns-strip`, `data-orientation="horizontal"` |
261
- | `Column` | `data-miller-columns-column`, `data-depth` |
262
- | `Item` | `data-state="selected" \| "unselected"`, `data-depth`, `data-has-children`, `data-disabled` |
333
+ | Tree (`Root`) | `data-miller-columns-tree`, `role="tree"` — the inner widget element |
334
+ | `Column` | `data-miller-columns-column`, `data-depth`, `data-empty` (no items) |
335
+ | `Item` | `data-state="selected" \| "unselected"`, `data-terminal` (deepest selected), `data-depth`, `data-has-children`, `data-disabled` |
263
336
  | `ItemIndicator` | `data-state`, `data-has-children` |
264
337
  | `ResizeHandle` | `data-miller-columns-resize-handle`, `data-dragging` (present mid-drag) |
265
- | `PreviewPanel` | `data-miller-columns-preview` |
338
+ | `PreviewPanel` | `data-miller-columns-preview`, `data-empty` (nothing selected) |
266
339
 
267
340
  ## Deferred / follow-up work
268
341
 
@@ -273,6 +346,3 @@ good candidates for later, independent cycles:
273
346
  exists, a leaf image `Item` could open a context menu on
274
347
  right-click whose first entry ("Preview", with an eye icon) opens
275
348
  the `Modal` to show the image larger.
276
- 2. **Keyboard-operable resize.** `MillerColumns.ResizeHandle` is
277
- pointer-only; an arrow-key resize on the focused handle would round
278
- out the WAI-ARIA window-splitter pattern.
@@ -67,6 +67,51 @@ describe("MillerColumns — aria", () => {
67
67
  );
68
68
  });
69
69
 
70
+ it("puts the tree role on an inner element, not the strip container", () => {
71
+ const { container } = render(<Tree />);
72
+
73
+ const strip = container.querySelector("[data-miller-columns-strip]");
74
+ const tree = screen.getByRole("tree");
75
+
76
+ expect(strip).not.toBe(tree);
77
+ expect(strip).toContainElement(tree);
78
+ });
79
+
80
+ it("keeps the preview panel outside the tree", () => {
81
+ render(
82
+ <MillerColumns.Root defaultValue={["a"]}>
83
+ <MillerColumns.Column>
84
+ <MillerColumns.Item value="a">A</MillerColumns.Item>
85
+ </MillerColumns.Column>
86
+ <MillerColumns.PreviewPanel data-testid="preview">
87
+ Preview
88
+ </MillerColumns.PreviewPanel>
89
+ </MillerColumns.Root>,
90
+ );
91
+
92
+ // A tree may only own treeitem and group; the panel is neither.
93
+ expect(screen.getByRole("tree")).not.toContainElement(
94
+ screen.getByTestId("preview"),
95
+ );
96
+ });
97
+
98
+ it("splits aria-* onto the tree widget and everything else onto the strip", () => {
99
+ const { container } = render(
100
+ <MillerColumns.Root aria-label="Files" className="strip" id="picker">
101
+ <MillerColumns.Column>
102
+ <MillerColumns.Item value="a">A</MillerColumns.Item>
103
+ </MillerColumns.Column>
104
+ </MillerColumns.Root>,
105
+ );
106
+
107
+ const strip = container.querySelector(".strip")!;
108
+
109
+ // The widget is what needs naming; the container is what needs styling.
110
+ expect(screen.getByRole("tree", { name: "Files" })).toBeInTheDocument();
111
+ expect(strip).not.toHaveAttribute("aria-label");
112
+ expect(strip).toHaveAttribute("id", "picker");
113
+ });
114
+
70
115
  it("keeps a treeitem's accessible name to its own cell content", async () => {
71
116
  const user = userEvent.setup();
72
117
 
@@ -32,9 +32,11 @@ describe("MillerColumns — auto-scroll", () => {
32
32
  it("scrolls the strip to reveal a newly opened column", async () => {
33
33
  const user = userEvent.setup();
34
34
 
35
- render(<Tree />);
35
+ const { container } = render(<Tree />);
36
36
 
37
- const strip = screen.getByRole("tree");
37
+ const strip = container.querySelector<HTMLElement>(
38
+ "[data-miller-columns-strip]",
39
+ )!;
38
40
  const scrollTo = vi.spyOn(strip, "scrollTo");
39
41
 
40
42
  await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
@@ -3,8 +3,8 @@ import { render, screen } from "@testing-library/react";
3
3
  import { MillerColumns } from "../MillerColumns";
4
4
 
5
5
  describe("MillerColumns — basic rendering", () => {
6
- it("renders the strip with role=tree and a horizontal orientation hook", () => {
7
- render(
6
+ it("renders a strip container with a horizontal orientation hook", () => {
7
+ const { container } = render(
8
8
  <MillerColumns.Root>
9
9
  <MillerColumns.Column>
10
10
  <MillerColumns.Item value="a">A</MillerColumns.Item>
@@ -12,12 +12,27 @@ describe("MillerColumns — basic rendering", () => {
12
12
  </MillerColumns.Root>,
13
13
  );
14
14
 
15
- const strip = screen.getByRole("tree");
15
+ const strip = container.querySelector("[data-miller-columns-strip]");
16
16
 
17
17
  expect(strip).toBeInTheDocument();
18
18
  expect(strip).toHaveAttribute("data-orientation", "horizontal");
19
19
  });
20
20
 
21
+ it("renders the tree widget inside the strip container", () => {
22
+ const { container } = render(
23
+ <MillerColumns.Root>
24
+ <MillerColumns.Column>
25
+ <MillerColumns.Item value="a">A</MillerColumns.Item>
26
+ </MillerColumns.Column>
27
+ </MillerColumns.Root>,
28
+ );
29
+
30
+ const tree = screen.getByRole("tree");
31
+
32
+ expect(tree).toHaveAttribute("data-miller-columns-tree");
33
+ expect(container.querySelector("[data-miller-columns-strip]")).toContainElement(tree);
34
+ });
35
+
21
36
  it("projects the top-level column into the strip as a group", () => {
22
37
  render(
23
38
  <MillerColumns.Root>
@@ -33,6 +33,27 @@ describe("MillerColumns — data attributes", () => {
33
33
  expect(groups[1]).toHaveAttribute("data-depth", "1");
34
34
  });
35
35
 
36
+ it("flags a column with no items as empty", async () => {
37
+ const user = userEvent.setup();
38
+
39
+ render(
40
+ <MillerColumns.Root>
41
+ <MillerColumns.Column>
42
+ <MillerColumns.Item value="empty-folder">
43
+ Empty folder
44
+ <MillerColumns.Column />
45
+ </MillerColumns.Item>
46
+ </MillerColumns.Column>
47
+ </MillerColumns.Root>,
48
+ );
49
+
50
+ await user.click(screen.getByRole("treeitem", { name: "Empty folder" }));
51
+
52
+ const [root, revealed] = screen.getAllByRole("group");
53
+ expect(root).not.toHaveAttribute("data-empty");
54
+ expect(revealed).toHaveAttribute("data-empty");
55
+ });
56
+
36
57
  it("reflects selection through data-state", async () => {
37
58
  const user = userEvent.setup();
38
59
 
@@ -46,6 +67,33 @@ describe("MillerColumns — data attributes", () => {
46
67
  expect(veg).toHaveAttribute("data-state", "selected");
47
68
  });
48
69
 
70
+ it("marks only the deepest selected item with data-terminal", async () => {
71
+ const user = userEvent.setup();
72
+
73
+ render(<Tree />);
74
+
75
+ const fruit = screen.getByRole("treeitem", { name: "Fruit" });
76
+
77
+ // Selected and deepest — it is the terminal selection.
78
+ await user.click(fruit);
79
+ expect(fruit).toHaveAttribute("data-terminal");
80
+
81
+ // Still selected, but now an ancestor of a deeper selection.
82
+ await user.click(screen.getByRole("treeitem", { name: "Apple" }));
83
+
84
+ expect(fruit).toHaveAttribute("data-state", "selected");
85
+ expect(fruit).not.toHaveAttribute("data-terminal");
86
+ expect(screen.getByRole("treeitem", { name: "Apple" })).toHaveAttribute(
87
+ "data-terminal",
88
+ );
89
+
90
+ // Unselected, but sitting at the same depth the terminal item once
91
+ // occupied — depth alone must not be what earns the attribute.
92
+ expect(
93
+ screen.getByRole("treeitem", { name: "Veg" }),
94
+ ).not.toHaveAttribute("data-terminal");
95
+ });
96
+
49
97
  it("flags branch items with data-has-children and omits it on leaves", () => {
50
98
  render(<Tree />);
51
99
 
@@ -1,6 +1,7 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
 
4
+ import { DirectionProvider } from "../../DirectionProvider/index.ts";
4
5
  import { MillerColumns } from "../MillerColumns";
5
6
  import { verticalArrowCases } from "./MillerColumns.fixtures";
6
7
 
@@ -16,9 +17,9 @@ function FlatList() {
16
17
  );
17
18
  }
18
19
 
19
- function Tree() {
20
+ function Tree({ dir }: { dir?: "ltr" | "rtl" } = {}) {
20
21
  return (
21
- <MillerColumns.Root>
22
+ <MillerColumns.Root dir={dir}>
22
23
  <MillerColumns.Column>
23
24
  <MillerColumns.Item value="fruit">
24
25
  Fruit
@@ -178,4 +179,36 @@ describe("MillerColumns — keyboard interaction", () => {
178
179
  expect(veg).toHaveFocus();
179
180
  });
180
181
  });
182
+
183
+ describe("reading direction", () => {
184
+ it("swaps the horizontal arrow pair under rtl", async () => {
185
+ const user = userEvent.setup();
186
+
187
+ render(<Tree dir="rtl" />);
188
+ screen.getByRole("treeitem", { name: "Fruit" }).focus();
189
+
190
+ // The child column sits to the LEFT in rtl, so ArrowLeft steps in.
191
+ await user.keyboard("{ArrowLeft}");
192
+ expect(screen.getByRole("treeitem", { name: "Apple" })).toHaveFocus();
193
+
194
+ // ...and ArrowRight walks back out to the parent.
195
+ await user.keyboard("{ArrowRight}");
196
+ expect(screen.getByRole("treeitem", { name: "Fruit" })).toHaveFocus();
197
+ });
198
+
199
+ it("inherits the direction from a DirectionProvider", async () => {
200
+ const user = userEvent.setup();
201
+
202
+ render(
203
+ <DirectionProvider dir="rtl">
204
+ <Tree />
205
+ </DirectionProvider>,
206
+ );
207
+ screen.getByRole("treeitem", { name: "Fruit" }).focus();
208
+
209
+ await user.keyboard("{ArrowLeft}");
210
+
211
+ expect(screen.getByRole("treeitem", { name: "Apple" })).toHaveFocus();
212
+ });
213
+ });
181
214
  });
@@ -0,0 +1,96 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { MillerColumns } from "../MillerColumns";
5
+
6
+ function Tree() {
7
+ return (
8
+ <MillerColumns.Root>
9
+ <MillerColumns.Column>
10
+ <MillerColumns.Item value="documents">
11
+ Documents
12
+ <MillerColumns.Column>
13
+ <MillerColumns.Item value="work">Work</MillerColumns.Item>
14
+ </MillerColumns.Column>
15
+ </MillerColumns.Item>
16
+ <MillerColumns.Item value="pictures">
17
+ Pictures
18
+ <MillerColumns.Column>
19
+ <MillerColumns.Item value="holiday">
20
+ Holiday
21
+ <MillerColumns.Column>
22
+ <MillerColumns.Item value="beach">beach.jpg</MillerColumns.Item>
23
+ <MillerColumns.Item value="sunset">sunset.jpg</MillerColumns.Item>
24
+ </MillerColumns.Column>
25
+ </MillerColumns.Item>
26
+ </MillerColumns.Column>
27
+ </MillerColumns.Item>
28
+ </MillerColumns.Column>
29
+ </MillerColumns.Root>
30
+ );
31
+ }
32
+
33
+ const state = (name: string) =>
34
+ screen.getByRole("treeitem", { name }).getAttribute("data-state");
35
+ const terminal = (name: string) =>
36
+ screen.getByRole("treeitem", { name }).hasAttribute("data-terminal");
37
+
38
+ describe("MillerColumns — selection state under keyboard navigation", () => {
39
+ it("deselects the previous branch when arrowing to a sibling and stepping in", async () => {
40
+ const user = userEvent.setup();
41
+
42
+ render(<Tree />);
43
+
44
+ await user.click(screen.getByRole("treeitem", { name: "Documents" }));
45
+ expect(state("Documents")).toBe("selected");
46
+
47
+ // Down to Pictures, then right to open it — Documents must let go.
48
+ await user.keyboard("{ArrowDown}{ArrowRight}");
49
+
50
+ expect(state("Pictures")).toBe("selected");
51
+ expect(state("Documents")).toBe("unselected");
52
+ });
53
+
54
+ it("keeps exactly one terminal item as focus moves deeper", async () => {
55
+ const user = userEvent.setup();
56
+
57
+ render(<Tree />);
58
+
59
+ await user.click(screen.getByRole("treeitem", { name: "Pictures" }));
60
+ await user.click(screen.getByRole("treeitem", { name: "Holiday" }));
61
+
62
+ expect(state("Pictures")).toBe("selected");
63
+ expect(terminal("Pictures")).toBe(false);
64
+ expect(terminal("Holiday")).toBe(true);
65
+
66
+ // Arrowing within the revealed column moves focus only — it must not
67
+ // change which row is terminal.
68
+ await user.keyboard("{ArrowRight}{ArrowDown}");
69
+
70
+ expect(screen.getByRole("treeitem", { name: "sunset.jpg" })).toHaveFocus();
71
+ expect(terminal("Holiday")).toBe(true);
72
+ expect(terminal("sunset.jpg")).toBe(false);
73
+ expect(state("Documents")).toBe("unselected");
74
+ });
75
+ });
76
+
77
+ describe("MillerColumns — a selected leaf deep in the path", () => {
78
+ it("marks the leaf terminal and its ancestors selected", async () => {
79
+ const user = userEvent.setup();
80
+
81
+ render(<Tree />);
82
+
83
+ await user.click(screen.getByRole("treeitem", { name: "Pictures" }));
84
+ await user.click(screen.getByRole("treeitem", { name: "Holiday" }));
85
+ await user.click(screen.getByRole("treeitem", { name: "beach.jpg" }));
86
+
87
+ expect(state("Pictures")).toBe("selected");
88
+ expect(terminal("Pictures")).toBe(false);
89
+ expect(state("Holiday")).toBe("selected");
90
+ expect(terminal("Holiday")).toBe(false);
91
+
92
+ // The row the user actually landed on.
93
+ expect(state("beach.jpg")).toBe("selected");
94
+ expect(terminal("beach.jpg")).toBe(true);
95
+ });
96
+ });
@@ -1,11 +1,12 @@
1
1
  import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
2
3
 
3
4
  import { MillerColumns } from "../MillerColumns";
4
5
 
5
6
  describe("MillerColumns — preview panel", () => {
6
7
  it("renders a panel at the trailing edge of the strip", () => {
7
- render(
8
- <MillerColumns.Root>
8
+ const { container } = render(
9
+ <MillerColumns.Root defaultValue={["a"]}>
9
10
  <MillerColumns.Column>
10
11
  <MillerColumns.Item value="a">A</MillerColumns.Item>
11
12
  </MillerColumns.Column>
@@ -13,7 +14,7 @@ describe("MillerColumns — preview panel", () => {
13
14
  </MillerColumns.Root>,
14
15
  );
15
16
 
16
- const strip = screen.getByRole("tree");
17
+ const strip = container.querySelector("[data-miller-columns-strip]")!;
17
18
  const panel = screen.getByText("preview");
18
19
 
19
20
  expect(panel).toHaveAttribute("data-miller-columns-preview");
@@ -21,9 +22,33 @@ describe("MillerColumns — preview panel", () => {
21
22
  expect(strip.lastElementChild).toBe(panel);
22
23
  });
23
24
 
24
- it("forwards props to the panel element", () => {
25
+ // Only reachable under forceMount: without it the panel does not exist while
26
+ // nothing is selected, so there is nothing to flag.
27
+ it("flags a force-mounted panel as empty until something is selected", async () => {
28
+ const user = userEvent.setup();
29
+
25
30
  render(
26
31
  <MillerColumns.Root>
32
+ <MillerColumns.Column>
33
+ <MillerColumns.Item value="a">A</MillerColumns.Item>
34
+ </MillerColumns.Column>
35
+ <MillerColumns.PreviewPanel forceMount>
36
+ preview
37
+ </MillerColumns.PreviewPanel>
38
+ </MillerColumns.Root>,
39
+ );
40
+
41
+ const panel = screen.getByText("preview");
42
+ expect(panel).toHaveAttribute("data-empty");
43
+
44
+ await user.click(screen.getByRole("treeitem", { name: "A" }));
45
+
46
+ expect(panel).not.toHaveAttribute("data-empty");
47
+ });
48
+
49
+ it("forwards props to the panel element", () => {
50
+ render(
51
+ <MillerColumns.Root defaultValue={["a"]}>
27
52
  <MillerColumns.Column>
28
53
  <MillerColumns.Item value="a">A</MillerColumns.Item>
29
54
  </MillerColumns.Column>
@@ -0,0 +1,79 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { MillerColumns } from "../MillerColumns";
5
+
6
+ function Tree({ forceMount }: { forceMount?: boolean } = {}) {
7
+ return (
8
+ <MillerColumns.Root>
9
+ <MillerColumns.Column>
10
+ <MillerColumns.Item value="fruit">
11
+ Fruit
12
+ <MillerColumns.Column>
13
+ <MillerColumns.Item value="apple">Apple</MillerColumns.Item>
14
+ </MillerColumns.Column>
15
+ </MillerColumns.Item>
16
+ <MillerColumns.Item value="veg">Veg</MillerColumns.Item>
17
+ </MillerColumns.Column>
18
+ <MillerColumns.PreviewPanel forceMount={forceMount}>
19
+ preview
20
+ </MillerColumns.PreviewPanel>
21
+ </MillerColumns.Root>
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Finder's rule: the preview occupies the slot *after* the selected item, so it
27
+ * exists only when that item has no children of its own. Select a folder and
28
+ * you get its contents there instead; select nothing and there is no preview
29
+ * at all.
30
+ */
31
+ describe("MillerColumns — preview panel visibility", () => {
32
+ it("renders no preview until something is selected", () => {
33
+ render(<Tree />);
34
+
35
+ expect(screen.queryByText("preview")).not.toBeInTheDocument();
36
+ });
37
+
38
+ it("renders no preview while a branch is selected", async () => {
39
+ const user = userEvent.setup();
40
+
41
+ render(<Tree />);
42
+ await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
43
+
44
+ // The slot after Fruit belongs to its children, not to a preview.
45
+ expect(screen.getByRole("treeitem", { name: "Apple" })).toBeInTheDocument();
46
+ expect(screen.queryByText("preview")).not.toBeInTheDocument();
47
+ });
48
+
49
+ it("renders the preview once a leaf is selected", async () => {
50
+ const user = userEvent.setup();
51
+
52
+ render(<Tree />);
53
+ await user.click(screen.getByRole("treeitem", { name: "Veg" }));
54
+
55
+ expect(screen.getByText("preview")).toBeInTheDocument();
56
+ });
57
+
58
+ it("renders the preview for a leaf nested inside a branch", async () => {
59
+ const user = userEvent.setup();
60
+
61
+ render(<Tree />);
62
+ await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
63
+ await user.click(screen.getByRole("treeitem", { name: "Apple" }));
64
+
65
+ expect(screen.getByText("preview")).toBeInTheDocument();
66
+ });
67
+
68
+ it("keeps the panel mounted throughout when forceMount is set", async () => {
69
+ const user = userEvent.setup();
70
+
71
+ render(<Tree forceMount />);
72
+
73
+ expect(screen.getByText("preview")).toBeInTheDocument();
74
+
75
+ await user.click(screen.getByRole("treeitem", { name: "Fruit" }));
76
+
77
+ expect(screen.getByText("preview")).toBeInTheDocument();
78
+ });
79
+ });