@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,160 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { Popover } from "../Popover";
6
+
7
+ describe("Popover asChild composition", () => {
8
+ it("renders the Trigger as a custom element with the trigger ARIA merged in", () => {
9
+ // Arrange
10
+ render(
11
+ <Popover.Root>
12
+ <Popover.Trigger<HTMLAnchorElement> asChild>
13
+ <a href="#open">Open</a>
14
+ </Popover.Trigger>
15
+ <Popover.Content>Content</Popover.Content>
16
+ </Popover.Root>,
17
+ );
18
+ const trigger = screen.getByRole("link", { name: "Open" });
19
+
20
+ // Assert
21
+ expect(trigger).toHaveAttribute("aria-haspopup", "dialog");
22
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
23
+ });
24
+
25
+ it("toggles the popover when the asChild Trigger is clicked and composes the child onClick", async () => {
26
+ // Arrange
27
+ const user = userEvent.setup();
28
+ const onClick = vi.fn();
29
+ render(
30
+ <Popover.Root>
31
+ <Popover.Trigger<HTMLAnchorElement> asChild>
32
+ <a href="#open" onClick={onClick}>
33
+ Open
34
+ </a>
35
+ </Popover.Trigger>
36
+ <Popover.Content>Content</Popover.Content>
37
+ </Popover.Root>,
38
+ );
39
+ const trigger = screen.getByRole("link", { name: "Open" });
40
+
41
+ // Act
42
+ await user.click(trigger);
43
+
44
+ // Assert
45
+ expect(onClick).toHaveBeenCalledTimes(1);
46
+ expect(trigger).toHaveAttribute("aria-expanded", "true");
47
+ });
48
+
49
+ it("renders the Content as a custom element with the dialog props merged in", () => {
50
+ // Arrange
51
+ render(
52
+ <Popover.Root defaultOpen>
53
+ <Popover.Trigger>Open</Popover.Trigger>
54
+ <Popover.Content asChild>
55
+ <section data-testid="content">Content</section>
56
+ </Popover.Content>
57
+ </Popover.Root>,
58
+ );
59
+ const content = screen.getByTestId("content");
60
+
61
+ // Assert
62
+ expect(content.tagName).toBe("SECTION");
63
+ expect(content).toHaveAttribute("role", "dialog");
64
+ expect(content).toHaveAttribute("popover", "auto");
65
+ });
66
+
67
+ it("renders the Close as a custom element that still closes the popover", async () => {
68
+ // Arrange
69
+ const user = userEvent.setup();
70
+ render(
71
+ <Popover.Root defaultOpen>
72
+ <Popover.Trigger>Open</Popover.Trigger>
73
+ <Popover.Content data-testid="content">
74
+ <Popover.Close asChild>
75
+ <a href="#close">Dismiss</a>
76
+ </Popover.Close>
77
+ </Popover.Content>
78
+ </Popover.Root>,
79
+ );
80
+
81
+ // Act
82
+ await user.click(screen.getByRole("link", { name: "Dismiss", hidden: true }));
83
+
84
+ // Assert
85
+ expect(screen.getByTestId("content")).toHaveAttribute("data-state", "closed");
86
+ });
87
+
88
+ it("renders the Anchor as a custom element", () => {
89
+ // Arrange
90
+ render(
91
+ <Popover.Root>
92
+ <Popover.Anchor asChild>
93
+ <label data-testid="anchor">Email</label>
94
+ </Popover.Anchor>
95
+ <Popover.Trigger>Open</Popover.Trigger>
96
+ <Popover.Content>Content</Popover.Content>
97
+ </Popover.Root>,
98
+ );
99
+ const anchor = screen.getByTestId("anchor");
100
+
101
+ // Assert
102
+ expect(anchor.tagName).toBe("LABEL");
103
+ });
104
+
105
+ it("renders the Title and Description as custom elements with ids still wired", () => {
106
+ // Arrange
107
+ render(
108
+ <Popover.Root defaultOpen>
109
+ <Popover.Trigger>Open</Popover.Trigger>
110
+ <Popover.Content data-testid="content">
111
+ <Popover.Title asChild>
112
+ <h3>Filters</h3>
113
+ </Popover.Title>
114
+ <Popover.Description asChild>
115
+ <div>Refine below.</div>
116
+ </Popover.Description>
117
+ </Popover.Content>
118
+ </Popover.Root>,
119
+ );
120
+ const content = screen.getByTestId("content");
121
+ const title = screen.getByRole("heading", { name: "Filters", hidden: true });
122
+ const description = screen.getByText("Refine below.");
123
+
124
+ // Assert
125
+ expect(title.tagName).toBe("H3");
126
+ expect(content).toHaveAttribute("aria-labelledby", title.id);
127
+ expect(content).toHaveAttribute("aria-describedby", description.id);
128
+ });
129
+
130
+ it("forwards a ref to the underlying Trigger element", () => {
131
+ // Arrange
132
+ const ref = createRef<HTMLButtonElement>();
133
+ render(
134
+ <Popover.Root>
135
+ <Popover.Trigger ref={ref}>Open</Popover.Trigger>
136
+ <Popover.Content>Content</Popover.Content>
137
+ </Popover.Root>,
138
+ );
139
+
140
+ // Assert
141
+ expect(ref.current).toBe(screen.getByRole("button", { name: "Open" }));
142
+ });
143
+
144
+ it("forwards a ref to the underlying Content element without breaking open state", () => {
145
+ // Arrange
146
+ const ref = createRef<HTMLDivElement>();
147
+ render(
148
+ <Popover.Root defaultOpen>
149
+ <Popover.Trigger>Open</Popover.Trigger>
150
+ <Popover.Content ref={ref} data-testid="content">Content</Popover.Content>
151
+ </Popover.Root>,
152
+ );
153
+ const content = screen.getByTestId("content");
154
+
155
+ // Assert
156
+ expect(ref.current).toBe(content);
157
+ expect(content).toHaveAttribute("data-state", "open");
158
+ expect(content).toHaveAttribute("data-popover-open");
159
+ });
160
+ });
@@ -0,0 +1,106 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Popover } from "../Popover";
4
+
5
+ describe("Popover basic rendering tests", () => {
6
+ it("should render Popover.Trigger as a button with type='button'", () => {
7
+ // Arrange
8
+ render(
9
+ <Popover.Root>
10
+ <Popover.Trigger>Open</Popover.Trigger>
11
+ <Popover.Content>Content</Popover.Content>
12
+ </Popover.Root>,
13
+ );
14
+ const trigger = screen.getByRole("button", { name: "Open" });
15
+
16
+ // Assert
17
+ expect(trigger.tagName).toBe("BUTTON");
18
+ expect(trigger).toHaveAttribute("type", "button");
19
+ });
20
+
21
+ it("should give Popover.Trigger aria-haspopup='dialog'", () => {
22
+ // Arrange
23
+ render(
24
+ <Popover.Root>
25
+ <Popover.Trigger>Open</Popover.Trigger>
26
+ <Popover.Content>Content</Popover.Content>
27
+ </Popover.Root>,
28
+ );
29
+ const trigger = screen.getByRole("button", { name: "Open" });
30
+
31
+ // Assert
32
+ expect(trigger).toHaveAttribute("aria-haspopup", "dialog");
33
+ });
34
+
35
+ it("should give Popover.Trigger aria-expanded='false' by default", () => {
36
+ // Arrange
37
+ render(
38
+ <Popover.Root>
39
+ <Popover.Trigger>Open</Popover.Trigger>
40
+ <Popover.Content>Content</Popover.Content>
41
+ </Popover.Root>,
42
+ );
43
+ const trigger = screen.getByRole("button", { name: "Open" });
44
+
45
+ // Assert
46
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
47
+ });
48
+
49
+ it("should wire aria-controls on Trigger to the id of Content", () => {
50
+ // Arrange
51
+ render(
52
+ <Popover.Root>
53
+ <Popover.Trigger>Open</Popover.Trigger>
54
+ <Popover.Content data-testid="content">Content</Popover.Content>
55
+ </Popover.Root>,
56
+ );
57
+ const trigger = screen.getByRole("button", { name: "Open" });
58
+ const content = screen.getByTestId("content");
59
+
60
+ // Assert
61
+ expect(trigger).toHaveAttribute("aria-controls", content.id);
62
+ expect(content.id).not.toBe("");
63
+ });
64
+
65
+ it("should render Popover.Content as a native-popover div with role='dialog'", () => {
66
+ // Arrange
67
+ render(
68
+ <Popover.Root>
69
+ <Popover.Trigger>Open</Popover.Trigger>
70
+ <Popover.Content>Content</Popover.Content>
71
+ </Popover.Root>,
72
+ );
73
+ const content = screen.getByRole("dialog", { hidden: true });
74
+
75
+ // Assert
76
+ expect(content.tagName).toBe("DIV");
77
+ expect(content).toHaveAttribute("popover", "auto");
78
+ });
79
+
80
+ it("should give Popover.Content data-state='closed' by default", () => {
81
+ // Arrange
82
+ render(
83
+ <Popover.Root>
84
+ <Popover.Trigger>Open</Popover.Trigger>
85
+ <Popover.Content data-testid="content">Content</Popover.Content>
86
+ </Popover.Root>,
87
+ );
88
+ const content = screen.getByTestId("content");
89
+
90
+ // Assert
91
+ expect(content).toHaveAttribute("data-state", "closed");
92
+ });
93
+
94
+ it("should render no DOM element for Popover.Root itself", () => {
95
+ // Arrange
96
+ const { container } = render(
97
+ <Popover.Root>
98
+ <Popover.Trigger>Open</Popover.Trigger>
99
+ <Popover.Content>Content</Popover.Content>
100
+ </Popover.Root>,
101
+ );
102
+
103
+ // Assert — the trigger button is the first rendered node, not a Root wrapper
104
+ expect(container.firstElementChild?.tagName).toBe("BUTTON");
105
+ });
106
+ });
@@ -0,0 +1,149 @@
1
+ import { act, render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Popover } from "../Popover";
5
+
6
+ describe("Popover light-dismiss", () => {
7
+ it("closes when the user clicks outside the open popover", async () => {
8
+ // Arrange
9
+ const user = userEvent.setup();
10
+ render(
11
+ <div>
12
+ <Popover.Root defaultOpen>
13
+ <Popover.Trigger>Open</Popover.Trigger>
14
+ <Popover.Content data-testid="content">Content</Popover.Content>
15
+ </Popover.Root>
16
+ <button type="button">Outside</button>
17
+ </div>,
18
+ );
19
+ const trigger = screen.getByRole("button", { name: "Open" });
20
+ const content = screen.getByTestId("content");
21
+
22
+ // Act
23
+ await user.click(screen.getByRole("button", { name: "Outside" }));
24
+
25
+ // Assert
26
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
27
+ expect(content).toHaveAttribute("data-state", "closed");
28
+ });
29
+
30
+ it("stays open when the user clicks inside the content", async () => {
31
+ // Arrange
32
+ const user = userEvent.setup();
33
+ render(
34
+ <Popover.Root defaultOpen>
35
+ <Popover.Trigger>Open</Popover.Trigger>
36
+ <Popover.Content data-testid="content">
37
+ <span>Inside</span>
38
+ </Popover.Content>
39
+ </Popover.Root>,
40
+ );
41
+ const trigger = screen.getByRole("button", { name: "Open" });
42
+ const content = screen.getByTestId("content");
43
+
44
+ // Act
45
+ await user.click(screen.getByText("Inside"));
46
+
47
+ // Assert
48
+ expect(trigger).toHaveAttribute("aria-expanded", "true");
49
+ expect(content).toHaveAttribute("data-state", "open");
50
+ });
51
+
52
+ it("syncs React state to closed when the browser light-dismisses (native toggle event)", () => {
53
+ // Arrange
54
+ render(
55
+ <Popover.Root defaultOpen>
56
+ <Popover.Trigger>Open</Popover.Trigger>
57
+ <Popover.Content data-testid="content">Content</Popover.Content>
58
+ </Popover.Root>,
59
+ );
60
+ const trigger = screen.getByRole("button", { name: "Open" });
61
+ const content = screen.getByTestId("content");
62
+
63
+ // Act — simulate the browser closing the popover on its own
64
+ const toggleEvent = new Event("toggle");
65
+ Object.defineProperty(toggleEvent, "newState", { value: "closed" });
66
+ act(() => {
67
+ content.dispatchEvent(toggleEvent);
68
+ });
69
+
70
+ // Assert
71
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
72
+ expect(content).toHaveAttribute("data-state", "closed");
73
+ });
74
+
75
+ it("ignores a native toggle event that reports the popover is opening", () => {
76
+ // Arrange
77
+ render(
78
+ <Popover.Root defaultOpen>
79
+ <Popover.Trigger>Open</Popover.Trigger>
80
+ <Popover.Content data-testid="content">Content</Popover.Content>
81
+ </Popover.Root>,
82
+ );
83
+ const content = screen.getByTestId("content");
84
+
85
+ // Act
86
+ const toggleEvent = new Event("toggle");
87
+ Object.defineProperty(toggleEvent, "newState", { value: "open" });
88
+ content.dispatchEvent(toggleEvent);
89
+
90
+ // Assert
91
+ expect(content).toHaveAttribute("data-state", "open");
92
+ });
93
+
94
+ it("skips the document-click fallback when the native Popover API is present", async () => {
95
+ // Simulate a real browser (native light-dismiss + toggle event handle
96
+ // outside clicks). The document-click fallback must not run there — it
97
+ // would catch the click that opened the popover from an external control
98
+ // and re-close it. With the fallback gated out and no native light-dismiss
99
+ // in jsdom, an outside click leaves the popover open.
100
+ Object.defineProperty(HTMLElement.prototype, "popover", {
101
+ configurable: true,
102
+ get() {
103
+ return "auto";
104
+ },
105
+ });
106
+ try {
107
+ const user = userEvent.setup();
108
+ render(
109
+ <div>
110
+ <Popover.Root defaultOpen>
111
+ <Popover.Trigger>Open</Popover.Trigger>
112
+ <Popover.Content data-testid="content">Content</Popover.Content>
113
+ </Popover.Root>
114
+ <button type="button">Outside</button>
115
+ </div>,
116
+ );
117
+ const content = screen.getByTestId("content");
118
+
119
+ await user.click(screen.getByRole("button", { name: "Outside" }));
120
+
121
+ expect(content).toHaveAttribute("data-state", "open");
122
+ } finally {
123
+ delete (HTMLElement.prototype as unknown as { popover?: unknown })
124
+ .popover;
125
+ }
126
+ });
127
+
128
+ it("closes only once when the trigger itself is clicked to close (no double dismiss)", async () => {
129
+ // Arrange
130
+ const user = userEvent.setup();
131
+ const onOpenChange = vi.fn();
132
+ render(
133
+ <Popover.Root onOpenChange={onOpenChange}>
134
+ <Popover.Trigger>Open</Popover.Trigger>
135
+ <Popover.Content>Content</Popover.Content>
136
+ </Popover.Root>,
137
+ );
138
+ const trigger = screen.getByRole("button", { name: "Open" });
139
+
140
+ // Act
141
+ await user.click(trigger); // open
142
+ await user.click(trigger); // close via the trigger, not light-dismiss
143
+
144
+ // Assert
145
+ expect(onOpenChange).toHaveBeenNthCalledWith(1, true);
146
+ expect(onOpenChange).toHaveBeenNthCalledWith(2, false);
147
+ expect(onOpenChange).toHaveBeenCalledTimes(2);
148
+ });
149
+ });
@@ -0,0 +1,70 @@
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.Close", () => {
7
+ it("renders a button that closes the popover when clicked", 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
+ <Popover.Close>Dismiss</Popover.Close>
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: "Dismiss", hidden: true }));
23
+
24
+ // Assert
25
+ expect(content).toHaveAttribute("data-state", "closed");
26
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
27
+ });
28
+
29
+ it("returns focus to the trigger after closing", async () => {
30
+ // Arrange
31
+ const user = userEvent.setup();
32
+ render(
33
+ <Popover.Root defaultOpen>
34
+ <Popover.Trigger>Open</Popover.Trigger>
35
+ <Popover.Content>
36
+ <Popover.Close>Dismiss</Popover.Close>
37
+ </Popover.Content>
38
+ </Popover.Root>,
39
+ );
40
+ const trigger = screen.getByRole("button", { name: "Open" });
41
+
42
+ // Act
43
+ await user.click(screen.getByRole("button", { name: "Dismiss", hidden: true }));
44
+
45
+ // Assert
46
+ expect(trigger).toHaveFocus();
47
+ });
48
+
49
+ it("lets a consumer onClick veto the close via preventDefault", async () => {
50
+ // Arrange
51
+ const user = userEvent.setup();
52
+ render(
53
+ <Popover.Root defaultOpen>
54
+ <Popover.Trigger>Open</Popover.Trigger>
55
+ <Popover.Content data-testid="content">
56
+ <Popover.Close onClick={(event) => event.preventDefault()}>
57
+ Dismiss
58
+ </Popover.Close>
59
+ </Popover.Content>
60
+ </Popover.Root>,
61
+ );
62
+ const content = screen.getByTestId("content");
63
+
64
+ // Act
65
+ await user.click(screen.getByRole("button", { name: "Dismiss", hidden: true }));
66
+
67
+ // Assert
68
+ expect(content).toHaveAttribute("data-state", "open");
69
+ });
70
+ });
@@ -0,0 +1,58 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Popover } from "../Popover";
4
+
5
+ describe("Popover.Title and Popover.Description", () => {
6
+ it("wires Content aria-labelledby to a Popover.Title", () => {
7
+ // Arrange
8
+ render(
9
+ <Popover.Root defaultOpen>
10
+ <Popover.Trigger>Open</Popover.Trigger>
11
+ <Popover.Content data-testid="content">
12
+ <Popover.Title>Filters</Popover.Title>
13
+ </Popover.Content>
14
+ </Popover.Root>,
15
+ );
16
+ const content = screen.getByTestId("content");
17
+ const title = screen.getByRole("heading", { name: "Filters", hidden: true });
18
+
19
+ // Assert
20
+ expect(title.tagName).toBe("H2");
21
+ expect(content).toHaveAttribute("aria-labelledby", title.id);
22
+ expect(title.id).not.toBe("");
23
+ });
24
+
25
+ it("wires Content aria-describedby to a Popover.Description", () => {
26
+ // Arrange
27
+ render(
28
+ <Popover.Root defaultOpen>
29
+ <Popover.Trigger>Open</Popover.Trigger>
30
+ <Popover.Content data-testid="content">
31
+ <Popover.Description>Narrow the results below.</Popover.Description>
32
+ </Popover.Content>
33
+ </Popover.Root>,
34
+ );
35
+ const content = screen.getByTestId("content");
36
+ const description = screen.getByText("Narrow the results below.");
37
+
38
+ // Assert
39
+ expect(description.tagName).toBe("P");
40
+ expect(content).toHaveAttribute("aria-describedby", description.id);
41
+ expect(description.id).not.toBe("");
42
+ });
43
+
44
+ it("omits aria-labelledby and aria-describedby when neither is present", () => {
45
+ // Arrange
46
+ render(
47
+ <Popover.Root defaultOpen>
48
+ <Popover.Trigger>Open</Popover.Trigger>
49
+ <Popover.Content data-testid="content">Content</Popover.Content>
50
+ </Popover.Root>,
51
+ );
52
+ const content = screen.getByTestId("content");
53
+
54
+ // Assert
55
+ expect(content).not.toHaveAttribute("aria-labelledby");
56
+ expect(content).not.toHaveAttribute("aria-describedby");
57
+ });
58
+ });
@@ -0,0 +1,45 @@
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 controlled state", () => {
7
+ it("reflects the controlled open prop", () => {
8
+ // Arrange & Act
9
+ render(
10
+ <Popover.Root open onOpenChange={() => {}}>
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("defers state changes to the parent — clicking calls onOpenChange but does not self-open", async () => {
25
+ // Arrange
26
+ const user = userEvent.setup();
27
+ const onOpenChange = vi.fn();
28
+ render(
29
+ <Popover.Root open={false} onOpenChange={onOpenChange}>
30
+ <Popover.Trigger>Open</Popover.Trigger>
31
+ <Popover.Content data-testid="content">Content</Popover.Content>
32
+ </Popover.Root>,
33
+ );
34
+ const trigger = screen.getByRole("button", { name: "Open" });
35
+ const content = screen.getByTestId("content");
36
+
37
+ // Act
38
+ await user.click(trigger);
39
+
40
+ // Assert
41
+ expect(onOpenChange).toHaveBeenCalledExactlyOnceWith(true);
42
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
43
+ expect(content).toHaveAttribute("data-state", "closed");
44
+ });
45
+ });
@@ -0,0 +1,51 @@
1
+ import { render } from "@testing-library/react";
2
+
3
+ import { Popover } from "../Popover";
4
+
5
+ describe("Popover error handling", () => {
6
+ beforeEach(() => {
7
+ // Suppress React's noisy uncaught-error logging while we deliberately
8
+ // render sub-components outside their Root.
9
+ vi.spyOn(console, "error").mockImplementation(() => {});
10
+ });
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ it("throws when Popover.Trigger is rendered without a Popover.Root", () => {
17
+ expect(() => render(<Popover.Trigger>Open</Popover.Trigger>)).toThrow(
18
+ /Popover\.Root/,
19
+ );
20
+ });
21
+
22
+ it("throws when Popover.Anchor is rendered without a Popover.Root", () => {
23
+ expect(() => render(<Popover.Anchor>Ref</Popover.Anchor>)).toThrow(
24
+ /Popover\.Root/,
25
+ );
26
+ });
27
+
28
+ it("throws when Popover.Content is rendered without a Popover.Root", () => {
29
+ expect(() => render(<Popover.Content>Content</Popover.Content>)).toThrow(
30
+ /Popover\.Root/,
31
+ );
32
+ });
33
+
34
+ it("throws when Popover.Close is rendered without a Popover.Root", () => {
35
+ expect(() => render(<Popover.Close>Close</Popover.Close>)).toThrow(
36
+ /Popover\.Root/,
37
+ );
38
+ });
39
+
40
+ it("throws when Popover.Title is rendered without a Popover.Root", () => {
41
+ expect(() => render(<Popover.Title>Title</Popover.Title>)).toThrow(
42
+ /Popover\.Root/,
43
+ );
44
+ });
45
+
46
+ it("throws when Popover.Description is rendered without a Popover.Root", () => {
47
+ expect(() =>
48
+ render(<Popover.Description>Desc</Popover.Description>),
49
+ ).toThrow(/Popover\.Root/);
50
+ });
51
+ });
@@ -0,0 +1,43 @@
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 focus management", () => {
7
+ it("moves focus to the first focusable element inside the content on open", async () => {
8
+ // Arrange
9
+ const user = userEvent.setup();
10
+ render(
11
+ <Popover.Root>
12
+ <Popover.Trigger>Open</Popover.Trigger>
13
+ <Popover.Content>
14
+ <button type="button">First</button>
15
+ <button type="button">Second</button>
16
+ </Popover.Content>
17
+ </Popover.Root>,
18
+ );
19
+
20
+ // Act
21
+ await user.click(screen.getByRole("button", { name: "Open" }));
22
+
23
+ // Assert
24
+ expect(screen.getByRole("button", { name: "First", hidden: true })).toHaveFocus();
25
+ });
26
+
27
+ it("moves focus to the content element itself when it has no focusable children", async () => {
28
+ // Arrange
29
+ const user = userEvent.setup();
30
+ render(
31
+ <Popover.Root>
32
+ <Popover.Trigger>Open</Popover.Trigger>
33
+ <Popover.Content data-testid="content">Just text, nothing to focus.</Popover.Content>
34
+ </Popover.Root>,
35
+ );
36
+
37
+ // Act
38
+ await user.click(screen.getByRole("button", { name: "Open" }));
39
+
40
+ // Assert
41
+ expect(screen.getByTestId("content")).toHaveFocus();
42
+ });
43
+ });