@primitiv-ui/react 0.1.28 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/README.md +4 -0
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +25 -4
  7. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  8. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  9. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  10. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  11. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  12. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  13. package/src/Accordion/types.ts +72 -21
  14. package/src/Alert/Alert.tsx +36 -7
  15. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  16. package/src/Alert/types.ts +18 -2
  17. package/src/Avatar/Avatar.tsx +99 -15
  18. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  19. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  20. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  21. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  22. package/src/Avatar/types.ts +15 -3
  23. package/src/Breadcrumb/Breadcrumb.tsx +136 -18
  24. package/src/Breadcrumb/README.md +47 -0
  25. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  27. package/src/Breadcrumb/types.ts +73 -7
  28. package/src/Button/Button.tsx +2 -0
  29. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  30. package/src/Button/types.ts +21 -0
  31. package/src/Carousel/Carousel.tsx +263 -15
  32. package/src/Carousel/README.md +749 -72
  33. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  34. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  35. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  36. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  37. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  38. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  39. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
  40. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  41. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  42. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  43. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  44. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  45. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  46. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  47. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  48. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  49. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  50. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  51. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  52. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  53. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  54. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  55. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  56. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  57. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  58. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  59. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  60. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  61. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  62. package/src/Carousel/hooks/index.ts +1 -0
  63. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  64. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  65. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  66. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  67. package/src/Carousel/loopEngine.ts +54 -0
  68. package/src/Carousel/types.ts +457 -35
  69. package/src/Checkbox/Checkbox.tsx +9 -1
  70. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  71. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  72. package/src/Checkbox/types.ts +19 -0
  73. package/src/CheckboxCard/CheckboxCard.tsx +13 -4
  74. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  75. package/src/CheckboxCard/types.ts +65 -17
  76. package/src/Collapsible/Collapsible.tsx +49 -3
  77. package/src/Collapsible/README.md +40 -0
  78. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  79. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  80. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  81. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  82. package/src/Collapsible/types.ts +89 -22
  83. package/src/ContextMenu/ContextMenu.tsx +270 -4
  84. package/src/ContextMenu/types.ts +148 -6
  85. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  86. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  87. package/src/DirectionProvider/types.ts +28 -3
  88. package/src/Divider/Divider.tsx +24 -10
  89. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  90. package/src/Divider/types.ts +10 -3
  91. package/src/Drawer/Drawer.tsx +315 -0
  92. package/src/Drawer/README.md +124 -0
  93. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  94. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  95. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  96. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  97. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  98. package/src/Drawer/index.ts +2 -0
  99. package/src/Drawer/types.ts +66 -0
  100. package/src/Dropdown/Dropdown.tsx +117 -0
  101. package/src/Dropdown/types.ts +241 -11
  102. package/src/EmptyState/EmptyState.tsx +74 -4
  103. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  104. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  105. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  106. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  107. package/src/EmptyState/types.ts +49 -6
  108. package/src/Field/Field.tsx +23 -0
  109. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  110. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  111. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  112. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  113. package/src/Field/types.ts +60 -12
  114. package/src/Fieldset/Fieldset.tsx +54 -18
  115. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  116. package/src/Fieldset/types.ts +20 -2
  117. package/src/Input/Input.tsx +14 -9
  118. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  119. package/src/Input/types.ts +19 -2
  120. package/src/InputGroup/InputGroup.tsx +11 -3
  121. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  122. package/src/InputGroup/types.ts +26 -5
  123. package/src/MillerColumns/MillerColumns.tsx +86 -19
  124. package/src/MillerColumns/types.ts +83 -27
  125. package/src/Modal/Modal.tsx +31 -4
  126. package/src/Modal/README.md +19 -10
  127. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  128. package/src/Modal/hooks/useModalContent.ts +32 -0
  129. package/src/Modal/types.ts +110 -29
  130. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  131. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  132. package/src/NavigationMenu/README.md +355 -0
  133. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  134. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  135. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  136. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  137. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  138. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  139. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  140. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  141. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  142. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  143. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  144. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  145. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  146. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  147. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  148. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  149. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  150. package/src/NavigationMenu/constants.ts +5 -0
  151. package/src/NavigationMenu/hooks/index.ts +7 -0
  152. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  153. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  154. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  155. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  156. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  157. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  158. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  159. package/src/NavigationMenu/index.ts +3 -0
  160. package/src/NavigationMenu/types.ts +259 -0
  161. package/src/NavigationMenu/utils.ts +89 -0
  162. package/src/Popover/Popover.tsx +427 -0
  163. package/src/Popover/PopoverContext.ts +13 -0
  164. package/src/Popover/README.md +106 -0
  165. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  166. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  167. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  168. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  169. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  170. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  171. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  172. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  173. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  174. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  175. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  176. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  177. package/src/Popover/hooks/index.ts +3 -0
  178. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  179. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  180. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  181. package/src/Popover/index.ts +2 -0
  182. package/src/Popover/types.ts +197 -0
  183. package/src/Portal/Portal.tsx +50 -10
  184. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  185. package/src/Portal/types.ts +5 -0
  186. package/src/Progress/Progress.tsx +33 -1
  187. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  188. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  189. package/src/Progress/types.ts +20 -4
  190. package/src/Radio/Radio.tsx +9 -1
  191. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  192. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  193. package/src/Radio/types.ts +20 -0
  194. package/src/RadioCard/RadioCard.tsx +11 -0
  195. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  196. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  197. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  198. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  199. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  200. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  201. package/src/RadioCard/types.ts +95 -22
  202. package/src/RadioGroup/RadioGroup.tsx +11 -0
  203. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  204. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  205. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  206. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  207. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  208. package/src/RadioGroup/types.ts +73 -9
  209. package/src/SegmentedControl/README.md +154 -0
  210. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  211. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  212. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  213. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  214. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  215. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  216. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  217. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  218. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  219. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  220. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  221. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  222. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  223. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  224. package/src/SegmentedControl/hooks/index.ts +2 -0
  225. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  226. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
  227. package/src/SegmentedControl/index.ts +2 -0
  228. package/src/SegmentedControl/types.ts +138 -0
  229. package/src/Select/README.md +122 -157
  230. package/src/Select/Select.tsx +595 -68
  231. package/src/Select/SelectContext.ts +38 -0
  232. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  233. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  234. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  235. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  236. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  237. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  238. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  239. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  240. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  241. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  242. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  243. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  244. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  245. package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
  246. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  247. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  248. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  249. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  250. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  251. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  252. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  253. package/src/Select/constants.ts +8 -0
  254. package/src/Select/hooks/index.ts +2 -0
  255. package/src/Select/hooks/useSelectContent.ts +149 -0
  256. package/src/Select/hooks/useSelectRoot.ts +149 -0
  257. package/src/Select/types.ts +199 -31
  258. package/src/SkipNav/SkipNav.tsx +69 -29
  259. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  260. package/src/SkipNav/types.ts +17 -9
  261. package/src/Slider/Slider.tsx +33 -1
  262. package/src/Slider/SliderContext.ts +28 -1
  263. package/src/Slider/types.ts +157 -9
  264. package/src/Slot/Slot.tsx +94 -19
  265. package/src/Slot/__tests__/Slot.test.tsx +110 -0
  266. package/src/Slot/composeEventHandlers.ts +26 -2
  267. package/src/Slot/types.ts +16 -0
  268. package/src/Status/Status.tsx +36 -15
  269. package/src/Status/__tests__/Status.test.tsx +5 -0
  270. package/src/Status/types.ts +12 -1
  271. package/src/Switch/Switch.tsx +15 -2
  272. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  273. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  274. package/src/Switch/types.ts +49 -22
  275. package/src/Table/Table.tsx +44 -10
  276. package/src/Table/types.ts +66 -17
  277. package/src/Tabs/Tabs.tsx +8 -0
  278. package/src/Tabs/types.ts +29 -4
  279. package/src/Textarea/Textarea.tsx +9 -1
  280. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  281. package/src/Textarea/types.ts +20 -3
  282. package/src/Toggle/Toggle.tsx +51 -13
  283. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  284. package/src/Toggle/types.ts +54 -4
  285. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  286. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  287. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  288. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  289. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  290. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  291. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  292. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  293. package/src/ToggleGroup/types.ts +55 -2
  294. package/src/Tooltip/Tooltip.tsx +98 -20
  295. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  296. package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
  297. package/src/Tooltip/types.ts +89 -8
  298. package/src/Tree/Tree.tsx +187 -33
  299. package/src/Tree/types.ts +54 -12
  300. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  301. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  302. package/src/VisuallyHidden/types.ts +11 -1
  303. package/src/index.ts +4 -0
  304. package/src/test/intersectionObserverPolyfill.ts +7 -0
  305. package/src/test/resizeObserverPolyfill.ts +80 -0
  306. package/src/utils/focusable.ts +15 -0
  307. package/src/utils/index.ts +1 -0
