@primitiv-ui/react 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/README.md +4 -0
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +25 -4
  7. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  8. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  9. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  10. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  11. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  12. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  13. package/src/Accordion/types.ts +72 -21
  14. package/src/Alert/Alert.tsx +36 -7
  15. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  16. package/src/Alert/types.ts +18 -2
  17. package/src/Avatar/Avatar.tsx +99 -15
  18. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  19. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  20. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  21. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  22. package/src/Avatar/types.ts +15 -3
  23. package/src/Breadcrumb/Breadcrumb.tsx +136 -18
  24. package/src/Breadcrumb/README.md +47 -0
  25. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  27. package/src/Breadcrumb/types.ts +73 -7
  28. package/src/Button/Button.tsx +2 -0
  29. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  30. package/src/Button/types.ts +21 -0
  31. package/src/Carousel/Carousel.tsx +263 -15
  32. package/src/Carousel/README.md +749 -72
  33. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  34. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  35. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  36. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  37. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  38. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  39. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
  40. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  41. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  42. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  43. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  44. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  45. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  46. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  47. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  48. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  49. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  50. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  51. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  52. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  53. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  54. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  55. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  56. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  57. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  58. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  59. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  60. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  61. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  62. package/src/Carousel/hooks/index.ts +1 -0
  63. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  64. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  65. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  66. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  67. package/src/Carousel/loopEngine.ts +54 -0
  68. package/src/Carousel/types.ts +457 -35
  69. package/src/Checkbox/Checkbox.tsx +9 -1
  70. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  71. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  72. package/src/Checkbox/types.ts +19 -0
  73. package/src/CheckboxCard/CheckboxCard.tsx +13 -4
  74. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  75. package/src/CheckboxCard/types.ts +65 -17
  76. package/src/Collapsible/Collapsible.tsx +49 -3
  77. package/src/Collapsible/README.md +40 -0
  78. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  79. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  80. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  81. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  82. package/src/Collapsible/types.ts +89 -22
  83. package/src/ContextMenu/ContextMenu.tsx +270 -4
  84. package/src/ContextMenu/types.ts +148 -6
  85. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  86. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  87. package/src/DirectionProvider/types.ts +28 -3
  88. package/src/Divider/Divider.tsx +24 -10
  89. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  90. package/src/Divider/types.ts +10 -3
  91. package/src/Drawer/Drawer.tsx +315 -0
  92. package/src/Drawer/README.md +124 -0
  93. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  94. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  95. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  96. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  97. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  98. package/src/Drawer/index.ts +2 -0
  99. package/src/Drawer/types.ts +66 -0
  100. package/src/Dropdown/Dropdown.tsx +117 -0
  101. package/src/Dropdown/types.ts +241 -11
  102. package/src/EmptyState/EmptyState.tsx +74 -4
  103. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  104. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  105. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  106. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  107. package/src/EmptyState/types.ts +49 -6
  108. package/src/Field/Field.tsx +23 -0
  109. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  110. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  111. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  112. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  113. package/src/Field/types.ts +60 -12
  114. package/src/Fieldset/Fieldset.tsx +54 -18
  115. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  116. package/src/Fieldset/types.ts +20 -2
  117. package/src/Input/Input.tsx +14 -9
  118. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  119. package/src/Input/types.ts +19 -2
  120. package/src/InputGroup/InputGroup.tsx +11 -3
  121. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  122. package/src/InputGroup/types.ts +26 -5
  123. package/src/MillerColumns/MillerColumns.tsx +86 -19
  124. package/src/MillerColumns/types.ts +83 -27
  125. package/src/Modal/Modal.tsx +31 -4
  126. package/src/Modal/README.md +19 -10
  127. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  128. package/src/Modal/hooks/useModalContent.ts +32 -0
  129. package/src/Modal/types.ts +110 -29
  130. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  131. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  132. package/src/NavigationMenu/README.md +355 -0
  133. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  134. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  135. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  136. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  137. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  138. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  139. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  140. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  141. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  142. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  143. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  144. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  145. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  146. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  147. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  148. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  149. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  150. package/src/NavigationMenu/constants.ts +5 -0
  151. package/src/NavigationMenu/hooks/index.ts +7 -0
  152. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  153. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  154. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  155. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  156. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  157. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  158. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  159. package/src/NavigationMenu/index.ts +3 -0
  160. package/src/NavigationMenu/types.ts +259 -0
  161. package/src/NavigationMenu/utils.ts +89 -0
  162. package/src/Popover/Popover.tsx +427 -0
  163. package/src/Popover/PopoverContext.ts +13 -0
  164. package/src/Popover/README.md +106 -0
  165. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  166. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  167. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  168. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  169. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  170. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  171. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  172. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  173. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  174. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  175. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  176. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  177. package/src/Popover/hooks/index.ts +3 -0
  178. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  179. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  180. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  181. package/src/Popover/index.ts +2 -0
  182. package/src/Popover/types.ts +197 -0
  183. package/src/Portal/Portal.tsx +50 -10
  184. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  185. package/src/Portal/types.ts +5 -0
  186. package/src/Progress/Progress.tsx +33 -1
  187. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  188. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  189. package/src/Progress/types.ts +20 -4
  190. package/src/Radio/Radio.tsx +9 -1
  191. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  192. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  193. package/src/Radio/types.ts +20 -0
  194. package/src/RadioCard/RadioCard.tsx +11 -0
  195. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  196. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  197. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  198. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  199. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  200. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  201. package/src/RadioCard/types.ts +95 -22
  202. package/src/RadioGroup/RadioGroup.tsx +11 -0
  203. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  204. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  205. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  206. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  207. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  208. package/src/RadioGroup/types.ts +73 -9
  209. package/src/SegmentedControl/README.md +154 -0
  210. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  211. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  212. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  213. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  214. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  215. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  216. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  217. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  218. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  219. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  220. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  221. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  222. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  223. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  224. package/src/SegmentedControl/hooks/index.ts +2 -0
  225. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  226. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
  227. package/src/SegmentedControl/index.ts +2 -0
  228. package/src/SegmentedControl/types.ts +138 -0
  229. package/src/Select/README.md +122 -157
  230. package/src/Select/Select.tsx +595 -68
  231. package/src/Select/SelectContext.ts +38 -0
  232. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  233. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  234. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  235. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  236. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  237. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  238. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  239. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  240. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  241. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  242. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  243. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  244. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  245. package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
  246. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  247. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  248. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  249. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  250. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  251. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  252. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  253. package/src/Select/constants.ts +8 -0
  254. package/src/Select/hooks/index.ts +2 -0
  255. package/src/Select/hooks/useSelectContent.ts +149 -0
  256. package/src/Select/hooks/useSelectRoot.ts +149 -0
  257. package/src/Select/types.ts +199 -31
  258. package/src/SkipNav/SkipNav.tsx +69 -29
  259. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  260. package/src/SkipNav/types.ts +17 -9
  261. package/src/Slider/Slider.tsx +33 -1
  262. package/src/Slider/SliderContext.ts +28 -1
  263. package/src/Slider/types.ts +157 -9
  264. package/src/Slot/Slot.tsx +94 -19
  265. package/src/Slot/__tests__/Slot.test.tsx +110 -0
  266. package/src/Slot/composeEventHandlers.ts +26 -2
  267. package/src/Slot/types.ts +16 -0
  268. package/src/Status/Status.tsx +36 -15
  269. package/src/Status/__tests__/Status.test.tsx +5 -0
  270. package/src/Status/types.ts +12 -1
  271. package/src/Switch/Switch.tsx +15 -2
  272. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  273. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  274. package/src/Switch/types.ts +49 -22
  275. package/src/Table/Table.tsx +44 -10
  276. package/src/Table/types.ts +66 -17
  277. package/src/Tabs/Tabs.tsx +8 -0
  278. package/src/Tabs/types.ts +29 -4
  279. package/src/Textarea/Textarea.tsx +9 -1
  280. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  281. package/src/Textarea/types.ts +20 -3
  282. package/src/Toggle/Toggle.tsx +51 -13
  283. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  284. package/src/Toggle/types.ts +54 -4
  285. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  286. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  287. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  288. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  289. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  290. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  291. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  292. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  293. package/src/ToggleGroup/types.ts +55 -2
  294. package/src/Tooltip/Tooltip.tsx +98 -20
  295. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  296. package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
  297. package/src/Tooltip/types.ts +89 -8
  298. package/src/Tree/Tree.tsx +187 -33
  299. package/src/Tree/types.ts +54 -12
  300. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  301. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  302. package/src/VisuallyHidden/types.ts +11 -1
  303. package/src/index.ts +4 -0
  304. package/src/test/intersectionObserverPolyfill.ts +7 -0
  305. package/src/test/resizeObserverPolyfill.ts +80 -0
  306. package/src/utils/focusable.ts +15 -0
  307. package/src/utils/index.ts +1 -0
