@primitiv-ui/react 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/README.md +19 -2
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -1,4 +1,11 @@
1
- import { PointerEvent, useEffect, useState } from "react";
1
+ import {
2
+ KeyboardEvent,
3
+ PointerEvent,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ } from "react";
2
9
 
3
10
  import type { MillerColumnsResizeHandleProps } from "../types";
4
11
 
@@ -9,12 +16,43 @@ type DragState = { startX: number; startWidth: number };
9
16
 
10
17
  export function useMillerColumnsResizeHandle({
11
18
  onPointerDown,
19
+ onKeyDown,
20
+ minWidth = 0,
21
+ maxWidth = Number.POSITIVE_INFINITY,
22
+ step = 10,
12
23
  ...rest
13
24
  }: MillerColumnsResizeHandleProps) {
14
- const { columnWidths, setColumnWidth } = useMillerColumnsContext();
25
+ const { columnWidths, setColumnWidth, dir } = useMillerColumnsContext();
15
26
  const { depth } = useMillerColumnsColumnContext();
16
27
 
28
+ const handleRef = useRef<HTMLDivElement>(null);
17
29
  const [drag, setDrag] = useState<DragState | null>(null);
30
+ // The column's rendered width before anything resizes it. Tracked so
31
+ // `aria-valuenow` is truthful from the first render — a splitter whose
32
+ // value only appears after the first drag announces nothing until then.
33
+ const [measured, setMeasured] = useState(0);
34
+
35
+ const clamp = (width: number) =>
36
+ Math.min(maxWidth, Math.max(minWidth, width));
37
+
38
+ // The resized width wins; otherwise the column is still sized by CSS.
39
+ const width = columnWidths.get(depth) ?? measured;
40
+
41
+ useLayoutEffect(() => {
42
+ const column = handleRef.current?.closest<HTMLElement>(
43
+ "[data-miller-columns-column]",
44
+ );
45
+ /* c8 ignore next 3 -- the handle only renders inside a Column, which
46
+ context already enforces, so the ancestor is always present. */
47
+ if (!column) {
48
+ return;
49
+ }
50
+ const measure = () => setMeasured(column.getBoundingClientRect().width);
51
+ measure();
52
+ const observer = new ResizeObserver(measure);
53
+ observer.observe(column);
54
+ return () => observer.disconnect();
55
+ }, []);
18
56
 
19
57
  // A drag is tracked on `window`, not the handle, so the pointer can
20
58
  // travel anywhere once it is down; the listeners live only for the
@@ -27,13 +65,8 @@ export function useMillerColumnsResizeHandle({
27
65
  const activeDrag = drag;
28
66
 
29
67
  function handleMove(event: globalThis.PointerEvent) {
30
- setColumnWidth(
31
- depth,
32
- Math.max(
33
- 0,
34
- activeDrag.startWidth + event.clientX - activeDrag.startX,
35
- ),
36
- );
68
+ const next = activeDrag.startWidth + event.clientX - activeDrag.startX;
69
+ setColumnWidth(depth, Math.min(maxWidth, Math.max(minWidth, next)));
37
70
  }
38
71
 
39
72
  function endDrag() {
@@ -48,7 +81,7 @@ export function useMillerColumnsResizeHandle({
48
81
  window.removeEventListener("pointerup", endDrag);
49
82
  window.removeEventListener("pointercancel", endDrag);
50
83
  };
51
- }, [drag, depth, setColumnWidth]);
84
+ }, [drag, depth, setColumnWidth, minWidth, maxWidth]);
52
85
 
53
86
  function handlePointerDown(event: PointerEvent<HTMLDivElement>) {
54
87
  onPointerDown?.(event);
@@ -63,13 +96,50 @@ export function useMillerColumnsResizeHandle({
63
96
  setDrag({ startX: event.clientX, startWidth });
64
97
  }
65
98
 
99
+ // The column extends towards the inline-end edge, which is leftwards
100
+ // under rtl — so the key that widens it mirrors with the direction.
101
+ const growKey = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
102
+ const shrinkKey = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
103
+
104
+ function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
105
+ onKeyDown?.(event);
106
+
107
+ const next = (() => {
108
+ switch (event.key) {
109
+ case growKey:
110
+ return width + step;
111
+ case shrinkKey:
112
+ return width - step;
113
+ case "Home":
114
+ return minWidth;
115
+ case "End":
116
+ return maxWidth;
117
+ default:
118
+ return null;
119
+ }
120
+ })();
121
+
122
+ if (next === null || !Number.isFinite(next)) {
123
+ return;
124
+ }
125
+
126
+ event.preventDefault();
127
+ setColumnWidth(depth, clamp(next));
128
+ }
129
+
66
130
  return {
67
131
  handleProps: {
132
+ ref: handleRef,
68
133
  role: "separator" as const,
69
134
  "aria-orientation": "vertical" as const,
135
+ tabIndex: 0,
136
+ "aria-valuenow": Math.round(width),
137
+ "aria-valuemin": minWidth,
138
+ ...(Number.isFinite(maxWidth) ? { "aria-valuemax": maxWidth } : {}),
70
139
  "data-miller-columns-resize-handle": "",
71
140
  ...(drag ? { "data-dragging": "" } : {}),
72
141
  onPointerDown: handlePointerDown,
142
+ onKeyDown: handleKeyDown,
73
143
  ...rest,
74
144
  },
75
145
  };
@@ -1,3 +1,7 @@
1
1
  export * from "./MillerColumns";
2
+ export {
3
+ MILLER_COLUMNS_PART,
4
+ type MillerColumnsPartName,
5
+ } from "./utils";
2
6
  export { useMillerColumnsSelection } from "./useMillerColumnsSelection";
3
7
  export * from "./types";
@@ -1,77 +1,173 @@
1
- import { ComponentProps, ReactNode, Ref } from "react";
1
+ import { ComponentProps, KeyboardEvent, ReactNode, Ref } from "react";
2
2
 
3
- /** Props common to both controlled and uncontrolled `MillerColumns.Root` modes. */
4
- export type MillerColumnsRootBaseProps = Omit<ComponentProps<"div">, "ref"> & {
5
- /** The columns and panels that make up the strip. */
3
+ import type { Direction } from "../DirectionProvider/types";
4
+
5
+ /**
6
+ * Shared base for both {@link MillerColumnsRootProps} variants — the native
7
+ * `<div>` attributes (minus `ref`, which the strip owns internally, and
8
+ * `dir`, narrowed below to the {@link Direction} union).
9
+ */
10
+ export type MillerColumnsRootBaseProps = Omit<
11
+ ComponentProps<"div">,
12
+ "ref" | "defaultValue" | "dir"
13
+ > & {
14
+ /**
15
+ * Reading direction. Under `"rtl"` the horizontal arrow pair is
16
+ * mirrored — ArrowLeft steps *into* a branch's child column and
17
+ * ArrowRight walks back out — so navigation follows the visual order.
18
+ * Inherited from a `DirectionProvider` ancestor when omitted.
19
+ * @default "ltr"
20
+ */
21
+ dir?: Direction;
22
+ /**
23
+ * The strip's tree, authored recursively: root
24
+ * {@link MillerColumnsColumnProps | Column(s)} whose branch
25
+ * {@link MillerColumnsItemProps | Items} nest further Columns, plus an
26
+ * optional trailing {@link MillerColumnsPreviewPanelProps | PreviewPanel}.
27
+ * There is no `items` data prop — the JSX *is* the data.
28
+ */
6
29
  children: ReactNode;
7
30
  };
8
31
 
9
32
  /**
10
- * Props for `MillerColumns.Root` in uncontrolled mode — the component owns the
11
- * selection path. Pass `defaultValue` to set the initial path.
33
+ * Uncontrolled variant of {@link MillerColumnsRootProps}: the component owns
34
+ * the selection path. Pass `defaultValue` (or omit it to start empty).
12
35
  */
13
36
  export type MillerColumnsRootUncontrolledProps = MillerColumnsRootBaseProps & {
14
- /** Initial selection path when uncontrolled. */
37
+ /** Initial selection path on first render, root column first (e.g.
38
+ * `["docs", "guides"]`). The component owns the path thereafter. */
15
39
  defaultValue?: string[];
16
- /** Forbidden in uncontrolled mode. */
40
+ /** Forbidden in uncontrolled mode — use `defaultValue` instead. */
17
41
  value?: never;
18
- /** Forbidden in uncontrolled mode. */
42
+ /** Forbidden in uncontrolled mode — pass `value` + `onValueChange` to
43
+ * control the component. */
19
44
  onValueChange?: never;
20
45
  };
21
46
 
22
47
  /**
23
- * Props for `MillerColumns.Root` in controlled mode — the parent owns the
48
+ * Controlled variant of {@link MillerColumnsRootProps}: the parent owns the
24
49
  * selection path. Pass `value` and `onValueChange` together.
25
50
  */
26
51
  export type MillerColumnsRootControlledProps = MillerColumnsRootBaseProps & {
27
- /** Forbidden in controlled mode. */
52
+ /** Forbidden in controlled mode — use `value` instead. */
28
53
  defaultValue?: never;
29
- /** The controlled selection path, root column first. */
54
+ /** The controlled selection path, root column first. Must be kept in sync
55
+ * by the parent via `onValueChange`. */
30
56
  value: string[];
31
- /** Called with the new path whenever the selection changes. */
57
+ /** Called with the new selection path whenever the selection changes.
58
+ * Required in controlled mode. */
32
59
  onValueChange: (path: string[]) => void;
33
60
  };
34
61
 
35
- /** Props for `MillerColumns.Root` — discriminated controlled/uncontrolled union. */
62
+ /**
63
+ * Props for {@link MillerColumnsRoot | `MillerColumns.Root`}.
64
+ *
65
+ * Resolves to either {@link MillerColumnsRootUncontrolledProps} or
66
+ * {@link MillerColumnsRootControlledProps} — only one shape is accepted by
67
+ * TypeScript at a time.
68
+ */
36
69
  export type MillerColumnsRootProps =
37
70
  | MillerColumnsRootUncontrolledProps
38
71
  | MillerColumnsRootControlledProps;
39
72
 
40
- /** Props for `MillerColumns.Column`, a single vertical list within the strip. */
73
+ /**
74
+ * Props for {@link MillerColumnsColumn | `MillerColumns.Column`}, a single
75
+ * vertical list. Rendered element is portal-projected into the Root strip.
76
+ */
41
77
  export type MillerColumnsColumnProps = ComponentProps<"div"> & {
42
- /** The items (and optional resize handle) of this column. */
43
- children: ReactNode;
78
+ /** The column's {@link MillerColumnsItemProps | Items} and, optionally, a
79
+ * {@link MillerColumnsResizeHandleProps | ResizeHandle}. Optional: a
80
+ * childless branch legitimately opens an empty column, which carries
81
+ * `data-empty` for the styling layer. */
82
+ children?: ReactNode;
44
83
  };
45
84
 
46
- /** Props for `MillerColumns.Item`, a single selectable node within a column. */
85
+ /**
86
+ * Props for {@link MillerColumnsItem | `MillerColumns.Item`}, a single
87
+ * selectable node within a column.
88
+ *
89
+ * Generic over the rendered element type so `asChild` consumers can type
90
+ * the forwarded `ref` (e.g. `MillerColumns.Item<HTMLAnchorElement>`).
91
+ */
47
92
  export type MillerColumnsItemProps<T extends HTMLElement = HTMLDivElement> =
48
93
  Omit<ComponentProps<"div">, "ref"> & {
49
- /** Stable identifier used to match this item against the selection path. */
94
+ /** Stable identifier used to match this item against the selection path.
95
+ * The item is selected when the path entry at its depth equals `value`. */
50
96
  value: string;
51
- /** When `true`, the item cannot be selected or focused. */
97
+ /**
98
+ * When `true`, the item renders `aria-disabled` / `data-disabled` and is
99
+ * skipped by roving navigation — it cannot be selected, focused, or
100
+ * activated.
101
+ * @default false
102
+ */
52
103
  disabled?: boolean;
53
- /** Render the child element instead of the default `<div>`. */
104
+ /**
105
+ * Render the cell as a single consumer-supplied child element (e.g. an
106
+ * `<a>`) instead of the default `<div>`, merging the treeitem ARIA,
107
+ * handlers, and ref onto it via the {@link Slot} pattern. Any nested
108
+ * `<MillerColumns.Column>` remains a sibling of the cell element.
109
+ * @default false
110
+ */
54
111
  asChild?: boolean;
55
- /** Cell content and, for a branch, a nested `<MillerColumns.Column>`. */
112
+ /** The cell content and, for a branch item, a single nested
113
+ * `<MillerColumns.Column>` (split out by `partitionItemChildren`). */
56
114
  children: ReactNode;
57
- /** Ref to the rendered element. Defaults to `HTMLDivElement`; when using
58
- * `asChild`, specify the child's element type. */
115
+ /** Forwarded to the rendered element. Defaults to `HTMLDivElement`; when
116
+ * using `asChild`, specify the child's element type. Composed with the
117
+ * library's internal ref. */
59
118
  ref?: Ref<T>;
60
119
  };
61
120
 
62
- /** Props for `MillerColumns.ItemIndicator`, the branch chevron affordance. */
121
+ /**
122
+ * Props for {@link MillerColumnsItemIndicator | `MillerColumns.ItemIndicator`},
123
+ * the branch chevron affordance (renders nothing for leaf items).
124
+ */
63
125
  export type MillerColumnsItemIndicatorProps = ComponentProps<"span"> & {
64
- /** The glyph to render (defaults to none). */
126
+ /** The glyph to render (e.g. `▸`). Optional. */
65
127
  children?: ReactNode;
66
128
  };
67
129
 
68
- /** Props for `MillerColumns.ResizeHandle`, the drag-to-resize separator. */
69
- export type MillerColumnsResizeHandleProps = ComponentProps<"div">;
130
+ /**
131
+ * Props for {@link MillerColumnsResizeHandle | `MillerColumns.ResizeHandle`},
132
+ * the drag-to-resize separator for its containing column.
133
+ */
134
+ export type MillerColumnsResizeHandleProps = ComponentProps<"div"> & {
135
+ /**
136
+ * Smallest width the column can be dragged or stepped to, in pixels.
137
+ * Also published as `aria-valuemin`.
138
+ * @default 0
139
+ */
140
+ minWidth?: number;
141
+ /**
142
+ * Largest width the column can be dragged or stepped to, in pixels.
143
+ * Published as `aria-valuemax` when finite; left unbounded by default,
144
+ * in which case the attribute is omitted.
145
+ * @default Infinity
146
+ */
147
+ maxWidth?: number;
148
+ /**
149
+ * Pixels moved per arrow-key press.
150
+ * @default 10
151
+ */
152
+ step?: number;
153
+ };
70
154
 
71
- /** Props for `MillerColumns.PreviewPanel`, the trailing preview pane. */
155
+ /**
156
+ * Props for {@link MillerColumnsPreviewPanel | `MillerColumns.PreviewPanel`},
157
+ * the trailing preview pane.
158
+ */
72
159
  export type MillerColumnsPreviewPanelProps = ComponentProps<"div"> & {
73
- /** The preview content for the current selection. */
160
+ /** The preview content for the current selection (typically driven by
161
+ * {@link useMillerColumnsSelection}). */
74
162
  children?: ReactNode;
163
+ /**
164
+ * Keep the panel mounted regardless of the selection, instead of following
165
+ * Finder's rule (a preview only where a selected *leaf* would open a child
166
+ * column). Use it for a persistent inspector, or to animate the panel in
167
+ * and out.
168
+ * @default false
169
+ */
170
+ forceMount?: boolean;
75
171
  };
76
172
 
77
173
  /** The current selection, as returned by `useMillerColumnsSelection`. */
@@ -100,6 +196,8 @@ export type MillerColumnsItemAddress = {
100
196
 
101
197
  /** Value shared through the root MillerColumns context. */
102
198
  export type MillerColumnsContextValue = {
199
+ /** The resolved reading direction — the `dir` prop, else the inherited one. */
200
+ dir: Direction;
103
201
  /** Portal slots keyed by column depth. */
104
202
  slots: Map<number, HTMLElement>;
105
203
  /** Per-depth column widths set via the resize handle. */
@@ -116,9 +214,15 @@ export type MillerColumnsContextValue = {
116
214
  value: string,
117
215
  element: HTMLElement | null,
118
216
  disabled: boolean,
217
+ hasChildren: boolean,
119
218
  ) => void;
219
+ /** Whether the deepest selected item is a leaf — Finder's rule for whether a
220
+ * preview occupies the slot after it. `false` when nothing is selected. */
221
+ terminalIsLeaf: boolean;
120
222
  /** Get the registered items for a column. */
121
223
  getColumnItems: (depth: number) => MillerColumnsItemMeta[];
224
+ /** Read a registered item's rendered cell text, for typeahead matching. */
225
+ getItemLabel: (depth: number, value: string) => string;
122
226
  /** Move focus to the given item. */
123
227
  focusItem: (depth: number, value: string) => void;
124
228
  /** Focus the first item in a column; returns whether one was focused. */
@@ -129,6 +233,8 @@ export type MillerColumnsContextValue = {
129
233
  activeItem: MillerColumnsItemAddress | null;
130
234
  /** Set the active (focused) item. */
131
235
  setActiveItem: (item: MillerColumnsItemAddress) => void;
236
+ /** Printable-character typeahead, scoped to the focused item's column. */
237
+ handleTypeahead: (event: KeyboardEvent<HTMLElement>) => void;
132
238
  };
133
239
 
134
240
  /** Value shared through a column's context. */
@@ -23,11 +23,77 @@ export function selectAtDepth(
23
23
  }
24
24
 
25
25
  /**
26
- * Whether `node` is a `MillerColumns.Column` element — the identity
27
- * check used to separate an Item's cell content from its child column.
26
+ * Splits a props bag into its `aria-*` entries and everything else.
27
+ *
28
+ * `Root` renders two elements — a plain strip container and the inner
29
+ * `role="tree"` widget — so one props bag has two possible homes. ARIA
30
+ * belongs on the widget (an `aria-label` must name the tree, not its
31
+ * scroll container); `className`, `id`, `style` and the rest belong on
32
+ * the container the consumer actually lays out.
33
+ */
34
+ export function partitionAriaProps(props: Record<string, unknown>): {
35
+ aria: Record<string, unknown>;
36
+ rest: Record<string, unknown>;
37
+ } {
38
+ const aria: Record<string, unknown> = {};
39
+ const rest: Record<string, unknown> = {};
40
+
41
+ for (const [key, value] of Object.entries(props)) {
42
+ if (key.startsWith("aria-")) {
43
+ aria[key] = value;
44
+ } else {
45
+ rest[key] = value;
46
+ }
47
+ }
48
+
49
+ return { aria, rest };
50
+ }
51
+
52
+ /**
53
+ * Marker a styling layer sets on a component that renders a `MillerColumns`
54
+ * part, so this module recognises the wrapper as that part.
55
+ *
56
+ * `Item` inspects its children **before** rendering them, so it sees the
57
+ * wrapper component rather than the `MillerColumns.Column` the wrapper
58
+ * returns. Without a marker a wrapped child column is mis-read as cell
59
+ * content: the item stops being a branch (no `aria-expanded`, no chevron, no
60
+ * ArrowRight) and — worse — the column projects into whatever depth slot
61
+ * happens to be open, so selecting *one* branch reveals *every* sibling's
62
+ * children at once.
63
+ *
64
+ * ```tsx
65
+ * function StyledColumn(props: MillerColumnsColumnProps) {
66
+ * return <MillerColumns.Column {...props} className="column" />;
67
+ * }
68
+ * StyledColumn[MILLER_COLUMNS_PART] = "column";
69
+ * ```
70
+ */
71
+ export const MILLER_COLUMNS_PART: "__primitivMillerColumnsPart" =
72
+ "__primitivMillerColumnsPart";
73
+
74
+ /** The part names {@link MILLER_COLUMNS_PART} accepts. */
75
+ export type MillerColumnsPartName = "column";
76
+
77
+ /** Read a component's {@link MILLER_COLUMNS_PART} marker, if it declares one. */
78
+ function millerColumnsPartOf(
79
+ type: unknown,
80
+ ): MillerColumnsPartName | undefined {
81
+ return (type as Record<string, MillerColumnsPartName | undefined> | null)?.[
82
+ MILLER_COLUMNS_PART
83
+ ];
84
+ }
85
+
86
+ /**
87
+ * Whether `node` is a `MillerColumns.Column` element — either the part itself,
88
+ * or a component that marks itself as standing in for it (see
89
+ * {@link MILLER_COLUMNS_PART}).
28
90
  */
29
91
  export function isColumnElement(node: ReactNode): node is ReactElement {
30
- return isValidElement(node) && node.type === MillerColumnsColumn;
92
+ return (
93
+ isValidElement(node) &&
94
+ (node.type === MillerColumnsColumn ||
95
+ millerColumnsPartOf(node.type) === "column")
96
+ );
31
97
  }
32
98
 
33
99
  /**
@@ -51,7 +51,7 @@ import {
51
51
  * @example Uncontrolled
52
52
  * ```tsx
53
53
  * <Modal.Root defaultOpen>
54
- * <Modal.Content>…</Modal.Content>
54
+ * <Modal.Content>...</Modal.Content>
55
55
  * </Modal.Root>
56
56
  * ```
57
57
  *
@@ -63,7 +63,7 @@ import {
63
63
  * <Modal.Trigger>Open</Modal.Trigger>
64
64
  * <Modal.Portal>
65
65
  * <Modal.Overlay />
66
- * <Modal.Content>…</Modal.Content>
66
+ * <Modal.Content>...</Modal.Content>
67
67
  * </Modal.Portal>
68
68
  * </Modal.Root>
69
69
  * ```
@@ -97,6 +97,8 @@ ModalRoot.displayName = "ModalRoot";
97
97
  * element (e.g. a router `<Link>`) with the trigger's ARIA attributes,
98
98
  * composed event handlers, and ref merged in.
99
99
  *
100
+ * @extends HTMLButtonElement
101
+ *
100
102
  * @example
101
103
  * ```tsx
102
104
  * <Modal.Trigger>Open</Modal.Trigger>
@@ -138,7 +140,7 @@ ModalTrigger.displayName = "ModalTrigger";
138
140
  * ```tsx
139
141
  * <Modal.Portal container={document.getElementById("modal-root")!}>
140
142
  * <Modal.Overlay />
141
- * <Modal.Content>…</Modal.Content>
143
+ * <Modal.Content>...</Modal.Content>
142
144
  * </Modal.Portal>
143
145
  * ```
144
146
  */
@@ -177,6 +179,8 @@ ModalPortal.displayName = "ModalPortal";
177
179
  * **`forceMount` prop.** Pass `forceMount` to keep the overlay in the
178
180
  * DOM while `open` is false so a CSS exit animation can play.
179
181
  *
182
+ * @extends HTMLDivElement
183
+ *
180
184
  * @example
181
185
  * ```tsx
182
186
  * <Modal.Overlay />
@@ -215,8 +219,15 @@ ModalOverlay.displayName = "ModalOverlay";
215
219
  /**
216
220
  * The native `<dialog>` element. React drives its `showModal()` and
217
221
  * `close()` calls in response to `open` state changes, letting the
218
- * browser own focus trapping, inert background, top-layer rendering,
219
- * and the `Esc` key.
222
+ * browser own the inert background, top-layer rendering, and the `Esc`
223
+ * key.
224
+ *
225
+ * **Focus trapping.** `showModal()` inerts the background but does
226
+ * *not* cycle focus — `Tab` past the last element escapes to the
227
+ * browser chrome, then wraps back in. To conform to the WAI-ARIA APG
228
+ * Modal Dialog pattern, a `keydown` handler wraps `Tab` (last → first)
229
+ * and `Shift+Tab` (first → last); interior Tabs are left to the
230
+ * browser's native, inert-aware order.
220
231
  *
221
232
  * **ARIA wiring.** `aria-labelledby` and `aria-describedby` are set
222
233
  * automatically when `Modal.Title` and `Modal.Description` are rendered
@@ -234,11 +245,21 @@ ModalOverlay.displayName = "ModalOverlay";
234
245
  * `PointerEvent` is passed through, not a synthetic React event.
235
246
  *
236
247
  * **`asChild` is intentionally not supported.** The native dialog
237
- * primitive is what provides the focus trap and the inert background;
238
- * swapping it for a `<div>` would break both.
248
+ * primitive is what provides the inert background and top-layer
249
+ * stacking (and hosts the Tab-wrap handler above); swapping it for a
250
+ * `<div>` would break all of it.
239
251
  *
240
252
  * **Styling hook.** `data-state="open" | "closed"` on the dialog.
241
253
  *
254
+ * **Nested modals.** A `Modal.Root` may be rendered inside another modal's
255
+ * `Modal.Content`. Each root owns its own {@link ModalContextValue | context}
256
+ * and dialog, so every dialog stacks independently in the browser's top
257
+ * layer and each dismissal gesture (Close, Esc, click-outside) resolves
258
+ * against the nearest dialog only — closing the inner modal leaves the
259
+ * outer one open.
260
+ *
261
+ * @extends HTMLDialogElement
262
+ *
242
263
  * @example
243
264
  * ```tsx
244
265
  * <Modal.Content
@@ -246,7 +267,7 @@ ModalOverlay.displayName = "ModalOverlay";
246
267
  * if (hasUnsavedChanges) event.preventDefault();
247
268
  * }}
248
269
  * >
249
- * …
270
+ * ...
250
271
  * </Modal.Content>
251
272
  * ```
252
273
  */
@@ -293,6 +314,8 @@ ModalContent.displayName = "ModalContent";
293
314
  * an `<h3>` for a nested dialog or a styled heading component); the
294
315
  * id is still registered.
295
316
  *
317
+ * @extends HTMLHeadingElement
318
+ *
296
319
  * @example
297
320
  * ```tsx
298
321
  * <Modal.Title>Payment</Modal.Title>
@@ -340,6 +363,8 @@ ModalTitle.displayName = "ModalTitle";
340
363
  * Pass `asChild` to render any consumer-supplied element; the id is
341
364
  * still registered.
342
365
  *
366
+ * @extends HTMLParagraphElement
367
+ *
343
368
  * @example
344
369
  * ```tsx
345
370
  * <Modal.Description>
@@ -386,6 +411,8 @@ ModalDescription.displayName = "ModalDescription";
386
411
  * text link or an icon-only `<button>` of your own) with the close
387
412
  * behaviour merged in.
388
413
  *
414
+ * @extends HTMLButtonElement
415
+ *
389
416
  * @example
390
417
  * ```tsx
391
418
  * <Modal.Close>Cancel</Modal.Close>
@@ -5,10 +5,16 @@ A headless, accessible compound component implementing the
5
5
 
6
6
  Modal is built on the native
7
7
  [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)
8
- element and its `showModal()` API, so focus trapping, inert background,
9
- top-layer stacking, and Esc-to-close are handled by the browser. The
10
- React layer adds what native `<dialog>` doesn't give you:
11
-
8
+ element and its `showModal()` API, so the inert background, top-layer
9
+ stacking, and Esc-to-close are handled by the browser. The React layer
10
+ adds what native `<dialog>` doesn't give you:
11
+
12
+ - **Focus trapping.** `showModal()` inerts the background but does
13
+ _not_ cycle focus — Tab past the last element escapes to the browser
14
+ chrome and back. A `keydown` handler on the dialog wraps `Tab`
15
+ (last → first) and `Shift+Tab` (first → last) so focus stays inside,
16
+ as the WAI-ARIA APG requires. Interior Tabs stay native, so the
17
+ browser's inert-aware order (radio groups, etc.) is untouched.
12
18
  - Click-outside-to-close via a `pointerdown` listener on the dialog
13
19
  that checks the pointer against `getBoundingClientRect()` — coords
14
20
  outside the rect mean the pointer landed on the native `::backdrop`.
@@ -68,10 +74,11 @@ README for the canonical composed example; nothing below changes.
68
74
 
69
75
  ## Keyboard interaction
70
76
 
71
- | Key | Behaviour |
72
- | ----- | ----------------------------------------------------------------------------- |
73
- | `Esc` | Closes the modal (native `cancel` event). Preventable via `onEscapeKeyDown` |
74
- | `Tab` | Focus is trapped inside the dialog by the browser's native modal dialog logic |
77
+ | Key | Behaviour |
78
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
79
+ | `Esc` | Closes the modal (native `cancel` event). Preventable via `onEscapeKeyDown` |
80
+ | `Tab` | Wraps from the last focusable back to the first — focus stays inside the dialog. `showModal()` inerts the background but doesn't cycle focus, so this wrap is added by a `keydown` handler |
81
+ | `Shift+Tab` | Wraps from the first focusable to the last |
75
82
 
76
83
  ## State modes
77
84
 
@@ -83,12 +90,12 @@ rejects mixing them.
83
90
 
84
91
  ```tsx
85
92
  // Uncontrolled
86
- <Modal.Root defaultOpen>…</Modal.Root>;
93
+ <Modal.Root defaultOpen>...</Modal.Root>;
87
94
 
88
95
  // Controlled
89
96
  const [open, setOpen] = useState(false);
90
97
  <Modal.Root open={open} onOpenChange={setOpen}>
91
- …
98
+ ...
92
99
  </Modal.Root>;
93
100
  ```
94
101
 
@@ -108,7 +115,7 @@ open. `onPointerDownOutside` receives the native `PointerEvent`
108
115
  if (isRequiredFlow) event.preventDefault();
109
116
  }}
110
117
  >
111
- …
118
+ ...
112
119
  </Modal.Content>
113
120
  ```
114
121
 
@@ -119,7 +126,7 @@ import { Modal, type ModalImperativeApi } from "@primitiv-ui/react";
119
126
 
120
127
  const ref = useRef<ModalImperativeApi>(null);
121
128
 
122
- <Modal.Root ref={ref}>…</Modal.Root>;
129
+ <Modal.Root ref={ref}>...</Modal.Root>;
123
130
 
124
131
  ref.current?.open();
125
132
  ref.current?.close();
@@ -156,8 +163,10 @@ handlers, so only its ARIA and `data-state` are forwarded.
156
163
  ```
157
164
 
158
165
  `Modal.Content` is intentionally **not** slot-able. Its native
159
- `<dialog>` element is the whole reason Modal works without a focus
160
- trap or a scroll-lock library, so we don't expose a way to swap it.
166
+ `<dialog>` element — the inert background and top-layer stacking — is
167
+ the whole reason Modal needs no scroll-lock library and only a minimal
168
+ Tab-wrap (rather than a full focus-trap library), so we don't expose a
169
+ way to swap it.
161
170
 
162
171
  ## Animation hooks
163
172
 
@@ -168,7 +177,7 @@ animation can play against the `data-state="closed"` attribute:
168
177
  ```tsx
169
178
  <Modal.Portal forceMount>
170
179
  <Modal.Overlay forceMount />
171
- <Modal.Content>…</Modal.Content>
180
+ <Modal.Content>...</Modal.Content>
172
181
  </Modal.Portal>
173
182
  ```
174
183