@@ -115,4 +115,9 @@ describe("Input basic rendering", () => {
115
115
  // Assert
116
116
  expect(onChange).toHaveBeenCalledTimes(2);
117
117
  });
118
+
119
+ it('sets displayName to "Input" for React DevTools and stack traces', () => {
120
+ // Assert — an empty displayName would render it as an anonymous component.
121
+ expect(Input.displayName).toBe("Input");
122
+ });
118
123
  });
@@ -1,11 +1,28 @@
1
1
  import { ComponentProps, ReactNode, Ref } from "react";
2
2
 
3
3
  /**
4
- * Props for {@link Input} — all native `<input>` attributes plus the
5
- * `asChild` escape hatch and a typed `ref`.
4
+ * Props for {@link Input} — all native `<input>` attributes (with `type`
5
+ * defaulting to `"text"`) plus the `asChild` escape hatch and a typed `ref`.
6
6
  */
7
7
  export type InputProps = ComponentProps<"input"> & {
8
+ /**
9
+ * Renders the child element instead of a native `<input>`, merging all
10
+ * props — `aria-*`, `data-*`, event handlers, `ref` — onto it via
11
+ * {@link Slot}. Event handlers compose (child runs first). `type` is
12
+ * not forwarded in this mode; the child owns its own type semantics.
13
+ * See the `asChild` example on {@link Input}.
14
+ * @default false
15
+ */
8
16
  asChild?: boolean;
17
+ /**
18
+ * Forwarded to the underlying `HTMLInputElement`. Under `asChild`,
19
+ * merged onto the rendered child via {@link Slot}.
20
+ */
9
21
  ref?: Ref<HTMLInputElement>;
22
+ /**
23
+ * Under `asChild`, becomes the single child element `Slot` merges props
24
+ * onto. Not rendered by the native `<input>` path (inputs are void
25
+ * elements in HTML).
26
+ */
10
27
  children?: ReactNode;
11
28
  };