@@ -24,11 +24,27 @@ import type {
24
24
  } from "./types";
25
25
 
26
26
  /**
27
- * Wraps one or more `Tooltip.Root` instances and provides shared
28
- * delay configuration. Required ancestor — rendering `Tooltip.Root`
29
- * without a Provider throws a context error.
27
+ * Wraps one or more {@link TooltipRoot | `Tooltip.Root`} instances and provides
28
+ * the shared delay configuration and group-wide open coordination. Required
29
+ * ancestor — rendering `Tooltip.Root` without a Provider throws a context
30
+ * error. Renders no DOM of its own (a `TooltipProviderContext` provider only).
30
31
  *
31
- * @example
32
+ * **Delay coordination.** Two timings govern how a group of tooltips feels:
33
+ *
34
+ * - `delayDuration` — how long a pointer must rest on a trigger before its
35
+ * tooltip opens (hover only; focus always opens immediately).
36
+ * - `skipDelayDuration` — the grace window after a tooltip closes during which
37
+ * moving to *another* trigger in the same Provider opens that tooltip
38
+ * instantly, with no `delayDuration` wait. Rest longer than this window and
39
+ * the next hover pays the full `delayDuration` again. This is what makes a
40
+ * toolbar of icon buttons feel responsive once the first tip has shown.
41
+ *
42
+ * Place a single Provider high in the tree (typically at the app root) so every
43
+ * tooltip shares one skip window. A per-instance
44
+ * {@link TooltipRootProps.delayDuration | `delayDuration`} on `Tooltip.Root`
45
+ * overrides the Provider's value for that one tooltip.
46
+ *
47
+ * @example App-wide provider
32
48
  * ```tsx
33
49
  * <Tooltip.Provider delayDuration={400}>
34
50
  * <App />
@@ -55,13 +71,27 @@ export function TooltipProviderComponent({
55
71
  TooltipProviderComponent.displayName = "TooltipProvider";
56
72
 
57
73
  /**
58
- * The state boundary for a single tooltip. Owns the open/closed
59
- * state and wires it down to all sub-components via context.
74
+ * The state boundary for a single tooltip. Owns the open/closed state, runs the
75
+ * open-delay and grace-period timers, and wires everything down to the
76
+ * sub-components via `TooltipContext`. Renders no DOM of its own. Must be a
77
+ * descendant of {@link TooltipProviderComponent | `Tooltip.Provider`}.
60
78
  *
61
- * Supports two state modes, statically discriminated at the type level:
79
+ * **Controlled vs uncontrolled**, statically discriminated at the type level so
80
+ * TypeScript accepts only one shape:
62
81
  *
63
- * - **Uncontrolled** — pass `defaultOpen` or omit for closed-on-mount.
64
- * - **Controlled** — pass `open` (and optionally `onOpenChange`).
82
+ * - **Uncontrolled** — pass {@link UncontrolledTooltipRootProps.defaultOpen | `defaultOpen`}
83
+ * (or omit it for closed-on-mount). The component owns the flag; observe
84
+ * transitions with `onOpenChange`.
85
+ * - **Controlled** — pass {@link ControlledTooltipRootProps.open | `open`}
86
+ * (with an optional `onOpenChange`). The parent owns the flag; the delay and
87
+ * grace timers still run and drive `onOpenChange`, but the tooltip only moves
88
+ * when the parent updates `open`.
89
+ *
90
+ * **Timing overrides.** {@link TooltipRootProps.delayDuration | `delayDuration`}
91
+ * overrides the Provider's open delay for this tooltip only.
92
+ * {@link TooltipRootProps.disableHoverableContent | `disableHoverableContent`}
93
+ * removes the grace period, so the tooltip closes the instant the pointer
94
+ * leaves the trigger (the user cannot move into the content).
65
95
  *
66
96
  * @example
67
97
  * ```tsx
@@ -94,13 +124,24 @@ export function TooltipRoot({
94
124
  TooltipRoot.displayName = "TooltipRoot";
95
125
 
96
126
  /**
97
- * The element that triggers the tooltip on hover and focus. Renders a
98
- * `<button type="button">` by default; use `asChild` to wrap a custom
99
- * element.
127
+ * The element that opens the tooltip. Renders a `<button type="button">` by
128
+ * default; pass `asChild` to project the trigger behaviour onto a custom
129
+ * element (a link, an icon button, etc.).
130
+ *
131
+ * **Open / close behaviour.** Pointer-enter opens after the effective
132
+ * `delayDuration` (immediately if the group's skip window is active);
133
+ * pointer-leave closes after a short grace period so the pointer can travel
134
+ * into hoverable content (immediately when `disableHoverableContent` is set on
135
+ * Root). Focus opens immediately and blur closes immediately, and `Escape`
136
+ * while focused closes immediately — matching the WAI-ARIA tooltip pattern.
137
+ * Consumer `onPointerEnter` / `onPointerLeave` / `onFocus` / `onBlur` /
138
+ * `onKeyDown` all compose (they run first).
100
139
  *
101
- * ARIA wiring:
102
- * - `aria-describedby` points at `Tooltip.Content`'s id.
103
- * - `data-state="open" | "closed"` mirrors the tooltip state.
140
+ * **ARIA wiring.**
141
+ * - `aria-describedby` points at {@link TooltipContent | `Tooltip.Content`}'s id.
142
+ * - `data-state="open" | "closed"` mirrors the tooltip state for CSS.
143
+ *
144
+ * @extends HTMLButtonElement
104
145
  *
105
146
  * @example
106
147
  * ```tsx
@@ -148,7 +189,8 @@ TooltipTrigger.displayName = "TooltipTrigger";
148
189
  *
149
190
  * By default the portal only renders while the tooltip is open. Pass
150
191
  * `forceMount` to keep the subtree mounted when closed — useful for
151
- * CSS exit animations driven by `data-state="closed"`.
192
+ * CSS exit animations driven by `data-state="closed"`. Renders no wrapper
193
+ * element of its own.
152
194
  *
153
195
  * @example
154
196
  * ```tsx
@@ -192,6 +234,8 @@ TooltipPortal.displayName = "TooltipPortal";
192
234
  * `anchor-name` to the trigger and `position-anchor` / `position-area`
193
235
  * to the content in your own stylesheet.
194
236
  *
237
+ * @extends HTMLDivElement
238
+ *
195
239
  * @example
196
240
  * ```tsx
197
241
  * <Tooltip.Content className="tooltip__content">
@@ -233,6 +277,8 @@ TooltipContent.displayName = "TooltipContent";
233
277
  *
234
278
  * All positioning and styling is the consumer's responsibility via CSS.
235
279
  *
280
+ * @extends HTMLSpanElement
281
+ *
236
282
  * @example
237
283
  * ```tsx
238
284
  * <Tooltip.Content>
@@ -270,11 +316,43 @@ export type TooltipCompound = typeof TooltipRoot & {
270
316
  * Headless, accessible **Tooltip** — a compound component implementing the
271
317
  * [WAI-ARIA tooltip pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/):
272
318
  * a deferred, hoverable label anchored to a trigger, with a shared
273
- * {@link Tooltip.Provider} governing open / skip delays across a group. Zero
274
- * styles ship.
319
+ * {@link TooltipProviderComponent | `Tooltip.Provider`} governing open / skip
320
+ * delays across a group. Opens on hover (after a delay) and on focus
321
+ * (immediately); closes on blur, on `Escape`, and after a short grace period on
322
+ * pointer-leave so the pointer can travel into hoverable content. Zero styles
323
+ * ship; visual placement is your CSS concern (CSS anchor positioning).
324
+ *
325
+ * `Tooltip` is both callable (an alias of {@link TooltipRoot | `Tooltip.Root`})
326
+ * and carries its sub-components as static properties. A `Tooltip.Provider`
327
+ * ancestor is required — `Tooltip.Root` throws without one.
328
+ *
329
+ * - {@link TooltipProviderComponent | `Tooltip.Provider`} — shared delay config + group open coordination.
330
+ * - {@link TooltipRoot | `Tooltip.Root`} — per-tooltip state owner and timing.
331
+ * - {@link TooltipTrigger | `Tooltip.Trigger`} — `<button>` that opens on hover/focus.
332
+ * - {@link TooltipPortal | `Tooltip.Portal`} — optional portal to `document.body`.
333
+ * - {@link TooltipContent | `Tooltip.Content`} — the `role="tooltip"` panel.
334
+ * - {@link TooltipArrow | `Tooltip.Arrow`} — optional visual pointer.
335
+ *
336
+ * @example Minimal usage
337
+ * ```tsx
338
+ * import { Tooltip } from "@primitiv-ui/react";
339
+ *
340
+ * <Tooltip.Provider>
341
+ * <Tooltip.Root>
342
+ * <Tooltip.Trigger>Save</Tooltip.Trigger>
343
+ * <Tooltip.Portal>
344
+ * <Tooltip.Content>
345
+ * Save your changes
346
+ * <Tooltip.Arrow />
347
+ * </Tooltip.Content>
348
+ * </Tooltip.Portal>
349
+ * </Tooltip.Root>
350
+ * </Tooltip.Provider>;
351
+ * ```
275
352
  *
276
- * The default export is the `Root`; sub-components are attached as static
277
- * properties (`Tooltip.Provider`, `Tooltip.Trigger`, `Tooltip.Content`, …).
353
+ * @see {@link TooltipProviderComponent} for delay / skip-delay coordination.
354
+ * @see {@link TooltipRoot} for the controlled/uncontrolled state modes and timing overrides.
355
+ * @see {@link TooltipContent} for the grace period and the Escape / outside-pointer escape hatches.
278
356
  */
