@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,223 @@
1
+ import { act, fireEvent, render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Tooltip } from "../Tooltip";
5
+
6
+ describe("Tooltip.Root — timer callbacks stay current across re-renders", () => {
7
+ it("uses the current onOpenChange for both a focus-open and a blur-close after the Provider re-renders", async () => {
8
+ const user = userEvent.setup();
9
+ const firstOnOpenChange = vi.fn();
10
+ const secondOnOpenChange = vi.fn();
11
+
12
+ function Wrapper({
13
+ onOpenChange,
14
+ }: {
15
+ onOpenChange: (open: boolean) => void;
16
+ }) {
17
+ return (
18
+ <Tooltip.Provider>
19
+ <Tooltip.Root onOpenChange={onOpenChange}>
20
+ <Tooltip.Trigger>Focusable</Tooltip.Trigger>
21
+ <Tooltip.Content>Tooltip text</Tooltip.Content>
22
+ </Tooltip.Root>
23
+ <button>Other</button>
24
+ </Tooltip.Provider>
25
+ );
26
+ }
27
+
28
+ const { rerender } = render(<Wrapper onOpenChange={firstOnOpenChange} />);
29
+ rerender(<Wrapper onOpenChange={secondOnOpenChange} />);
30
+
31
+ await user.tab();
32
+ await user.tab();
33
+
34
+ expect(firstOnOpenChange).not.toHaveBeenCalled();
35
+ expect(secondOnOpenChange).toHaveBeenCalledWith(true);
36
+ expect(secondOnOpenChange).toHaveBeenCalledWith(false);
37
+ });
38
+ });
39
+
40
+ describe("Tooltip.Root — timer coordination (fake timers)", () => {
41
+ beforeEach(() => {
42
+ vi.useFakeTimers();
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.useRealTimers();
47
+ });
48
+
49
+ it("opens synchronously when the effective delay is 0, not via a scheduled timer", () => {
50
+ render(
51
+ <Tooltip.Provider delayDuration={0}>
52
+ <Tooltip.Root>
53
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
54
+ <Tooltip.Content>Tooltip text</Tooltip.Content>
55
+ </Tooltip.Root>
56
+ </Tooltip.Provider>,
57
+ );
58
+
59
+ fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
60
+
61
+ expect(screen.getByRole("tooltip")).toBeInTheDocument();
62
+ });
63
+
64
+ it("cancels a pending hover-delay open timer when the trigger is focused before it elapses", () => {
65
+ const onOpenChange = vi.fn();
66
+
67
+ render(
68
+ <Tooltip.Provider delayDuration={1000}>
69
+ <Tooltip.Root onOpenChange={onOpenChange}>
70
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
71
+ <Tooltip.Content>Tooltip text</Tooltip.Content>
72
+ </Tooltip.Root>
73
+ </Tooltip.Provider>,
74
+ );
75
+
76
+ fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
77
+ fireEvent.focus(screen.getByRole("button", { name: "Hover me" }));
78
+
79
+ expect(onOpenChange).toHaveBeenCalledTimes(1);
80
+
81
+ act(() => {
82
+ vi.advanceTimersByTime(1000);
83
+ });
84
+
85
+ expect(onOpenChange).toHaveBeenCalledTimes(1);
86
+ });
87
+
88
+ it("cancels a pending grace-period timer when the tooltip closes immediately during the grace window", () => {
89
+ const onOpenChange = vi.fn();
90
+
91
+ // No Tooltip.Content here: its document-level Escape listener would also
92
+ // fire and call closeImmediate a second time, muddying the call count
93
+ // this test cares about. The trigger's own Escape handler is enough to
94
+ // exercise closeImmediate while a grace-period timer is pending.
95
+ render(
96
+ <Tooltip.Provider delayDuration={0}>
97
+ <Tooltip.Root onOpenChange={onOpenChange}>
98
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
99
+ </Tooltip.Root>
100
+ </Tooltip.Provider>,
101
+ );
102
+
103
+ fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
104
+ expect(onOpenChange).toHaveBeenCalledTimes(1);
105
+
106
+ // Leaving the trigger starts the grace-period close timer...
107
+ fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
108
+ // ...then Escape closes immediately, which must cancel that pending timer.
109
+ fireEvent.keyDown(screen.getByRole("button", { name: "Hover me" }), {
110
+ key: "Escape",
111
+ });
112
+
113
+ expect(onOpenChange).toHaveBeenCalledTimes(2);
114
+
115
+ act(() => {
116
+ vi.advanceTimersByTime(100);
117
+ });
118
+
119
+ expect(onOpenChange).toHaveBeenCalledTimes(2);
120
+ });
121
+
122
+ it("cancels the grace-period close timer when the pointer moves into the content", () => {
123
+ render(
124
+ <Tooltip.Provider delayDuration={0}>
125
+ <Tooltip.Root>
126
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
127
+ <Tooltip.Content>Tooltip text</Tooltip.Content>
128
+ </Tooltip.Root>
129
+ </Tooltip.Provider>,
130
+ );
131
+
132
+ fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
133
+ expect(screen.getByRole("tooltip")).toBeInTheDocument();
134
+
135
+ fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
136
+ fireEvent.pointerEnter(screen.getByRole("tooltip"));
137
+
138
+ act(() => {
139
+ vi.advanceTimersByTime(100);
140
+ });
141
+
142
+ expect(screen.getByRole("tooltip")).toBeInTheDocument();
143
+ });
144
+
145
+ it("closes after the grace period once the pointer leaves the trigger without re-entering the content", () => {
146
+ render(
147
+ <Tooltip.Provider delayDuration={0}>
148
+ <Tooltip.Root>
149
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
150
+ <Tooltip.Content>Tooltip text</Tooltip.Content>
151
+ </Tooltip.Root>
152
+ </Tooltip.Provider>,
153
+ );
154
+
155
+ fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
156
+ expect(screen.getByRole("tooltip")).toBeInTheDocument();
157
+
158
+ fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
159
+
160
+ // Still open right up until the grace period elapses.
161
+ expect(screen.getByRole("tooltip")).toBeInTheDocument();
162
+
163
+ act(() => {
164
+ vi.advanceTimersByTime(100);
165
+ });
166
+
167
+ expect(screen.queryByRole("tooltip")).toBeNull();
168
+ });
169
+
170
+ it("cancels a pending hover-delay open timer when the pointer leaves before it elapses, so the tooltip does not open later", () => {
171
+ render(
172
+ <Tooltip.Provider delayDuration={500}>
173
+ <Tooltip.Root>
174
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
175
+ <Tooltip.Content>Tooltip text</Tooltip.Content>
176
+ </Tooltip.Root>
177
+ </Tooltip.Provider>,
178
+ );
179
+
180
+ fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
181
+ fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
182
+
183
+ act(() => {
184
+ vi.advanceTimersByTime(500);
185
+ });
186
+
187
+ expect(screen.queryByRole("tooltip")).toBeNull();
188
+ });
189
+
190
+ it("uses the current onOpenChange callback for a grace-period close after a re-render", () => {
191
+ const firstOnOpenChange = vi.fn();
192
+ const secondOnOpenChange = vi.fn();
193
+
194
+ function Wrapper({
195
+ onOpenChange,
196
+ }: {
197
+ onOpenChange: (open: boolean) => void;
198
+ }) {
199
+ return (
200
+ <Tooltip.Provider delayDuration={0}>
201
+ <Tooltip.Root onOpenChange={onOpenChange}>
202
+ <Tooltip.Trigger>Hover me</Tooltip.Trigger>
203
+ <Tooltip.Content>Tooltip text</Tooltip.Content>
204
+ </Tooltip.Root>
205
+ </Tooltip.Provider>
206
+ );
207
+ }
208
+
209
+ const { rerender } = render(<Wrapper onOpenChange={firstOnOpenChange} />);
210
+ rerender(<Wrapper onOpenChange={secondOnOpenChange} />);
211
+
212
+ fireEvent.pointerEnter(screen.getByRole("button", { name: "Hover me" }));
213
+ fireEvent.pointerLeave(screen.getByRole("button", { name: "Hover me" }));
214
+
215
+ act(() => {
216
+ vi.advanceTimersByTime(100);
217
+ });
218
+
219
+ expect(firstOnOpenChange).not.toHaveBeenCalled();
220
+ expect(secondOnOpenChange).toHaveBeenCalledWith(true);
221
+ expect(secondOnOpenChange).toHaveBeenCalledWith(false);
222
+ });
223
+ });
@@ -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
+ });
@@ -29,6 +29,9 @@ export function useTooltipContent({
29
29
  useEffect(() => {
30
30
  if (!open) return;
31
31
  const handlePointerDown = (event: PointerEvent) => {
32
+ // Stryker disable next-line OptionalChaining: equivalent — this listener
33
+ // is only attached while `open` is true, and the Content `<div>` (and
34
+ // therefore internalRef.current) is always mounted whenever open is true.
32
35
  if (internalRef.current?.contains(event.target as Node)) return;
33
36
  onPointerDownOutside?.(event);
34
37
  if (event.defaultPrevented) return;
@@ -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
 
@@ -14,21 +14,59 @@ export function useTooltipProvider({
14
14
  const [isOpenGlobally, setIsOpenGlobally] = useState(false);
15
15
  const skipDelayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
16
16
 
17
- const onOpenGlobally = useCallback(() => {
18
- if (skipDelayTimerRef.current !== null) {
19
- clearTimeout(skipDelayTimerRef.current);
20
- skipDelayTimerRef.current = null;
21
- }
22
- setIsOpenGlobally(true);
23
- }, []);
17
+ const onOpenGlobally = useCallback(
18
+ () => {
19
+ // Stryker disable next-line ConditionalExpression: equivalent —
20
+ // clearTimeout(null) is a spec'd no-op, so forcing this guard true is
21
+ // harmless when no skip-delay timer is pending; the false / never-clear
22
+ // variant is killed by the "clears a pending skip-delay timer when a
23
+ // tooltip opens again within the skip window" test.
24
+ if (skipDelayTimerRef.current !== null) {
25
+ clearTimeout(skipDelayTimerRef.current);
26
+ skipDelayTimerRef.current = null;
27
+ }
28
+ setIsOpenGlobally(true);
29
+ },
30
+ // Stryker disable next-line ArrayDeclaration: equivalent — no dependency to freeze.
31
+ [],
32
+ );
24
33
 
25
34
  const onCloseGlobally = useCallback(() => {
35
+ // Cancel before re-scheduling. The ref tracks only the newest timer, so
36
+ // overwriting it without cancelling orphans the previous one: it survives,
37
+ // fires unobserved, and can no longer be cancelled by a later open. Two
38
+ // closes in a row is not hypothetical — `closeImmediate` is unguarded and
39
+ // both Escape and blur are wired straight to it, so pressing Escape and then
40
+ // tabbing away leaks a timer. `clearTimeout(undefined)` is a spec'd no-op, so
41
+ // this needs no guard for the first close.
42
+ clearTimeout(skipDelayTimerRef.current ?? undefined);
26
43
  skipDelayTimerRef.current = setTimeout(() => {
27
44
  setIsOpenGlobally(false);
28
45
  skipDelayTimerRef.current = null;
29
46
  }, skipDelayDuration);
30
47
  }, [skipDelayDuration]);
31
48
 
49
+ // The skip-delay reset is a raw `setTimeout`, so it outlives the
50
+ // component it was scheduled from unless explicitly cancelled — without
51
+ // this, an in-flight timer fires after unmount and can throw once its
52
+ // surrounding environment (e.g. a test's jsdom `window`) is gone.
53
+ useEffect(
54
+ () => {
55
+ return () => {
56
+ // Stryker disable next-line ConditionalExpression: equivalent —
57
+ // clearTimeout(null) is a spec'd no-op, so forcing this guard true is
58
+ // harmless when no skip-delay timer is pending; the false / never-clear
59
+ // variant is killed by the "clears the pending skip-delay timeout on
60
+ // unmount" test.
61
+ if (skipDelayTimerRef.current !== null) {
62
+ clearTimeout(skipDelayTimerRef.current);
63
+ }
64
+ };
65
+ },
66
+ // Stryker disable next-line ArrayDeclaration: equivalent — no dependency to freeze (mount/unmount only).
67
+ [],
68
+ );
69
+
32
70
  const contextValue: TooltipProviderContextValue = {
33
71
  delayDuration,
34
72
  skipDelayDuration,
@@ -31,6 +31,11 @@ export function useTooltipRoot({
31
31
  const gracePeriodTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
32
32
 
33
33
  const openImmediate = useCallback(() => {
34
+ // Stryker disable next-line ConditionalExpression: equivalent —
35
+ // clearTimeout(null) is a spec'd no-op, so forcing this guard true is
36
+ // harmless when no open timer is pending; the false / never-clear variant
37
+ // is killed by the "cancels a pending hover-delay open timer when the
38
+ // trigger is focused before it elapses" test.
34
39
  if (openTimerRef.current !== null) {
35
40
  clearTimeout(openTimerRef.current);
36
41
  openTimerRef.current = null;
@@ -52,6 +57,11 @@ export function useTooltipRoot({
52
57
  }, [openImmediate, effectiveDelayDuration, providerCtx.isOpenGlobally]);
53
58
 
54
59
  const closeImmediate = useCallback(() => {
60
+ // Stryker disable next-line ConditionalExpression: equivalent —
61
+ // clearTimeout(null) is a spec'd no-op, so forcing this guard true is
62
+ // harmless when no grace timer is pending; the false / never-clear variant
63
+ // is killed by the "cancels a pending grace-period timer when the tooltip
64
+ // closes immediately during the grace window" test.
55
65
  if (gracePeriodTimerRef.current !== null) {
56
66
  clearTimeout(gracePeriodTimerRef.current);
57
67
  gracePeriodTimerRef.current = null;
@@ -60,14 +70,28 @@ export function useTooltipRoot({
60
70
  providerCtx.onCloseGlobally();
61
71
  }, [setOpen, providerCtx]);
62
72
 
63
- const cancelGrace = useCallback(() => {
64
- if (gracePeriodTimerRef.current !== null) {
65
- clearTimeout(gracePeriodTimerRef.current);
66
- gracePeriodTimerRef.current = null;
67
- }
68
- }, []);
73
+ const cancelGrace = useCallback(
74
+ () => {
75
+ // Stryker disable next-line ConditionalExpression: equivalent —
76
+ // clearTimeout(null) is a spec'd no-op, so forcing this guard true is
77
+ // harmless when no grace timer is pending; the false / never-clear
78
+ // variant is killed by the "cancels the grace-period close timer when
79
+ // the pointer moves into the content" test.
80
+ if (gracePeriodTimerRef.current !== null) {
81
+ clearTimeout(gracePeriodTimerRef.current);
82
+ gracePeriodTimerRef.current = null;
83
+ }
84
+ },
85
+ // Stryker disable next-line ArrayDeclaration: equivalent — no dependency to freeze.
86
+ [],
87
+ );
69
88
 
70
89
  const closeWithGrace = useCallback(() => {
90
+ // Stryker disable next-line ConditionalExpression: equivalent —
91
+ // clearTimeout(null) is a spec'd no-op, so forcing this guard true is
92
+ // harmless when no open timer is pending; the false / never-clear variant
93
+ // is killed by the "cancels a pending hover-delay open timer when the
94
+ // pointer leaves before it elapses" test.
71
95
  if (openTimerRef.current !== null) {
72
96
  clearTimeout(openTimerRef.current);
73
97
  openTimerRef.current = null;
@@ -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
  };
@@ -56,7 +56,7 @@ Passing both `default*` and the controlled prop is a type error.
56
56
 
57
57
  ```ts
58
58
  // Uncontrolled
59
- <Tree.Root defaultExpandedValues={["src"]} onExpandedChange={(v) => …}>
59
+ <Tree.Root defaultExpandedValues={["src"]} onExpandedChange={(v) => ...}>
60
60
 
61
61
  // Controlled
62
62
  <Tree.Root expandedValues={values} onExpandedChange={setValues}>
@@ -70,14 +70,14 @@ The selection shape depends on `selectionMode`:
70
70
 
71
71
  ```ts
72
72
  // Single (default) — selectedValue is `string | null`
73
- <Tree.Root defaultSelectedValue="readme" onSelectedValueChange={(v) => …}>
73
+ <Tree.Root defaultSelectedValue="readme" onSelectedValueChange={(v) => ...}>
74
74
  <Tree.Root selectedValue={v} onSelectedValueChange={setV}>
75
75
 
76
76
  // Multiple — selectedValues is `string[]`
77
77
  <Tree.Root
78
78
  selectionMode="multiple"
79
79
  defaultSelectedValues={["readme", "index"]}
80
- onSelectedValuesChange={(v) => …}
80
+ onSelectedValuesChange={(v) => ...}
81
81
  >
82
82
  <Tree.Root
83
83
  selectionMode="multiple"
@@ -187,7 +187,7 @@ styles.
187
187
  > [role="treeitem"][data-depth="2"] > .row {
188
188
  > padding-inline-start: 3rem;
189
189
  > }
190
- > /* …extend as deep as your tree can go */
190
+ > /* ...extend as deep as your tree can go */
191
191
  > ```
192
192
  >
193
193
  > **2. Set a CSS variable inline from the render layer** — the
@@ -223,6 +223,70 @@ styles.
223
223
  > }
224
224
  > ```
225
225
 
226
+ ## Wrapping Tree in a styled layer
227
+
228
+ `Tree.Branch` finds its control and content by **inspecting its children before
229
+ rendering them**, so it sees the component you wrote — not what that component
230
+ renders. A styling layer that wraps each part in its own component to attach
231
+ classes is therefore invisible to it, and the branch throws:
232
+
233
+ ```
234
+ A Tree.Branch must contain a <Tree.BranchControl>.
235
+ ```
236
+
237
+ Set `TREE_PART` on the wrapper and it partitions like the part it renders:
238
+
239
+ ```tsx
240
+ import { TREE_PART, Tree } from "@primitiv-ui/react";
241
+
242
+ function StyledBranchControl(props: TreeBranchControlProps) {
243
+ return <Tree.BranchControl {...props} className="row" />;
244
+ }
245
+ StyledBranchControl[TREE_PART] = "branch-control";
246
+
247
+ function StyledBranchContent(props: TreeBranchContentProps) {
248
+ return <Tree.BranchContent {...props} className="group" />;
249
+ }
250
+ StyledBranchContent[TREE_PART] = "branch-content";
251
+ ```
252
+
253
+ The accepted names are `"branch-control"` and `"branch-content"`
254
+ (`TreePartName`). Only these two parts need it — every other part is found by
255
+ position or context, not by inspection. The one-control and one-content rules
256
+ still apply: two marked controls in a branch is still an error.
257
+
258
+ This is what the registry `tree` component does; no other primitive in the
259
+ library needs a marker, because none of the others read their children's types.
260
+
261
+ ### Indenting without `attr()`
262
+
263
+ `useTreeLevel()` returns `{ depth }` — `0` directly under `Tree.Root`, one
264
+ deeper per enclosing `BranchContent`. A styling layer needs the depth as a
265
+ *number* to apply the indent as the **row's own padding**, which is what keeps
266
+ each row's box full-width so a hover or selected band runs edge to edge rather
267
+ than starting at the nested level. Reading `data-depth` back in CSS would need
268
+ `attr(data-depth type(<integer>))` — Chrome 133+ / Safari 18.2+, unsupported in
269
+ Firefox — so the depth is exposed here instead.
270
+
271
+ ```tsx
272
+ function StyledItem(props: TreeItemProps) {
273
+ const { depth } = useTreeLevel();
274
+ return (
275
+ <Tree.Item
276
+ {...props}
277
+ style={{ "--depth": depth } as CSSProperties}
278
+ className="row"
279
+ />
280
+ );
281
+ }
282
+ ```
283
+
284
+ ```css
285
+ .row {
286
+ padding-inline-start: calc(var(--depth, 0) * 1.25rem + 0.5rem);
287
+ }
288
+ ```
289
+
226
290
  ## Selection path
227
291
 
228
292
  `Tree.Item` and `Tree.Branch` accept an optional `label` prop. It does
@@ -256,7 +320,7 @@ import { Tree } from "@primitiv-ui/react";
256
320
  import { ChevronRight } from "@primitiv-ui/icons";
257
321
 
258
322
  <Tree.Root defaultSelectedValue="index">
259
- …
323
+ ...
260
324
  <Tree.SelectionPath separator={<ChevronRight />} />
261
325
  </Tree.Root>;
262
326
  ```