@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
@@ -0,0 +1,71 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Popover } from "../Popover";
5
+
6
+ describe("Popover keyboard interaction", () => {
7
+ it("closes the popover when Escape is pressed", async () => {
8
+ // Arrange
9
+ const user = userEvent.setup();
10
+ render(
11
+ <Popover.Root defaultOpen>
12
+ <Popover.Trigger>Open</Popover.Trigger>
13
+ <Popover.Content data-testid="content">
14
+ <button type="button">Inside</button>
15
+ </Popover.Content>
16
+ </Popover.Root>,
17
+ );
18
+ const trigger = screen.getByRole("button", { name: "Open" });
19
+ const content = screen.getByTestId("content");
20
+
21
+ // Act
22
+ await user.click(screen.getByRole("button", { name: "Inside", hidden: true }));
23
+ await user.keyboard("{Escape}");
24
+
25
+ // Assert
26
+ expect(content).toHaveAttribute("data-state", "closed");
27
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
28
+ });
29
+
30
+ it("returns focus to the trigger after Escape", async () => {
31
+ // Arrange
32
+ const user = userEvent.setup();
33
+ render(
34
+ <Popover.Root defaultOpen>
35
+ <Popover.Trigger>Open</Popover.Trigger>
36
+ <Popover.Content>
37
+ <button type="button">Inside</button>
38
+ </Popover.Content>
39
+ </Popover.Root>,
40
+ );
41
+ const trigger = screen.getByRole("button", { name: "Open" });
42
+
43
+ // Act
44
+ await user.click(screen.getByRole("button", { name: "Inside", hidden: true }));
45
+ await user.keyboard("{Escape}");
46
+
47
+ // Assert
48
+ expect(trigger).toHaveFocus();
49
+ });
50
+
51
+ it("still fires a consumer onKeyDown on the content", async () => {
52
+ // Arrange
53
+ const user = userEvent.setup();
54
+ const onKeyDown = vi.fn();
55
+ render(
56
+ <Popover.Root defaultOpen>
57
+ <Popover.Trigger>Open</Popover.Trigger>
58
+ <Popover.Content onKeyDown={onKeyDown}>
59
+ <button type="button">Inside</button>
60
+ </Popover.Content>
61
+ </Popover.Root>,
62
+ );
63
+
64
+ // Act
65
+ await user.click(screen.getByRole("button", { name: "Inside", hidden: true }));
66
+ await user.keyboard("a");
67
+
68
+ // Assert
69
+ expect(onKeyDown).toHaveBeenCalled();
70
+ });
71
+ });
@@ -0,0 +1,69 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Popover } from "../Popover";
5
+
6
+ describe("Popover mouse interaction", () => {
7
+ it("opens the popover when the trigger is clicked", async () => {
8
+ // Arrange
9
+ const user = userEvent.setup();
10
+ render(
11
+ <Popover.Root>
12
+ <Popover.Trigger>Open</Popover.Trigger>
13
+ <Popover.Content data-testid="content">Content</Popover.Content>
14
+ </Popover.Root>,
15
+ );
16
+ const trigger = screen.getByRole("button", { name: "Open" });
17
+ const content = screen.getByTestId("content");
18
+
19
+ // Act
20
+ await user.click(trigger);
21
+
22
+ // Assert
23
+ expect(trigger).toHaveAttribute("aria-expanded", "true");
24
+ expect(content).toHaveAttribute("data-state", "open");
25
+ expect(content).toHaveAttribute("data-popover-open");
26
+ });
27
+
28
+ it("closes the popover when the trigger is clicked again", async () => {
29
+ // Arrange
30
+ const user = userEvent.setup();
31
+ render(
32
+ <Popover.Root>
33
+ <Popover.Trigger>Open</Popover.Trigger>
34
+ <Popover.Content data-testid="content">Content</Popover.Content>
35
+ </Popover.Root>,
36
+ );
37
+ const trigger = screen.getByRole("button", { name: "Open" });
38
+ const content = screen.getByTestId("content");
39
+
40
+ // Act
41
+ await user.click(trigger);
42
+ await user.click(trigger);
43
+
44
+ // Assert
45
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
46
+ expect(content).toHaveAttribute("data-state", "closed");
47
+ expect(content).not.toHaveAttribute("data-popover-open");
48
+ });
49
+
50
+ it("composes a consumer onClick on the trigger with the toggle behaviour", async () => {
51
+ // Arrange
52
+ const user = userEvent.setup();
53
+ const onClick = vi.fn();
54
+ render(
55
+ <Popover.Root>
56
+ <Popover.Trigger onClick={onClick}>Open</Popover.Trigger>
57
+ <Popover.Content>Content</Popover.Content>
58
+ </Popover.Root>,
59
+ );
60
+ const trigger = screen.getByRole("button", { name: "Open" });
61
+
62
+ // Act
63
+ await user.click(trigger);
64
+
65
+ // Assert
66
+ expect(onClick).toHaveBeenCalledTimes(1);
67
+ expect(trigger).toHaveAttribute("aria-expanded", "true");
68
+ });
69
+ });
@@ -0,0 +1,44 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Popover } from "../Popover";
5
+
6
+ describe("Popover uncontrolled state", () => {
7
+ it("starts open when defaultOpen is set", () => {
8
+ // Arrange & Act
9
+ render(
10
+ <Popover.Root defaultOpen>
11
+ <Popover.Trigger>Open</Popover.Trigger>
12
+ <Popover.Content data-testid="content">Content</Popover.Content>
13
+ </Popover.Root>,
14
+ );
15
+ const trigger = screen.getByRole("button", { name: "Open" });
16
+ const content = screen.getByTestId("content");
17
+
18
+ // Assert
19
+ expect(trigger).toHaveAttribute("aria-expanded", "true");
20
+ expect(content).toHaveAttribute("data-state", "open");
21
+ expect(content).toHaveAttribute("data-popover-open");
22
+ });
23
+
24
+ it("notifies onOpenChange as the internal state changes", async () => {
25
+ // Arrange
26
+ const user = userEvent.setup();
27
+ const onOpenChange = vi.fn();
28
+ render(
29
+ <Popover.Root onOpenChange={onOpenChange}>
30
+ <Popover.Trigger>Open</Popover.Trigger>
31
+ <Popover.Content>Content</Popover.Content>
32
+ </Popover.Root>,
33
+ );
34
+ const trigger = screen.getByRole("button", { name: "Open" });
35
+
36
+ // Act
37
+ await user.click(trigger);
38
+ await user.click(trigger);
39
+
40
+ // Assert
41
+ expect(onOpenChange).toHaveBeenNthCalledWith(1, true);
42
+ expect(onOpenChange).toHaveBeenNthCalledWith(2, false);
43
+ });
44
+ });
@@ -0,0 +1,3 @@
1
+ export { usePopoverRoot } from "./usePopoverRoot";
2
+ export { usePopoverTrigger } from "./usePopoverTrigger";
3
+ export { usePopoverContent } from "./usePopoverContent";
@@ -0,0 +1,98 @@
1
+ import {
2
+ KeyboardEvent,
3
+ KeyboardEventHandler,
4
+ Ref,
5
+ useEffect,
6
+ useRef,
7
+ } from "react";
8
+
9
+ import { composeEventHandlers, composeRefs } from "../../Slot/index.ts";
10
+ import { FOCUSABLE_SELECTOR } from "../../utils/index.ts";
11
+ import { usePopoverContext } from "../PopoverContext";
12
+
13
+ type UsePopoverContentArgs = {
14
+ onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
15
+ ref?: Ref<HTMLDivElement>;
16
+ };
17
+
18
+ export function usePopoverContent({
19
+ onKeyDown,
20
+ ref,
21
+ }: UsePopoverContentArgs = {}) {
22
+ const { open, setOpen, contentId, triggerRef, titleId, descriptionId } =
23
+ usePopoverContext();
24
+ const contentRef = useRef<HTMLDivElement | null>(null);
25
+ const composedRef = ref ? composeRefs(contentRef, ref) : contentRef;
26
+
27
+ useEffect(() => {
28
+ const content = contentRef.current!;
29
+ if (open) {
30
+ content.showPopover();
31
+ // Move focus into the popover (WAI-ARIA non-modal dialog): the first
32
+ // focusable descendant, or the content itself (it carries tabIndex=-1).
33
+ const focusable = content.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
34
+ (focusable ?? content).focus();
35
+ } else {
36
+ try {
37
+ content.hidePopover();
38
+ } catch {
39
+ // already hidden — no-op (e.g. browser already light-dismissed it)
40
+ }
41
+ }
42
+ }, [open]);
43
+
44
+ // Sync React state when the browser closes the popover without our code
45
+ // doing it (real-browser ToggleEvent on light-dismiss or native Escape).
46
+ useEffect(() => {
47
+ const content = contentRef.current!;
48
+ const handleToggle = (event: Event) => {
49
+ if ((event as ToggleEvent).newState === "closed") setOpen(false);
50
+ };
51
+ content.addEventListener("toggle", handleToggle);
52
+ return () => content.removeEventListener("toggle", handleToggle);
53
+ }, [setOpen]);
54
+
55
+ // Close on clicks outside the popover. This is only a fallback for
56
+ // environments WITHOUT the native Popover API (e.g. jsdom): a real browser
57
+ // already light-dismisses an `auto` popover on an outside click and fires the
58
+ // `toggle` event we sync above. Running this listener in a real browser is
59
+ // not just redundant — React 19 can flush this effect synchronously inside
60
+ // the very click that opened the popover from an external control, so the
61
+ // listener would catch that click and immediately re-close it. Gate it to
62
+ // non-native environments; `"popover" in HTMLElement.prototype` is true only
63
+ // where the native API (and its own light-dismiss) exists.
64
+ useEffect(() => {
65
+ if (!open) return;
66
+ if ("popover" in HTMLElement.prototype) return;
67
+ const handleClick = (event: MouseEvent) => {
68
+ const target = event.target as Element;
69
+ if (triggerRef.current?.contains(target)) return;
70
+ if (target.closest?.("[popover]")) return;
71
+ setOpen(false);
72
+ };
73
+ document.addEventListener("click", handleClick);
74
+ return () => document.removeEventListener("click", handleClick);
75
+ }, [open, setOpen, triggerRef]);
76
+
77
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
78
+ if (event.key === "Escape") {
79
+ event.preventDefault();
80
+ setOpen(false);
81
+ triggerRef.current?.focus();
82
+ }
83
+ };
84
+
85
+ const contentProps = {
86
+ ref: composedRef,
87
+ id: contentId,
88
+ role: "dialog" as const,
89
+ popover: "auto" as const,
90
+ tabIndex: -1,
91
+ "aria-labelledby": titleId,
92
+ "aria-describedby": descriptionId,
93
+ "data-state": (open ? "open" : "closed") as "open" | "closed",
94
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
95
+ };
96
+
97
+ return { contentProps };
98
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+
10
+ import { useControllableState } from "../../hooks/index.ts";
11
+ import { PopoverContextValue } from "../types";
12
+
13
+ type UsePopoverRootArgs = {
14
+ defaultOpen?: boolean;
15
+ open?: boolean;
16
+ onOpenChange?: (open: boolean) => void;
17
+ };
18
+
19
+ export function usePopoverRoot({
20
+ defaultOpen = false,
21
+ open: controlledOpen,
22
+ onOpenChange,
23
+ }: UsePopoverRootArgs) {
24
+ const contentId = useId();
25
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
26
+ const [open, setOpenBase] = useControllableState<boolean>(
27
+ controlledOpen,
28
+ defaultOpen,
29
+ onOpenChange,
30
+ );
31
+
32
+ // Mirror `open` so setOpen can short-circuit repeat transitions within a
33
+ // single event without a re-render in between. Without this, internal
34
+ // paths that converge on the same close (e.g. a light-dismiss firing both
35
+ // a `toggle` event and a document click) would double-notify consumers.
36
+ const openRef = useRef(open);
37
+ useEffect(() => {
38
+ openRef.current = open;
39
+ });
40
+
41
+ const setOpen = useCallback(
42
+ (next: boolean) => {
43
+ if (openRef.current === next) return;
44
+ openRef.current = next;
45
+ setOpenBase(next);
46
+ },
47
+ [setOpenBase],
48
+ );
49
+
50
+ const [titleId, setTitleId] = useState<string | undefined>(undefined);
51
+ const [descriptionId, setDescriptionId] = useState<string | undefined>(
52
+ undefined,
53
+ );
54
+ const registerTitle = useCallback((id: string | undefined) => {
55
+ setTitleId(id);
56
+ }, []);
57
+ const registerDescription = useCallback((id: string | undefined) => {
58
+ setDescriptionId(id);
59
+ }, []);
60
+
61
+ const contextValue = useMemo<PopoverContextValue>(
62
+ () => ({
63
+ open,
64
+ setOpen,
65
+ triggerRef,
66
+ contentId,
67
+ titleId,
68
+ descriptionId,
69
+ registerTitle,
70
+ registerDescription,
71
+ }),
72
+ [
73
+ open,
74
+ setOpen,
75
+ contentId,
76
+ titleId,
77
+ descriptionId,
78
+ registerTitle,
79
+ registerDescription,
80
+ ],
81
+ );
82
+
83
+ return { contextValue };
84
+ }
@@ -0,0 +1,33 @@
1
+ import { MouseEventHandler, Ref } from "react";
2
+
3
+ import { composeEventHandlers, composeRefs } from "../../Slot/index.ts";
4
+ import { PopoverTriggerProps } from "../types";
5
+ import { usePopoverContext } from "../PopoverContext";
6
+
7
+ export function usePopoverTrigger({
8
+ ref,
9
+ onClick,
10
+ asChild = false,
11
+ ...rest
12
+ }: Omit<PopoverTriggerProps, "children">) {
13
+ const { open, setOpen, triggerRef, contentId } = usePopoverContext();
14
+ const composedRef = ref
15
+ ? composeRefs(triggerRef, ref as Ref<HTMLButtonElement>)
16
+ : triggerRef;
17
+
18
+ const toggle = () => setOpen(!open);
19
+
20
+ const triggerProps = {
21
+ ref: composedRef,
22
+ "aria-haspopup": "dialog" as const,
23
+ "aria-expanded": open,
24
+ "aria-controls": contentId,
25
+ onClick: composeEventHandlers(
26
+ onClick as MouseEventHandler | undefined,
27
+ toggle,
28
+ ),
29
+ ...rest,
30
+ };
31
+
32
+ return { triggerProps };
33
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./Popover";
2
+ export * from "./types";
@@ -0,0 +1,197 @@
1
+ import { ComponentProps, ReactNode, Ref, RefObject } from "react";
2
+
3
+ /** Props common to both controlled and uncontrolled `Popover.Root` modes. */
4
+ export type PopoverRootBaseProps = {
5
+ /** The Popover sub-components — typically a {@link PopoverTriggerProps | `Popover.Trigger`}
6
+ * and a {@link PopoverContentProps | `Popover.Content`}, optionally a
7
+ * {@link PopoverAnchorProps | `Popover.Anchor`}. */
8
+ children?: ReactNode;
9
+ };
10
+
11
+ /**
12
+ * Props for {@link PopoverRoot | `Popover.Root`} in uncontrolled mode — the
13
+ * component owns the open state. Pass `defaultOpen` to set the initial value;
14
+ * `open` is forbidden.
15
+ */
16
+ export type PopoverRootUncontrolledProps = PopoverRootBaseProps & {
17
+ /**
18
+ * Whether the popover is open on first render. The component owns the flag
19
+ * from then on (trigger toggles, `Popover.Close`, Escape, and light-dismiss
20
+ * all mutate it internally).
21
+ * @default false
22
+ */
23
+ defaultOpen?: boolean;
24
+ /** Forbidden in uncontrolled mode — use `defaultOpen` instead. */
25
+ open?: never;
26
+ /** Called with the new open state after every transition. Optional in
27
+ * uncontrolled mode; use it to observe (not drive) open/close. */
28
+ onOpenChange?: (open: boolean) => void;
29
+ };
30
+
31
+ /**
32
+ * Props for {@link PopoverRoot | `Popover.Root`} in controlled mode — the
33
+ * parent owns the open value. Pass `open` and `onOpenChange` together;
34
+ * `defaultOpen` is forbidden.
35
+ */
36
+ export type PopoverRootControlledProps = PopoverRootBaseProps & {
37
+ /** The current open state. Must be kept in sync by the parent via
38
+ * `onOpenChange`. */
39
+ open: boolean;
40
+ /** Called with the requested open state whenever the popover wants to open
41
+ * or close (trigger click, `Popover.Close`, Escape, outside dismiss).
42
+ * Required in controlled mode — the popover will not move unless the parent
43
+ * updates `open`. */
44
+ onOpenChange: (open: boolean) => void;
45
+ /** Forbidden in controlled mode — use `open` instead. */
46
+ defaultOpen?: never;
47
+ };
48
+
49
+ /**
50
+ * Props for {@link PopoverRoot | `Popover.Root`} — a discriminated union that
51
+ * resolves to either {@link PopoverRootUncontrolledProps} or
52
+ * {@link PopoverRootControlledProps}, so TypeScript accepts exactly one mode.
53
+ */
54
+ export type PopoverRootProps =
55
+ | PopoverRootUncontrolledProps
56
+ | PopoverRootControlledProps;
57
+
58
+ /**
59
+ * Props for {@link PopoverTrigger | `Popover.Trigger`}, the button that toggles
60
+ * the popover. Extends the native `<button>` attributes (with `ref` re-typed to
61
+ * the generic element `T`); the trigger's `aria-haspopup` / `aria-expanded` /
62
+ * `aria-controls` and toggle `onClick` are added by the component (a consumer
63
+ * `onClick` composes and can `preventDefault()` to veto the toggle).
64
+ */
65
+ export type PopoverTriggerProps<T extends HTMLElement = HTMLButtonElement> =
66
+ Omit<ComponentProps<"button">, "ref"> & {
67
+ /** Trigger content (label, icon, etc.). */
68
+ children?: ReactNode;
69
+ /**
70
+ * Render the single child element instead of the default `<button>`,
71
+ * merging the trigger's ARIA, composed event handlers, and ref onto it via
72
+ * the {@link Slot} pattern.
73
+ * @default false
74
+ */
75
+ asChild?: boolean;
76
+ /** Ref to the rendered element, composed with Root's internal trigger ref
77
+ * (used for focus return on close). Defaults to `HTMLButtonElement`; with
78
+ * `asChild`, set the generic to the child's element type
79
+ * (e.g. `HTMLAnchorElement`). */
80
+ ref?: Ref<T>;
81
+ };
82
+
83
+ /**
84
+ * Props for {@link PopoverContent | `Popover.Content`}, the floating panel.
85
+ * Extends the native `<div>` attributes; the component adds `role="dialog"`,
86
+ * `popover="auto"`, `tabIndex={-1}`, the `aria-labelledby` / `aria-describedby`
87
+ * wiring (from {@link PopoverTitleProps | `Popover.Title`} /
88
+ * {@link PopoverDescriptionProps | `Popover.Description`}), and the
89
+ * `data-state` hook. Open/close, focus movement into the panel, Escape, and
90
+ * light-dismiss are all handled internally — no props needed.
91
+ */
92
+ export type PopoverContentProps = ComponentProps<"div"> & {
93
+ /** Panel content — typically {@link PopoverTitleProps | `Popover.Title`},
94
+ * body markup, and a {@link PopoverCloseProps | `Popover.Close`}. */
95
+ children?: ReactNode;
96
+ /**
97
+ * Render the single child element (e.g. a `<section>` or motion wrapper)
98
+ * instead of the default `<div>`, merging the dialog props and internal ref
99
+ * onto it via the {@link Slot} pattern.
100
+ * @default false
101
+ */
102
+ asChild?: boolean;
103
+ /** Ref to the rendered element, composed with Root's internal content ref so
104
+ * open/close and focus management keep working. */
105
+ ref?: Ref<HTMLDivElement>;
106
+ };
107
+
108
+ /**
109
+ * Props for {@link PopoverAnchor | `Popover.Anchor`}, an optional element the
110
+ * popover is positioned against instead of the trigger. Attach a CSS
111
+ * `anchor-name` to it and a matching `position-anchor` to
112
+ * {@link PopoverContentProps | `Popover.Content`}. Renders no behaviour of its
113
+ * own — positioning is purely a CSS concern.
114
+ */
115
+ export type PopoverAnchorProps = ComponentProps<"div"> & {
116
+ /** Anchor content — the element(s) the popover should be positioned against. */
117
+ children?: ReactNode;
118
+ /**
119
+ * Render the single child element instead of the default `<div>`, projecting
120
+ * the anchor onto an existing element via the {@link Slot} pattern.
121
+ * @default false
122
+ */
123
+ asChild?: boolean;
124
+ };
125
+
126
+ /**
127
+ * Props for {@link PopoverClose | `Popover.Close`}, the button that closes the
128
+ * popover and returns focus to the trigger. Extends the native `<button>`
129
+ * attributes; a consumer `onClick` composes with the close (runs first, can
130
+ * `preventDefault()` to veto closing).
131
+ */
132
+ export type PopoverCloseProps = ComponentProps<"button"> & {
133
+ /** Close-button content (label, icon, etc.). */
134
+ children?: ReactNode;
135
+ /**
136
+ * Render the single child element (e.g. a text link or icon button) instead
137
+ * of the default `<button>`, merging the close behaviour onto it via the
138
+ * {@link Slot} pattern.
139
+ * @default false
140
+ */
141
+ asChild?: boolean;
142
+ };
143
+
144
+ /**
145
+ * Props for {@link PopoverTitle | `Popover.Title`}, the popover's accessible
146
+ * name. Extends the native `<h2>` attributes; the component supplies a
147
+ * generated `id` and registers it so `Popover.Content` exposes it via
148
+ * `aria-labelledby`.
149
+ */
150
+ export type PopoverTitleProps = ComponentProps<"h2"> & {
151
+ /** Title content, rendered as the popover's accessible name. */
152
+ children?: ReactNode;
153
+ /**
154
+ * Render the single child element (e.g. an `<h3>`) instead of the default
155
+ * `<h2>` via the {@link Slot} pattern; the generated `id` is still applied
156
+ * and registered.
157
+ * @default false
158
+ */
159
+ asChild?: boolean;
160
+ };
161
+
162
+ /**
163
+ * Props for {@link PopoverDescription | `Popover.Description`}, the popover's
164
+ * accessible description. Extends the native `<p>` attributes; the component
165
+ * supplies a generated `id` and registers it so `Popover.Content` exposes it
166
+ * via `aria-describedby`.
167
+ */
168
+ export type PopoverDescriptionProps = ComponentProps<"p"> & {
169
+ /** Description content, exposed as the popover's accessible description. */
170
+ children?: ReactNode;
171
+ /**
172
+ * Render the single child element instead of the default `<p>` via the
173
+ * {@link Slot} pattern; the generated `id` is still applied and registered.
174
+ * @default false
175
+ */
176
+ asChild?: boolean;
177
+ };
178
+
179
+ /** Value shared through Popover context to all sub-components. */
180
+ export type PopoverContextValue = {
181
+ /** Whether the popover is currently open. */
182
+ open: boolean;
183
+ /** Request an open-state change. */
184
+ setOpen: (open: boolean) => void;
185
+ /** Ref to the trigger element, used for focus return. */
186
+ triggerRef: RefObject<HTMLButtonElement | null>;
187
+ /** Generated id of the content panel. */
188
+ contentId: string;
189
+ /** Id of the rendered `Popover.Title`, or `undefined` when absent. */
190
+ titleId: string | undefined;
191
+ /** Id of the rendered `Popover.Description`, or `undefined` when absent. */
192
+ descriptionId: string | undefined;
193
+ /** Register (or clear) the title id for `aria-labelledby`. */
194
+ registerTitle: (id: string | undefined) => void;
195
+ /** Register (or clear) the description id for `aria-describedby`. */
196
+ registerDescription: (id: string | undefined) => void;
197
+ };
@@ -4,21 +4,61 @@ import { createPortal } from "react-dom";
4
4
  import type { PortalProps } from "./types";
5
5
 
6
6
  /**
7
- * Renders children outside the current React subtree by mounting them into a
8
- * DOM node via `createPortal`. Defaults to `document.body`; pass `container`
9
- * to target a different element.
7
+ * Teleports its children into a DOM node outside the current React subtree
8
+ * using React's `createPortal`. Renders no host element of its own — the
9
+ * children appear directly inside the target `container`.
10
+ *
11
+ * Use `Portal` whenever a subtree must escape its ancestor's stacking context,
12
+ * overflow clipping, or z-index hierarchy — tooltips, modals, dropdowns, and
13
+ * similar overlays are the typical cases. `Portal` carries no state, context,
14
+ * or open/close awareness; those concerns belong in the composing component
15
+ * (see `Modal.Portal`, which layers open-state suppression and `forceMount` on
16
+ * top of this primitive).
17
+ *
18
+ * **Conditional rendering.** `Portal` always mounts when rendered. To control
19
+ * visibility, conditionally render the `Portal` itself, or manage show/hide
20
+ * logic in the composing component:
21
+ *
22
+ * ```tsx
23
+ * {isOpen && (
24
+ * <Portal>
25
+ * <div role="dialog">…</div>
26
+ * </Portal>
27
+ * )}
28
+ * ```
29
+ *
30
+ * For CSS-driven exit animations, keep the portal mounted and drive styles
31
+ * through a `data-state` attribute on its child:
32
+ *
33
+ * ```tsx
34
+ * <Portal>
35
+ * <div data-state={isOpen ? "open" : "closed"}>…</div>
36
+ * </Portal>
37
+ * ```
38
+ *
39
+ * **Ref forwarding.** `Portal` does not forward a ref (there is no host
40
+ * element to attach one to). Attach `ref` directly to the child element
41
+ * rendered inside the portal.
42
+ *
43
+ * @example Mount into document.body (default)
44
+ * ```tsx
45
+ * import { Portal } from "@primitiv-ui/react";
10
46
  *
11
- * @example
12
- * // Mount into document.body (default)
13
47
  * <Portal>
14
- * <div role="dialog">…</div>
48
+ * <div role="dialog" aria-modal="true">
49
+ * Modal content rendered directly in document.body
50
+ * </div>
15
51
  * </Portal>
52
+ * ```
53
+ *
54
+ * @example Mount into a custom container
55
+ * ```tsx
56
+ * const overlayRoot = document.getElementById("overlay-root");
16
57
  *
17
- * @example
18
- * // Mount into a specific container
19
- * <Portal container={document.getElementById("overlays")}>
20
- * <div role="dialog">…</div>
58
+ * <Portal container={overlayRoot}>
59
+ * <div role="tooltip">Tooltip content</div>
21
60
  * </Portal>
61
+ * ```
22
62
  */
23
63
  function Portal({ children, container }: PortalProps): ReactPortal {
24
64
  return createPortal(children, container ?? document.body);