@@ -19,6 +19,8 @@ import { InputGroupAdornmentProps, InputGroupRootProps } from "./types";
19
19
  * element instead of `<div>` — e.g. a `<label>` so a click anywhere on
20
20
  * the frame focuses the wrapped input.
21
21
  *
22
+ * @extends HTMLDivElement
23
+ *
22
24
  * @example Basic frame with no adornments
23
25
  * ```tsx
24
26
  * <InputGroup>
@@ -56,6 +58,7 @@ export function InputGroupRoot({
56
58
  }
57
59
 
58
60
  /** @internal */
61
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
59
62
  InputGroupRoot.displayName = "InputGroupRoot";
60
63
 
61
64
  /**
@@ -76,6 +79,8 @@ InputGroupRoot.displayName = "InputGroupRoot";
76
79
  * search, opens a colour picker, or any other action. Event handlers
77
80
  * compose (child runs first); refs forward to the consumer element.
78
81
  *
82
+ * @extends HTMLSpanElement
83
+ *
79
84
  * @example Decorative leading icon
80
85
  * ```tsx
81
86
  * <InputGroup.LeadingAdornment>
@@ -131,6 +136,8 @@ InputGroupLeadingAdornment.displayName = "InputGroupLeadingAdornment";
131
136
  * element such as `<button>`. Event handlers compose (child runs
132
137
  * first); refs forward to the consumer element.
133
138
  *
139
+ * @extends HTMLSpanElement
140
+ *
134
141
  * @example Trailing clear button via asChild
