@primitiv-ui/react 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/README.md +19 -2
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -0,0 +1,427 @@
1
+ import { useEffect, useId } from "react";
2
+ import type { ReactElement, Ref } from "react";
3
+
4
+ import { Slot, composeEventHandlers } from "../Slot/index.ts";
5
+ import { PopoverContext, usePopoverContext } from "./PopoverContext";
6
+ import {
7
+ usePopoverContent,
8
+ usePopoverRoot,
9
+ usePopoverTrigger,
10
+ } from "./hooks/index.ts";
11
+
12
+ import type {
13
+ PopoverAnchorProps,
14
+ PopoverCloseProps,
15
+ PopoverContentProps,
16
+ PopoverDescriptionProps,
17
+ PopoverRootProps,
18
+ PopoverTitleProps,
19
+ PopoverTriggerProps,
20
+ } from "./types";
21
+
22
+ /**
23
+ * The root of a Popover — owns open state, generates the ids that wire the
24
+ * trigger to the content, and provides context to descendants. Renders no DOM
25
+ * of its own.
26
+ *
27
+ * Supports two state modes, statically discriminated at the type level:
28
+ *
29
+ * - **Uncontrolled** — pass {@link PopoverRootUncontrolledProps.defaultOpen | `defaultOpen`}
30
+ * (or omit it for closed-on-mount). The component owns the open flag
31
+ * internally; observe transitions with `onOpenChange`.
32
+ * - **Controlled** — pass {@link PopoverRootControlledProps.open | `open`} *and*
33
+ * {@link PopoverRootControlledProps.onOpenChange | `onOpenChange`} together.
34
+ * The parent owns the flag; the component defers every change back through
35
+ * the callback.
36
+ *
37
+ * @example Uncontrolled
38
+ * ```tsx
39
+ * <Popover.Root>
40
+ * <Popover.Trigger>Open</Popover.Trigger>
41
+ * <Popover.Content>...</Popover.Content>
42
+ * </Popover.Root>
43
+ * ```
44
+ *
45
+ * @example Controlled
46
+ * ```tsx
47
+ * const [open, setOpen] = useState(false);
48
+ *
49
+ * <Popover.Root open={open} onOpenChange={setOpen}>
50
+ * <Popover.Trigger>Open</Popover.Trigger>
51
+ * <Popover.Content>...</Popover.Content>
52
+ * </Popover.Root>
53
+ * ```
54
+ */
55
+ export function PopoverRoot({
56
+ children,
57
+ defaultOpen,
58
+ open,
59
+ onOpenChange,
60
+ }: PopoverRootProps): ReactElement {
61
+ const { contextValue } = usePopoverRoot({ defaultOpen, open, onOpenChange });
62
+
63
+ return (
64
+ <PopoverContext.Provider value={contextValue}>
65
+ {children}
66
+ </PopoverContext.Provider>
67
+ );
68
+ }
69
+
70
+ /** @internal */
71
+ PopoverRoot.displayName = "PopoverRoot";
72
+
73
+ /**
74
+ * A button that toggles the popover. Renders `<button type="button">` with
75
+ * full ARIA wiring:
76
+ *
77
+ * - `aria-haspopup="dialog"`
78
+ * - `aria-expanded` tracks open state
79
+ * - `aria-controls` points at the `Popover.Content` dialog's id
80
+ *
81
+ * Clicking toggles the popover; a consumer `onClick` composes (runs first, can
82
+ * `event.preventDefault()` to veto the toggle).
83
+ *
84
+ * **`asChild` prop.** Pass `asChild` to render any consumer-supplied element
85
+ * (e.g. an icon button of your own) with the trigger's ARIA, composed event
86
+ * handlers, and ref merged in. Supply the child's element type via the generic
87
+ * (e.g. `Popover.Trigger<HTMLAnchorElement>`).
88
+ *
89
+ * @extends HTMLButtonElement
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * <Popover.Trigger>Open</Popover.Trigger>
94
+ *
95
+ * <Popover.Trigger<HTMLAnchorElement> asChild>
96
+ * <a href="#panel">Open</a>
97
+ * </Popover.Trigger>
98
+ * ```
99
+ */
100
+ export function PopoverTrigger<T extends HTMLElement = HTMLButtonElement>({
101
+ children,
102
+ asChild = false,
103
+ ref,
104
+ ...rest
105
+ }: PopoverTriggerProps<T>): ReactElement {
106
+ const { triggerProps } = usePopoverTrigger({
107
+ ref: ref as Ref<HTMLButtonElement>,
108
+ asChild,
109
+ ...rest,
110
+ });
111
+
112
+ if (asChild) {
113
+ return <Slot {...triggerProps}>{children}</Slot>;
114
+ }
115
+
116
+ return (
117
+ <button type="button" {...triggerProps}>
118
+ {children}
119
+ </button>
120
+ );
121
+ }
122
+
123
+ /** @internal */
124
+ PopoverTrigger.displayName = "PopoverTrigger";
125
+
126
+ /**
127
+ * The floating panel, rendered with the native HTML
128
+ * [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
129
+ * (`popover="auto"`) — no portal, no floating-ui. The browser manages the top
130
+ * layer and light-dismiss (outside click / Escape); visual placement is your
131
+ * CSS concern (use CSS anchor positioning against the trigger or
132
+ * {@link PopoverAnchor | `Popover.Anchor`}).
133
+ *
134
+ * Renders a `<div role="dialog">`. On open, focus moves into the panel (first
135
+ * focusable descendant, else the panel itself — it carries `tabIndex={-1}`).
136
+ * `Escape` closes and returns focus to the trigger. `aria-labelledby` /
137
+ * `aria-describedby` are wired automatically when `Popover.Title` /
138
+ * `Popover.Description` are rendered inside.
139
+ *
140
+ * **`asChild` prop.** Pass `asChild` to render a consumer element (e.g. a
141
+ * `<section>` or a motion wrapper) with the dialog props and internal ref
142
+ * merged in. A consumer `ref` composes with the internal ref, so open state
143
+ * keeps working.
144
+ *
145
+ * **Styling hook.** `data-state="open" | "closed"`.
146
+ *
147
+ * @extends HTMLDivElement
148
+ *
149
+ * @example
150
+ * ```tsx
151
+ * <Popover.Content className="panel">
152
+ * <Popover.Title>Filters</Popover.Title>
153
+ * <Popover.Close>Done</Popover.Close>
154
+ * </Popover.Content>
155
+ * ```
156
+ */
157
+ export function PopoverContent({
158
+ children,
159
+ onKeyDown,
160
+ asChild = false,
161
+ ref,
162
+ ...rest
163
+ }: PopoverContentProps): ReactElement {
164
+ const { contentProps } = usePopoverContent({
165
+ onKeyDown,
166
+ ref: ref as Ref<HTMLDivElement>,
167
+ });
168
+
169
+ if (asChild) {
170
+ return (
171
+ <Slot {...rest} {...contentProps}>
172
+ {children}
173
+ </Slot>
174
+ );
175
+ }
176
+
177
+ return (
178
+ <div {...rest} {...contentProps}>
179
+ {children}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ /** @internal */
185
+ PopoverContent.displayName = "PopoverContent";
186
+
187
+ /**
188
+ * An optional positioning reference. By default the popover is positioned
189
+ * against the trigger; render `Popover.Anchor` around (or in place of) another
190
+ * element to anchor the panel there instead — useful when the trigger is a
191
+ * small icon inside a larger control.
192
+ *
193
+ * Renders a `<div>`. Positioning is CSS-driven: attach a CSS `anchor-name` to
194
+ * this element and a matching `position-anchor` to `Popover.Content`. Pass
195
+ * `asChild` to project the anchor semantics onto an existing element.
196
+ *
197
+ * @extends HTMLDivElement
198
+ *
199
+ * @example
200
+ * ```tsx
201
+ * <Popover.Anchor asChild>
202
+ * <div className="field" style={{ anchorName: "--popover" }}>
203
+ * <input />
204
+ * <Popover.Trigger>▾</Popover.Trigger>
205
+ * </div>
206
+ * </Popover.Anchor>
207
+ * ```
208
+ */
209
+ export function PopoverAnchor({
210
+ children,
211
+ asChild = false,
212
+ ...rest
213
+ }: PopoverAnchorProps): ReactElement {
214
+ // Consume context so an Anchor rendered outside a Root throws, matching the
215
+ // other sub-components. Positioning itself is a CSS (anchor-name) concern.
216
+ usePopoverContext();
217
+
218
+ if (asChild) {
219
+ return <Slot {...rest}>{children}</Slot>;
220
+ }
221
+
222
+ return <div {...rest}>{children}</div>;
223
+ }
224
+
225
+ /** @internal */
226
+ PopoverAnchor.displayName = "PopoverAnchor";
227
+
228
+ /**
229
+ * A button that closes the popover and returns focus to the trigger. Renders a
230
+ * `<button type="button">` whose `onClick` composes with the close — consumer
231
+ * handlers run first and can `event.preventDefault()` to veto closing.
232
+ *
233
+ * Pass `asChild` to render any element (e.g. a text link or your own icon
234
+ * button) with the close behaviour merged in.
235
+ *
236
+ * @extends HTMLButtonElement
237
+ *
238
+ * @example
239
+ * ```tsx
240
+ * <Popover.Close>Done</Popover.Close>
241
+ * ```
242
+ */
243
+ export function PopoverClose({
244
+ children,
245
+ onClick,
246
+ asChild = false,
247
+ ...rest
248
+ }: PopoverCloseProps): ReactElement {
249
+ const { setOpen, triggerRef } = usePopoverContext();
250
+
251
+ const closeProps = {
252
+ ...rest,
253
+ onClick: composeEventHandlers(onClick, () => {
254
+ setOpen(false);
255
+ triggerRef.current?.focus();
256
+ }),
257
+ };
258
+
259
+ if (asChild) {
260
+ return <Slot {...closeProps}>{children}</Slot>;
261
+ }
262
+
263
+ return (
264
+ <button type="button" {...closeProps}>
265
+ {children}
266
+ </button>
267
+ );
268
+ }
269
+
270
+ /** @internal */
271
+ PopoverClose.displayName = "PopoverClose";
272
+
273
+ /**
274
+ * The popover's accessible name. Renders an `<h2>` by default and
275
+ * auto-registers its generated id so `Popover.Content` wires it up as
276
+ * `aria-labelledby`. Pass `asChild` to render your own heading element (e.g.
277
+ * an `<h3>`); the id is still registered.
278
+ *
279
+ * @extends HTMLHeadingElement
280
+ *
281
+ * @example
282
+ * ```tsx
283
+ * <Popover.Title>Filters</Popover.Title>
284
+ * ```
285
+ */
286
+ export function PopoverTitle({
287
+ children,
288
+ asChild = false,
289
+ ...rest
290
+ }: PopoverTitleProps): ReactElement {
291
+ const { registerTitle } = usePopoverContext();
292
+ const id = useId();
293
+
294
+ useEffect(() => {
295
+ registerTitle(id);
296
+ return () => registerTitle(undefined);
297
+ }, [registerTitle, id]);
298
+
299
+ if (asChild) {
300
+ return (
301
+ <Slot id={id} {...rest}>
302
+ {children}
303
+ </Slot>
304
+ );
305
+ }
306
+
307
+ return (
308
+ <h2 id={id} {...rest}>
309
+ {children}
310
+ </h2>
311
+ );
312
+ }
313
+
314
+ /** @internal */
315
+ PopoverTitle.displayName = "PopoverTitle";
316
+
317
+ /**
318
+ * The popover's accessible description. Renders a `<p>` by default and
319
+ * auto-registers its generated id so `Popover.Content` wires it up as
320
+ * `aria-describedby`. Pass `asChild` to render any element; the id is still
321
+ * registered.
322
+ *
323
+ * @extends HTMLParagraphElement
324
+ *
325
+ * @example
326
+ * ```tsx
327
+ * <Popover.Description>Narrow the results below.</Popover.Description>
328
+ * ```
329
+ */
330
+ export function PopoverDescription({
331
+ children,
332
+ asChild = false,
333
+ ...rest
334
+ }: PopoverDescriptionProps): ReactElement {
335
+ const { registerDescription } = usePopoverContext();
336
+ const id = useId();
337
+
338
+ useEffect(() => {
339
+ registerDescription(id);
340
+ return () => registerDescription(undefined);
341
+ }, [registerDescription, id]);
342
+
343
+ if (asChild) {
344
+ return (
345
+ <Slot id={id} {...rest}>
346
+ {children}
347
+ </Slot>
348
+ );
349
+ }
350
+
351
+ return (
352
+ <p id={id} {...rest}>
353
+ {children}
354
+ </p>
355
+ );
356
+ }
357
+
358
+ /** @internal */
359
+ PopoverDescription.displayName = "PopoverDescription";
360
+
361
+ /** Type of the {@link Popover} compound — the Root callable plus its sub-components. */
362
+ type PopoverCompound = typeof PopoverRoot & {
363
+ /** The root, owning open state and context. */
364
+ Root: typeof PopoverRoot;
365
+ /** The button that toggles the popover. */
366
+ Trigger: typeof PopoverTrigger;
367
+ /** An optional positioning reference used instead of the trigger. */
368
+ Anchor: typeof PopoverAnchor;
369
+ /** The floating panel. */
370
+ Content: typeof PopoverContent;
371
+ /** A button that closes the popover. */
372
+ Close: typeof PopoverClose;
373
+ /** The popover's accessible name; auto-wires `aria-labelledby`. */
374
+ Title: typeof PopoverTitle;
375
+ /** The popover's accessible description; auto-wires `aria-describedby`. */
376
+ Description: typeof PopoverDescription;
377
+ };
378
+
379
+ /**
380
+ * Headless, accessible **Popover** — a compound component built on the native
381
+ * HTML Popover API (`popover="auto"`). A non-modal dialog anchored to a
382
+ * trigger: the browser owns the top layer and light-dismiss, focus moves into
383
+ * the panel on open and returns to the trigger on close, and the background
384
+ * stays interactive (no focus trap — use `Modal` for that). Zero styles ship;
385
+ * visual placement is your CSS concern (CSS anchor positioning).
386
+ *
387
+ * `Popover` is both callable (an alias of {@link PopoverRoot | `Popover.Root`})
388
+ * and carries its sub-components as static properties.
389
+ *
390
+ * - {@link PopoverRoot | `Popover.Root`} — state owner and context provider.
391
+ * - {@link PopoverTrigger | `Popover.Trigger`} — `<button>` that toggles the popover.
392
+ * - {@link PopoverAnchor | `Popover.Anchor`} — optional positioning reference.
393
+ * - {@link PopoverContent | `Popover.Content`} — the native-popover `role="dialog"` panel.
394
+ * - {@link PopoverClose | `Popover.Close`} — `<button>` that closes the popover.
395
+ * - {@link PopoverTitle | `Popover.Title`} — accessible name; auto-wires `aria-labelledby`.
396
+ * - {@link PopoverDescription | `Popover.Description`} — auto-wires `aria-describedby`.
397
+ *
398
+ * @example Minimal usage
399
+ * ```tsx
400
+ * import { Popover } from "@primitiv-ui/react";
401
+ *
402
+ * <Popover.Root>
403
+ * <Popover.Trigger>Filters</Popover.Trigger>
404
+ * <Popover.Content>
405
+ * <Popover.Title>Filters</Popover.Title>
406
+ * <Popover.Description>Narrow the results.</Popover.Description>
407
+ * <Popover.Close>Done</Popover.Close>
408
+ * </Popover.Content>
409
+ * </Popover.Root>;
410
+ * ```
411
+ *
412
+ * @see {@link PopoverRoot} for the controlled/uncontrolled state modes.
413
+ * @see {@link PopoverContent} for focus behaviour and ARIA auto-wiring.
414
+ */
415
+ const PopoverCompound: PopoverCompound = Object.assign(PopoverRoot, {
416
+ Root: PopoverRoot,
417
+ Trigger: PopoverTrigger,
418
+ Anchor: PopoverAnchor,
419
+ Content: PopoverContent,
420
+ Close: PopoverClose,
421
+ Title: PopoverTitle,
422
+ Description: PopoverDescription,
423
+ });
424
+
425
+ PopoverCompound.displayName = "Popover";
426
+
427
+ export { PopoverCompound as Popover };
@@ -0,0 +1,13 @@
1
+ import type { Context } from "react";
2
+
3
+ import { createStrictContext } from "../utils/index.ts";
4
+ import { PopoverContextValue } from "./types";
5
+
6
+ const popoverContext = createStrictContext<PopoverContextValue>(
7
+ "Popover sub-components must be rendered inside a <Popover.Root>.",
8
+ "PopoverContext",
9
+ );
10
+
11
+ export const PopoverContext: Context<PopoverContextValue | null> =
12
+ popoverContext[0];
13
+ export const usePopoverContext: () => PopoverContextValue = popoverContext[1];
@@ -0,0 +1,106 @@
1
+ # Popover
2
+
3
+ A headless, accessible **non-modal dialog** anchored to a trigger, built on the
4
+ native HTML [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
5
+ (`popover="auto"`). The browser owns the top layer and light-dismiss; the
6
+ background stays interactive. For a modal dialog that traps focus and inerts the
7
+ page, use [Modal](../Modal/README.md) instead.
8
+
9
+ ```tsx
10
+ import { Popover } from "@primitiv-ui/react";
11
+
12
+ <Popover.Root>
13
+ <Popover.Trigger>Filters</Popover.Trigger>
14
+ <Popover.Content>
15
+ <Popover.Title>Filters</Popover.Title>
16
+ <Popover.Description>Narrow the results below.</Popover.Description>
17
+ <Popover.Close>Done</Popover.Close>
18
+ </Popover.Content>
19
+ </Popover.Root>;
20
+ ```
21
+
22
+ ## Sub-components
23
+
24
+ | Export | Role | Notes |
25
+ | ---------------------- | --------------------- | ----------------------------------------------------------------------------------------- |
26
+ | `Popover.Root` | State owner | Uncontrolled (`defaultOpen`) or controlled (`open` + `onOpenChange`); renders no DOM |
27
+ | `Popover.Trigger` | Toggle button | `<button>` with `aria-haspopup="dialog"` / `aria-expanded` / `aria-controls`; `asChild` |
28
+ | `Popover.Anchor` | Positioning reference | Optional `<div>` to anchor against instead of the trigger; `asChild` |
29
+ | `Popover.Content` | Floating panel | Native-popover `<div role="dialog">`; manages focus + Escape; `asChild`, ref-composing |
30
+ | `Popover.Close` | Close button | Composes `onClick` with close + focus return; `asChild` |
31
+ | `Popover.Title` | Accessible name | `<h2>`; auto-wires `aria-labelledby`; `asChild` |
32
+ | `Popover.Description` | Accessible description | `<p>`; auto-wires `aria-describedby`; `asChild` |
33
+
34
+ ## State modes
35
+
36
+ - **Uncontrolled** — pass `defaultOpen` (or omit to start closed). `onOpenChange`
37
+ is optional and fires on every transition.
38
+ - **Controlled** — pass `open` and `onOpenChange` together. The parent owns the
39
+ value; the component defers every change back through the callback.
40
+
41
+ The two shapes are discriminated at the type level: passing `defaultOpen`
42
+ alongside `open` is a type error.
43
+
44
+ ## Focus & keyboard
45
+
46
+ | Key | Behaviour |
47
+ | -------- | ---------------------------------------------- |
48
+ | `Escape` | Closes the popover, returns focus to the trigger |
49
+
50
+ On open, focus moves into the panel — to the first focusable descendant, or to
51
+ the `Popover.Content` element itself (it carries `tabIndex={-1}`) when it has
52
+ none. Closing via `Escape` or `Popover.Close` returns focus to the trigger.
53
+ There is **no focus trap**: the popover is non-modal and the rest of the page
54
+ remains reachable.
55
+
56
+ ## Light-dismiss
57
+
58
+ Because `Popover.Content` uses `popover="auto"`, the browser light-dismisses it
59
+ on an outside click or `Escape` and promotes it to the top layer. The component
60
+ listens for the native `toggle` event to keep React state in sync, with a
61
+ document-click fallback for environments that don't dispatch it (e.g. jsdom).
62
+
63
+ ## Positioning
64
+
65
+ Placement is a **CSS concern** — no floating-ui, no JS measurement. Use CSS
66
+ anchor positioning: give the trigger (or `Popover.Anchor`) an `anchor-name` and
67
+ `Popover.Content` a matching `position-anchor`, then place it with
68
+ `position-area` / `position-try-fallbacks`. Reach for `Popover.Anchor` when the
69
+ panel should be anchored to something other than the trigger — e.g. a whole
70
+ input group whose trigger is a small button.
71
+
72
+ ```tsx
73
+ <Popover.Root>
74
+ <Popover.Anchor asChild>
75
+ <div className="field" style={{ anchorName: "--popover" }}>
76
+ <input aria-label="Amount" />
77
+ <Popover.Trigger aria-label="Options">▾</Popover.Trigger>
78
+ </div>
79
+ </Popover.Anchor>
80
+ <Popover.Content /* position-anchor: --popover in CSS */>...</Popover.Content>
81
+ </Popover.Root>;
82
+ ```
83
+
84
+ ## `asChild` composition
85
+
86
+ Every rendered sub-component accepts `asChild` to project its behaviour onto a
87
+ consumer element via [Slot](../Slot.tsx): event handlers compose (child first,
88
+ then the library's, unless the child calls `event.preventDefault()`), `style`
89
+ is shallow-merged, `className` strings concatenate, and refs compose. On
90
+ `Popover.Content`, a consumer `ref` composes with the internal ref so open state
91
+ keeps working.
92
+
93
+ ## Styling hooks
94
+
95
+ - `data-state="open" | "closed"` on `Popover.Content`.
96
+ - `aria-expanded` on the trigger tracks the same state.
97
+
98
+ > **Gotcha — don't set `display` on the closed panel.** A closed popover is
99
+ > hidden by the UA rule `[popover]:not(:popover-open) { display: none }`. An
100
+ > author `display` (e.g. `display: flex` for layout) overrides it and leaves the
101
+ > panel visible while closed. Put layout `display` behind `:popover-open`:
102
+ >
103
+ > ```css
104
+ > .panel { padding: 12px; /* no display here */ }
105
+ > .panel:popover-open { display: flex; flex-direction: column; gap: 8px; }
106
+ > ```
@@ -0,0 +1,38 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Popover } from "../Popover";
4
+
5
+ describe("Popover.Anchor", () => {
6
+ it("renders a div wrapping its children as the positioning reference", () => {
7
+ // Arrange
8
+ render(
9
+ <Popover.Root>
10
+ <Popover.Anchor data-testid="anchor">
11
+ <span>Reference</span>
12
+ </Popover.Anchor>
13
+ <Popover.Trigger>Open</Popover.Trigger>
14
+ <Popover.Content>Content</Popover.Content>
15
+ </Popover.Root>,
16
+ );
17
+ const anchor = screen.getByTestId("anchor");
18
+
19
+ // Assert
20
+ expect(anchor.tagName).toBe("DIV");
21
+ expect(anchor).toContainElement(screen.getByText("Reference"));
22
+ });
23
+
24
+ it("forwards arbitrary props to the rendered div", () => {
25
+ // Arrange
26
+ render(
27
+ <Popover.Root>
28
+ <Popover.Anchor data-testid="anchor" className="anchor-name" />
29
+ <Popover.Trigger>Open</Popover.Trigger>
30
+ <Popover.Content>Content</Popover.Content>
31
+ </Popover.Root>,
32
+ );
33
+ const anchor = screen.getByTestId("anchor");
34
+
35
+ // Assert
36
+ expect(anchor).toHaveClass("anchor-name");
37
+ });
38
+ });