279
357
  const TooltipCompound: TooltipCompound = Object.assign(TooltipRoot, {
280
358
  Provider: TooltipProviderComponent,
@@ -0,0 +1,40 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { useTooltipProvider } from "../useTooltipProvider";
5
+
6
+ /**
7
+ * `useTooltipProvider` schedules its skip-delay reset with a raw
8
+ * `setTimeout`, so it is exercised here directly rather than through the
9
+ * full `Tooltip.Provider` tree — the defect under test (a timer surviving
10
+ * unmount) has no user-facing render output to assert on.
11
+ */
12
+ function Probe() {
13
+ const { contextValue } = useTooltipProvider({
14
+ delayDuration: 0,
15
+ skipDelayDuration: 300,
16
+ });
17
+ return <button onClick={contextValue.onCloseGlobally}>close</button>;
18
+ }
19
+
20
+ describe("useTooltipProvider — timer cleanup", () => {
21
+ it("clears the pending skip-delay timeout on unmount instead of letting it fire later", async () => {
22
+ const user = userEvent.setup();
23
+ const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
24
+ const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
25
+
26
+ const { unmount } = render(<Probe />);
27
+ await user.click(screen.getByRole("button", { name: "close" }));
28
+
29
+ // setTimeoutSpy also captures unrelated timers (userEvent's own
30
+ // scheduling, React internals) — find the one this hook scheduled with
31
+ // its 300ms skipDelayDuration.
32
+ const callIndex = setTimeoutSpy.mock.calls.findIndex(
33
+ ([, delay]) => delay === 300,
34
+ );
35
+ const timerId = setTimeoutSpy.mock.results[callIndex].value;
36
+ unmount();
37
+
38
+ expect(clearTimeoutSpy).toHaveBeenCalledWith(timerId);
39
+ });
40
+ });
@@ -1,4 +1,4 @@
1
- import { useCallback, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
3
  import type { TooltipProviderContextValue } from "../types";
4
4
 
@@ -29,6 +29,18 @@ export function useTooltipProvider({
29
29
  }, skipDelayDuration);
30
30
  }, [skipDelayDuration]);
31
31
 
32
+ // The skip-delay reset is a raw `setTimeout`, so it outlives the
33
+ // component it was scheduled from unless explicitly cancelled — without
34
+ // this, an in-flight timer fires after unmount and can throw once its
35
+ // surrounding environment (e.g. a test's jsdom `window`) is gone.
36
+ useEffect(() => {
37
+ return () => {
38
+ if (skipDelayTimerRef.current !== null) {
39
+ clearTimeout(skipDelayTimerRef.current);
40
+ }
41
+ };
42
+ }, []);
43
+
32
44
  const contextValue: TooltipProviderContextValue = {
33
45
  delayDuration,
34
46
  skipDelayDuration,
@@ -30,12 +30,26 @@ export type TooltipContextValue = {
30
30
  };
31
31
 
32
32
  /**
33
- * Props for {@link Tooltip.Provider} — sets the `delayDuration` and
34
- * `skipDelayDuration` shared by every tooltip in the subtree.
33
+ * Props for {@link Tooltip.Provider} — sets the delay timings shared by every
34
+ * tooltip in the subtree.
35
35
  */
36
36
  export type TooltipProviderProps = {
37
+ /** The tooltips (and other markup) that share this Provider's timings. */
37
38
  children?: ReactNode;
39
+ /**
40
+ * How long, in milliseconds, a pointer must rest on a trigger before the
41
+ * tooltip opens on hover. Focus always opens immediately regardless of this.
42
+ * Overridable per tooltip via {@link TooltipRootProps.delayDuration}.
43
+ * @default 700
44
+ */
38
45
  delayDuration?: number;
46
+ /**
47
+ * The grace window, in milliseconds, after a tooltip in this group closes
48
+ * during which hovering *another* trigger in the same Provider opens it
49
+ * instantly (no `delayDuration` wait). Rest longer than this and the next
50
+ * hover pays the full delay again.
51
+ * @default 300
52
+ */
39
53
  skipDelayDuration?: number;
40
54
  };
41
55
 
@@ -45,8 +59,16 @@ export type TooltipProviderProps = {
45
59
  * and `open` is forbidden.
46
60
  */
47
61
  export type UncontrolledTooltipRootProps = {
62
+ /**
63
+ * Whether the tooltip is open on first render. The component owns the flag
64
+ * from then on (hover/focus/blur/Escape drive it internally).
65
+ * @default false
66
+ */
48
67
  defaultOpen?: boolean;
68
+ /** Forbidden in uncontrolled mode — use `defaultOpen` instead. */
49
69
  open?: never;
70
+ /** Called with the new open state after every transition. Optional in
71
+ * uncontrolled mode; use it to observe (not drive) open/close. */
50
72
  onOpenChange?: (open: boolean) => void;
51
73
  };
52
74
 
@@ -56,8 +78,13 @@ export type UncontrolledTooltipRootProps = {
56
78
  * forbidden.
57
79
  */
58
80
  export type ControlledTooltipRootProps = {
81
+ /** The current open state. The delay and grace timers still run and call
82
+ * `onOpenChange`, but the tooltip only moves when the parent updates this. */
59
83
  open: boolean;
84
+ /** Called with the requested open state whenever the tooltip wants to open
85
+ * or close. Optional even in controlled mode. */
60
86
  onOpenChange?: (open: boolean) => void;
87
+ /** Forbidden in controlled mode — use `open` instead. */
61
88
  defaultOpen?: never;
62
89
  };
63
90
 
@@ -68,45 +95,99 @@ export type ControlledTooltipRootProps = {
68
95
  * so TypeScript accepts exactly one mode.
69
96
  */
70
97
  export type TooltipRootProps = {
98
+ /** The tooltip's {@link TooltipTriggerProps | `Tooltip.Trigger`},
99
+ * {@link TooltipContentProps | `Tooltip.Content`} (usually inside a
100
+ * {@link TooltipPortalProps | `Tooltip.Portal`}), and optional
101
+ * {@link TooltipArrowProps | `Tooltip.Arrow`}. */
71
102
  children?: ReactNode;
103
+ /**
104
+ * Overrides {@link TooltipProviderProps.delayDuration} for this tooltip only
105
+ * — the hover open delay in milliseconds. Falls back to the Provider's value
106
+ * when omitted.
107
+ */
72
108
  delayDuration?: number;
109
+ /**
110
+ * When `true`, removes the pointer-leave grace period: the tooltip closes the
111
+ * instant the pointer leaves the trigger, so the user cannot move into the
112
+ * content. Leave `false` for tooltips whose content is meant to be hovered.
113
+ * @default false
114
+ */
73
115
  disableHoverableContent?: boolean;
74
116
  } & (UncontrolledTooltipRootProps | ControlledTooltipRootProps);
75
117
 
76
118
  /**
77
119
  * Props for {@link Tooltip.Trigger} — all native `<button>` attributes plus
78
- * the `asChild` escape hatch.
120
+ * the `asChild` escape hatch. The `aria-describedby`, `data-state`, and the
121
+ * hover/focus/blur/Escape open-close handlers are added by the component
122
+ * (consumer handlers compose and run first).
79
123
  */
80
124
  export type TooltipTriggerProps = ComponentProps<"button"> & {
125
+ /**
126
+ * Render the single child element (a link, icon button, etc.) instead of the
127
+ * default `<button>`, merging the trigger's ARIA and event handlers onto it
128
+ * via the {@link Slot} pattern.
129
+ * @default false
130
+ */
81
131
  asChild?: boolean;
82
132
  };
83
133
 
84
134
  /**
85
135
  * Props for {@link Tooltip.Portal} — the `container` to render the content
86
- * into (defaults to `document.body`) and `forceMount` to keep the content
87
- * in the DOM while closed for exit animations.
136
+ * into and `forceMount` to keep the content in the DOM while closed for exit
137
+ * animations.
88
138
  */
89
139
  export type TooltipPortalProps = {
140
+ /** The content to portal — typically a single
141
+ * {@link TooltipContentProps | `Tooltip.Content`}. */
90
142
  children?: ReactNode;
143
+ /**
144
+ * The element to portal the content into.
145
+ * @default document.body
146
+ */
91
147
  container?: HTMLElement;
148
+ /**
149
+ * Keep the content mounted while the tooltip is closed (instead of removing
150
+ * it), so a CSS exit animation on `data-state="closed"` can play.
151
+ * @default false
152
+ */
92
153
  forceMount?: boolean;
93
154
  };
94
155
 
95
156
  /**
96
157
  * Props for {@link Tooltip.Content} — all native `<div>` attributes plus
97
- * `forceMount` and the dismissal callbacks fired on Escape
98
- * (`onEscapeKeyDown`) and outside pointer-down (`onPointerDownOutside`).
158
+ * `forceMount` and the dismissal escape hatches. The component adds
159
+ * `role="tooltip"`, the shared `id` (for the trigger's `aria-describedby`),
160
+ * the `data-state` hook, and the pointer handlers that cancel/allow the grace
161
+ * period close.
99
162
  */
100
163
  export type TooltipContentProps = ComponentProps<"div"> & {
164
+ /**
165
+ * Keep the content mounted while the tooltip is closed (instead of removing
166
+ * it), so a CSS exit animation on `data-state="closed"` can play.
167
+ * @default false
168
+ */
101
169
  forceMount?: boolean;
170
+ /**
171
+ * Fires when `Escape` is pressed while the tooltip is open. Call
172
+ * `event.preventDefault()` to keep the tooltip open; otherwise it closes.
173
+ */
102
174
  onEscapeKeyDown?: (event: KeyboardEvent) => void;
175
+ /**
176
+ * Fires on a pointer-down outside the content. Call `event.preventDefault()`
177
+ * to keep the tooltip open; otherwise it closes.
178
+ */
103
179
  onPointerDownOutside?: (event: PointerEvent) => void;
104
180
  };
105
181
 
106
182
  /**
107
183
  * Props for {@link Tooltip.Arrow} — all native `<span>` attributes plus the
108
- * `asChild` escape hatch.
184
+ * `asChild` escape hatch. Purely presentational; position and size it with CSS.
109
185
  */
110
186
  export type TooltipArrowProps = ComponentProps<"span"> & {
187
+ /**
188
+ * Render the single child element (e.g. an `<svg>`) instead of the default
189
+ * `<span>` via the {@link Slot} pattern.
190
+ * @default false
191
+ */
111
192
  asChild?: boolean;
112
193
  };