135
142
  * ```tsx
136
143
  * <InputGroup.TrailingAdornment asChild>
@@ -191,9 +198,10 @@ export type TInputGroupCompound = typeof InputGroupRoot & {
191
198
  * trailing slot, `data-input-group-adornment="trailing"`.
192
199
  *
193
200
  * **State coordination.** None. CSS handles disabled / invalid /
194
- * focus-within styling via `:has()` and `:focus-within`. When `Field`
195
- * lands it will sit *outside* `InputGroup` and own label / error /
196
- * description wiring without InputGroup needing to change.
201
+ * focus-within styling via `:has()` and `:focus-within`. {@link Field}
202
+ * sits *outside* `InputGroup` and owns the label / error / description
203
+ * wiring, so the two compose without `InputGroup` needing to know about
204
+ * it — wrap the `Input` in an `InputGroup` inside a `Field.Root`.
197
205
  *
198
206
  * @example Leading search icon
199
207
  * ```tsx
@@ -103,4 +103,18 @@ describe("InputGroup basic rendering", () => {
103
103
  expect(screen.getByTestId("lead")).toHaveClass("lead");
104
104
  expect(screen.getByTestId("trail")).toHaveClass("trail");
105
105
  });
106
+
107
+ it("sets a displayName on the compound and each adornment", () => {
108
+ // Assert — empty displayNames would render each as anonymous in DevTools.
109
+ // `InputGroup`, `InputGroup.Root`, and the underlying function are one
110
+ // object (Object.assign compound), so the compound's "InputGroup" alias is
111
+ // the observable Root displayName; the adornments are distinct objects.
112
+ expect(InputGroup.displayName).toBe("InputGroup");
113
+ expect(InputGroup.LeadingAdornment.displayName).toBe(
114
+ "InputGroupLeadingAdornment",
115
+ );
116
+ expect(InputGroup.TrailingAdornment.displayName).toBe(
117
+ "InputGroupTrailingAdornment",
118
+ );
119
+ });
106
120
  });
@@ -1,22 +1,43 @@
1
1
  import { ComponentProps, ReactNode, Ref } from "react";
2
2
 
3
3
  /**
4
- * Props for {@link InputGroup.Root} — all `<div>` attributes plus the
5
- * `asChild` escape hatch and a typed `ref`.
4
+ * Props for {@link InputGroupRoot | `InputGroup.Root`} — the frame
5
+ * wrapper. Extends the native `<div>` attributes plus the `asChild`
6
+ * escape hatch and a typed `ref`.
6
7
  */
7
8
  export type InputGroupRootProps = ComponentProps<"div"> & {
9
+ /** Render the consumer's element instead of `<div>` via the
10
+ * {@link Slot} pattern — e.g. a `<label>` so the whole frame is
11
+ * clickable. The `data-input-group` hook is preserved.
12
+ * @default false */
8
13
  asChild?: boolean;
14
+ /** Forwarded to the underlying `HTMLDivElement` (or the `asChild`
15
+ * element). */
9
16
  ref?: Ref<HTMLDivElement>;
17
+ /** The wrapped control and its optional leading / trailing
18
+ * adornments. */
10
19
  children?: ReactNode;
11
20
  };
12
21
 
13
22
  /**
14
- * Props for {@link InputGroup.LeadingAdornment} and
15
- * {@link InputGroup.TrailingAdornment} — all `<span>` attributes plus the
16
- * `asChild` escape hatch and a typed `ref`.
23
+ * Props shared by
24
+ * {@link InputGroupLeadingAdornment | `InputGroup.LeadingAdornment`} and
25
+ * {@link InputGroupTrailingAdornment | `InputGroup.TrailingAdornment`} —
26
+ * the positioned adornment slots. Extends the native `<span>` attributes
27
+ * plus the `asChild` escape hatch and a typed `ref`.
17
28
  */
18
29
  export type InputGroupAdornmentProps = ComponentProps<"span"> & {
30
+ /** Render the consumer's element instead of `<span>` via the
31
+ * {@link Slot} pattern — e.g. an interactive `<button>`. Event
32
+ * handlers compose (child runs first) and the `data-input-group-adornment`
33
+ * hook is preserved.
34
+ * @default false */
19
35
  asChild?: boolean;
36
+ /** Forwarded to the underlying `HTMLSpanElement` (or the `asChild`
37
+ * element). */
20
38
  ref?: Ref<HTMLSpanElement>;
39
+ /** The adornment content — a decorative icon (mark it
40
+ * `aria-hidden="true"`), suffix text, or an interactive control with an
41
+ * accessible name. */
21
42
  children?: ReactNode;
22
43
  };
@@ -33,11 +33,41 @@ import type {
33
33
  * The root of a Miller Columns widget — a horizontal strip of vertical
34
34
  * lists where selecting a node reveals its children in the next column.
35
35
  *
36
- * Renders the strip container (`role="tree"`) into which every
37
- * `MillerColumns.Column` projects itself via a portal. Authoring the tree
38
- * is recursive: an `Item` declares its child column as a *nested*
39
- * `<MillerColumns.Column>`, and the strip flattens those nested columns
40
- * into a single left-to-right row.
36
+ * Renders the strip container (`role="tree"`, `data-orientation="horizontal"`)
37
+ * into which every {@link MillerColumnsColumn | `MillerColumns.Column`}
38
+ * projects itself via a portal. Authoring the tree is **recursive** and
39
+ * data-less: there is no `items` prop — an {@link MillerColumnsItem | `Item`}
40
+ * declares its child column as a *nested* `<MillerColumns.Column>`, and the
41
+ * strip flattens those nested columns into a single left-to-right row by
42
+ * rendering one `display: contents` slot per visible depth and portalling
43
+ * each column into its slot. This fixes left-to-right DOM order by slot
44
+ * order rather than by portal commit order.
45
+ *
46
+ * **Controlled vs uncontrolled.** State is the *selection path* — an array
47
+ * of item ids, root column first (`["docs", "guides"]`). The two modes are
48
+ * statically discriminated at the type level:
49
+ *
50
+ * - **Uncontrolled** — pass {@link MillerColumnsRootUncontrolledProps.defaultValue | `defaultValue`}
51
+ * (or omit it to start empty). The component owns the path.
52
+ * - **Controlled** — pass {@link MillerColumnsRootControlledProps.value | `value`}
53
+ * and {@link MillerColumnsRootControlledProps.onValueChange | `onValueChange`}
54
+ * together; every selection defers back through the callback.
55
+ *
56
+ * **Roving tabindex.** The whole tree has exactly **one** Tab stop
57
+ * (`tabIndex={0}`), not one per column. It follows the last-focused item;
58
+ * before any focus it defaults to the deepest selected item, falling back
59
+ * to the first enabled root item. Within a column, Up/Down/Home/End move
60
+ * focus; ArrowRight opens (and steps into) a branch item's child column,
61
+ * ArrowLeft returns to the parent item.
62
+ *
63
+ * **Context.** Root establishes {@link MillerColumnsContext} (selection
64
+ * path, portal slots, item registry, column widths, and focus commands);
65
+ * each Column provides {@link MillerColumnsColumnContext} (its depth) and
66
+ * each Item {@link MillerColumnsItemContext} (`selected` / `hasChildren`).
67
+ * Read the current selection from consumer content with
68
+ * {@link useMillerColumnsSelection}.
69
+ *
70
+ * @extends HTMLDivElement
41
71
  *
42
72
  * @example
43
73
  * ```tsx
@@ -86,9 +116,20 @@ MillerColumnsRoot.displayName = "MillerColumnsRoot";
86
116
 
87
117
  /**
88
118
  * A single vertical list of items within the strip. Renders a
89
- * `<div role="group">` that is portal-projected into the `Root` strip, so
90
- * a `Column` nested inside an `Item` still appears side-by-side with its
91
- * ancestors rather than in document flow.
119
+ * `<div role="group">` (carrying `data-depth`) that is **portal-projected**
120
+ * into the {@link MillerColumnsRoot | `Root`} strip's depth slot, so a
121
+ * `Column` nested inside an `Item` still appears side-by-side with its
122
+ * ancestors rather than in document flow. Until its slot has mounted the
123
+ * component renders `null`.
124
+ *
125
+ * It reads its depth from {@link MillerColumnsContext} and re-provides it to
126
+ * descendant {@link MillerColumnsItem | Items} via
127
+ * {@link MillerColumnsColumnContext}. A width set by a
128
+ * {@link MillerColumnsResizeHandle | `ResizeHandle`} in this column is
129
+ * applied here as an inline `width` style (consumer `style` still wins for
130
+ * every other property).
131
+ *
132
+ * @extends HTMLDivElement
92
133
  *
93
134
  * @example
94
135
  * ```tsx
@@ -128,27 +169,44 @@ export function MillerColumnsColumn({
128
169
  MillerColumnsColumn.displayName = "MillerColumnsColumn";
129
170
 
130
171
  /**
131
- * A single selectable node within a `Column`. Renders a
132
- * `<div role="treeitem">` for its cell content.
172
+ * A single selectable node within a {@link MillerColumnsColumn | `Column`}.
173
+ * Renders a `<div role="treeitem">` for its cell content, with
174
+ * `aria-selected`, `aria-level` (1-based depth), and `data-state` /
175
+ * `data-depth` styling hooks.
133
176
  *
134
177
  * The {@link MillerColumnsItemProps.value | `value`} prop is the stable
135
178
  * identifier used to match this item against the active selection path.
136
179
  *
137
- * An `Item` becomes a branch by nesting a `<MillerColumns.Column>` among
138
- * its children. That child column is mounted (and projected into the
139
- * strip) only while the item is selected; an item with no nested column
140
- * is a leaf.
180
+ * **Branch vs leaf.** An `Item` becomes a *branch* by nesting a
181
+ * `<MillerColumns.Column>` among its children — `partitionItemChildren`
182
+ * splits the children into the cell content and that single optional
183
+ * nested column (matched however deeply it is wrapped in fragments; a
184
+ * second nested column throws). A branch carries `aria-expanded` and
185
+ * mounts its child column (projecting it into the strip) only while the
186
+ * item is selected; an item with no nested column is a *leaf*.
187
+ *
188
+ * **Keyboard & selection.** Clicking or activating (Enter/Space) selects
189
+ * the item, truncating the path to this depth. ArrowRight on a branch opens
190
+ * and steps into the child column; ArrowLeft returns focus to the parent
191
+ * item. The item participates in the tree-wide single roving Tab stop.
192
+ *
193
+ * **Disabled.** Pass `disabled` to render `aria-disabled="true"` /
194
+ * `data-disabled`; the item is skipped by roving navigation and cannot be
195
+ * selected, focused, or activated.
141
196
  *
142
197
  * **`asChild` prop.** Pass `asChild` to render the cell as a
143
198
  * consumer-supplied element (e.g. an `<a>`) instead of the default
144
199
  * `<div>`. All treeitem ARIA attributes, event handlers, and the internal
145
- * ref are merged onto that child. A nested `<MillerColumns.Column>` is
146
- * still declared as a sibling of the cell element.
200
+ * ref are merged onto that child via the {@link Slot} pattern. A nested
201
+ * `<MillerColumns.Column>` is still declared as a sibling of the cell
202
+ * element.
147
203
  *
148
204
  * **Ref forwarding.** A `ref` prop (React 19 ref-as-prop style) is
149
205
  * forwarded to the rendered element and composed with the library's
150
206
  * internal ref.
151
207
  *
208
+ * @extends HTMLDivElement
209
+ *
152
210
  * @example Leaf
153
211
  * ```tsx
154
212
  * <MillerColumns.Item value="guides">Guides</MillerColumns.Item>
@@ -204,6 +262,8 @@ MillerColumnsItem.displayName = "MillerColumnsItem";
204
262
  * - `data-has-children` — always present (the indicator only renders
205
263
  * for branch items).
206
264
  *
265
+ * @extends HTMLSpanElement
266
+ *
207
267
  * @example
208
268
  * ```tsx
209
269
  * <MillerColumns.Item value="docs">
@@ -243,10 +303,13 @@ MillerColumnsItemIndicator.displayName = "MillerColumnsItemIndicator";
243
303
  * `<div role="separator" aria-orientation="vertical">` that, while
244
304
  * pointer-dragged, drives that column's width as state on the `Root`.
245
305
  *
246
- * Must be rendered among a `MillerColumns.Column`'s children — it reads
247
- * the column's depth from context. Position it with CSS (typically
306
+ * Must be rendered among a {@link MillerColumnsColumn | `MillerColumns.Column`}'s
307
+ * children — it reads the column's depth from
308
+ * {@link MillerColumnsColumnContext}. Position it with CSS (typically
248
309
  * absolutely, against the column's trailing edge).
249
310
  *
311
+ * @extends HTMLDivElement
312
+ *
250
313
  * @example
251
314
  * ```tsx
252
315
  * <MillerColumns.Column>
@@ -277,7 +340,11 @@ MillerColumnsResizeHandle.displayName = "MillerColumnsResizeHandle";
277
340
  *
278
341
  * Pair it with {@link useMillerColumnsSelection} to render content for
279
342
  * the current selection. Author it as the **last child** of `Root`, a
280
- * sibling of the root `Column`.
343
+ * sibling of the root `Column`. It subscribes to
344
+ * {@link MillerColumnsContext} (so it must live inside a `Root`) but does
345
+ * not portal — it stays in document flow after the projected columns.
346
+ *
347
+ * @extends HTMLDivElement
281
348
  *
282
349
  * @example
283
350
  * ```tsx
@@ -1,76 +1,132 @@
1
1
  import { ComponentProps, 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
+ /**
4
+ * Shared base for both {@link MillerColumnsRootProps} variants — the native
5
+ * `<div>` attributes (minus `ref`, which the strip owns internally).
6
+ */
7
+ export type MillerColumnsRootBaseProps = Omit<
8
+ ComponentProps<"div">,
9
+ "ref" | "defaultValue"
10
+ > & {
11
+ /**
12
+ * The strip's tree, authored recursively: root
13
+ * {@link MillerColumnsColumnProps | Column(s)} whose branch
14
+ * {@link MillerColumnsItemProps | Items} nest further Columns, plus an
15
+ * optional trailing {@link MillerColumnsPreviewPanelProps | PreviewPanel}.
16
+ * There is no `items` data prop — the JSX *is* the data.
17
+ */
6
18
  children: ReactNode;
7
19
  };
8
20
 
9
21
  /**
10
- * Props for `MillerColumns.Root` in uncontrolled mode — the component owns the
11
- * selection path. Pass `defaultValue` to set the initial path.
22
+ * Uncontrolled variant of {@link MillerColumnsRootProps}: the component owns
23
+ * the selection path. Pass `defaultValue` (or omit it to start empty).
12
24
  */
13
25
  export type MillerColumnsRootUncontrolledProps = MillerColumnsRootBaseProps & {
14
- /** Initial selection path when uncontrolled. */
26
+ /** Initial selection path on first render, root column first (e.g.
27
+ * `["docs", "guides"]`). The component owns the path thereafter. */
15
28
  defaultValue?: string[];
16
- /** Forbidden in uncontrolled mode. */
29
+ /** Forbidden in uncontrolled mode — use `defaultValue` instead. */
17
30
  value?: never;
18
- /** Forbidden in uncontrolled mode. */
31
+ /** Forbidden in uncontrolled mode — pass `value` + `onValueChange` to
32
+ * control the component. */
19
33
  onValueChange?: never;
20
34
  };
21
35
 
22
36
  /**
23
- * Props for `MillerColumns.Root` in controlled mode — the parent owns the
37
+ * Controlled variant of {@link MillerColumnsRootProps}: the parent owns the
24
38
  * selection path. Pass `value` and `onValueChange` together.
25
39
  */
26
40
  export type MillerColumnsRootControlledProps = MillerColumnsRootBaseProps & {
27
- /** Forbidden in controlled mode. */
41
+ /** Forbidden in controlled mode — use `value` instead. */
28
42
  defaultValue?: never;
29
- /** The controlled selection path, root column first. */
43
+ /** The controlled selection path, root column first. Must be kept in sync
44
+ * by the parent via `onValueChange`. */
30
45
  value: string[];
31
- /** Called with the new path whenever the selection changes. */
46
+ /** Called with the new selection path whenever the selection changes.
47
+ * Required in controlled mode. */
32
48
  onValueChange: (path: string[]) => void;
33
49
  };
34
50
 
35
- /** Props for `MillerColumns.Root` — discriminated controlled/uncontrolled union. */
51
+ /**
52
+ * Props for {@link MillerColumnsRoot | `MillerColumns.Root`}.
53
+ *
54
+ * Resolves to either {@link MillerColumnsRootUncontrolledProps} or
55
+ * {@link MillerColumnsRootControlledProps} — only one shape is accepted by
56
+ * TypeScript at a time.
57
+ */
36
58
  export type MillerColumnsRootProps =
37
59
  | MillerColumnsRootUncontrolledProps
38
60
  | MillerColumnsRootControlledProps;
39
61
 
40
- /** Props for `MillerColumns.Column`, a single vertical list within the strip. */
62
+ /**
63
+ * Props for {@link MillerColumnsColumn | `MillerColumns.Column`}, a single
64
+ * vertical list. Rendered element is portal-projected into the Root strip.
65
+ */
41
66
  export type MillerColumnsColumnProps = ComponentProps<"div"> & {
42
- /** The items (and optional resize handle) of this column. */
67
+ /** The column's {@link MillerColumnsItemProps | Items} and, optionally, a
68
+ * {@link MillerColumnsResizeHandleProps | ResizeHandle}. */
43
69
  children: ReactNode;
44
70
  };
45
71
 
46
- /** Props for `MillerColumns.Item`, a single selectable node within a column. */
72
+ /**
73
+ * Props for {@link MillerColumnsItem | `MillerColumns.Item`}, a single
74
+ * selectable node within a column.
75
+ *
76
+ * Generic over the rendered element type so `asChild` consumers can type
77
+ * the forwarded `ref` (e.g. `MillerColumns.Item<HTMLAnchorElement>`).
78
+ */
47
79
  export type MillerColumnsItemProps<T extends HTMLElement = HTMLDivElement> =
48
80
  Omit<ComponentProps<"div">, "ref"> & {
49
- /** Stable identifier used to match this item against the selection path. */
81
+ /** Stable identifier used to match this item against the selection path.
82
+ * The item is selected when the path entry at its depth equals `value`. */
50
83
  value: string;
51
- /** When `true`, the item cannot be selected or focused. */
84
+ /**
85
+ * When `true`, the item renders `aria-disabled` / `data-disabled` and is
86
+ * skipped by roving navigation — it cannot be selected, focused, or
87
+ * activated.
88
+ * @default false
89
+ */
52
90
  disabled?: boolean;
53
- /** Render the child element instead of the default `<div>`. */
91
+ /**
92
+ * Render the cell as a single consumer-supplied child element (e.g. an
93
+ * `<a>`) instead of the default `<div>`, merging the treeitem ARIA,
94
+ * handlers, and ref onto it via the {@link Slot} pattern. Any nested
95
+ * `<MillerColumns.Column>` remains a sibling of the cell element.
96
+ * @default false
97
+ */
54
98
  asChild?: boolean;
55
- /** Cell content and, for a branch, a nested `<MillerColumns.Column>`. */
99
+ /** The cell content and, for a branch item, a single nested
100
+ * `<MillerColumns.Column>` (split out by `partitionItemChildren`). */
56
101
  children: ReactNode;
57
- /** Ref to the rendered element. Defaults to `HTMLDivElement`; when using
58
- * `asChild`, specify the child's element type. */
102
+ /** Forwarded to the rendered element. Defaults to `HTMLDivElement`; when
103
+ * using `asChild`, specify the child's element type. Composed with the
104
+ * library's internal ref. */
59
105
  ref?: Ref<T>;
60
106
  };
61
107
 
62
- /** Props for `MillerColumns.ItemIndicator`, the branch chevron affordance. */
108
+ /**
109
+ * Props for {@link MillerColumnsItemIndicator | `MillerColumns.ItemIndicator`},
110
+ * the branch chevron affordance (renders nothing for leaf items).
111
+ */
63
112
  export type MillerColumnsItemIndicatorProps = ComponentProps<"span"> & {
64
- /** The glyph to render (defaults to none). */
113
+ /** The glyph to render (e.g. `▸`). Optional. */
65
114
  children?: ReactNode;
66
115
  };
67
116
 
68
- /** Props for `MillerColumns.ResizeHandle`, the drag-to-resize separator. */
117
+ /**
118
+ * Props for {@link MillerColumnsResizeHandle | `MillerColumns.ResizeHandle`},
119
+ * the drag-to-resize separator for its containing column.
120
+ */
69
121
  export type MillerColumnsResizeHandleProps = ComponentProps<"div">;
70
122
 
71
- /** Props for `MillerColumns.PreviewPanel`, the trailing preview pane. */
123
+ /**
124
+ * Props for {@link MillerColumnsPreviewPanel | `MillerColumns.PreviewPanel`},
125
+ * the trailing preview pane.
126
+ */
72
127
  export type MillerColumnsPreviewPanelProps = ComponentProps<"div"> & {
73
- /** The preview content for the current selection. */
128
+ /** The preview content for the current selection (typically driven by
129
+ * {@link useMillerColumnsSelection}). */
74
130
  children?: ReactNode;
75
131
  };
76
132
 
@@ -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>
@@ -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
@@ -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
 
@@ -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