@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
@@ -1,30 +1,58 @@
1
1
  import { ComponentProps, ReactNode, Ref } from "react";
2
2
  import { HeadingLevel } from "../types";
3
3
 
4
- /** Props shared by both control modes of `Accordion.Root`. */
5
- export type AccordionRootBaseProps = ComponentProps<"div"> & {
6
- /** Allow more than one section to be expanded at a time. */
4
+ /** Props shared by both control modes of `Accordion.Root`. `dir` is `Omit`ted
5
+ * from the native `<div>` props because it is narrowed from the DOM's
6
+ * `string` to {@link AccordionReadingDirection}; without the `Omit` the
7
+ * docs-data extractor picks the wider native declaration and silently drops
8
+ * the narrowed type and its JSDoc (§1.16). */
9
+ export type AccordionRootBaseProps = Omit<
10
+ ComponentProps<"div">,
11
+ "dir" | "defaultValue"
12
+ > & {
13
+ /** Allow more than one section to be expanded at a time. When `false`,
14
+ * opening an item collapses whichever item was previously open.
15
+ * @default false */
7
16
  multiple?: boolean;
8
- /** Layout axis, controlling arrow-key navigation. Defaults to `"vertical"`. */
17
+ /** Layout axis, controlling arrow-key navigation: `"vertical"` binds
18
+ * ArrowUp/ArrowDown, `"horizontal"` binds ArrowLeft/ArrowRight. Surfaces as
19
+ * `data-orientation` on the root.
20
+ * @default "vertical" */
9
21
  orientation?: "vertical" | "horizontal";
10
- /** Reading direction, controlling left/right key semantics. */
22
+ /** Reading direction; see {@link AccordionReadingDirection}. In `"rtl"` the
23
+ * horizontal arrow keys are mirrored. Also set as the container's `dir`
24
+ * attribute. Inherited from the nearest {@link DirectionProvider} when
25
+ * omitted, falling back to `"ltr"`. */
11
26
  dir?: AccordionReadingDirection;
12
27
  };
13
28
 
14
- /** `Accordion.Root` props for the uncontrolled (self-managed) expanded state. */
29
+ /** `Accordion.Root` props for the uncontrolled (self-managed) expanded state.
30
+ * The component owns and updates the expanded set internally, and — matching
31
+ * the Tabs contract — does **not** call `onValueChange` in this mode. */
15
32
  export type AccordionRootUncontrolledProps = AccordionRootBaseProps & {
16
- /** Initially expanded item value when uncontrolled. */
33
+ /** Value of the item expanded on first render. A single value even in
34
+ * `multiple` mode — uncontrolled accordions can seed only one open item;
35
+ * pass controlled {@link AccordionRootControlledProps.value | `value`} to
36
+ * start with several. Omit to start with everything collapsed. */
17
37
  defaultValue?: string;
38
+ /** Forbidden in uncontrolled mode — use `defaultValue`. */
18
39
  value?: never;
40
+ /** Forbidden in uncontrolled mode. */
19
41
  onValueChange?: never;
20
42
  };
21
43
 
22
- /** `Accordion.Root` props for the controlled expanded state. */
44
+ /** `Accordion.Root` props for the controlled expanded state — the parent owns
45
+ * the expanded set and the component defers every change back through the
46
+ * callback. */
23
47
  export type AccordionRootControlledProps = AccordionRootBaseProps & {
48
+ /** Forbidden in controlled mode — use `value`. */
24
49
  defaultValue?: never;
25
- /** Controlled set of expanded item values. */
50
+ /** The full set of currently expanded item values. Must be kept in sync by
51
+ * the parent via `onValueChange`. In single (`multiple={false}`) mode this
52
+ * holds at most one value. */
26
53
  value: string[];
27
- /** Called whenever the set of expanded item values changes. */
54
+ /** Called with the complete next array of expanded values whenever the user
55
+ * toggles an item. */
28
56
  onValueChange: (values: string[]) => void;
29
57
  };
30
58
 
@@ -38,20 +66,33 @@ export type AccordionRootProps =
38
66
 
39
67
  /** Props for `Accordion.Item` — a single collapsible section. */
40
68
  export type AccordionItemProps = ComponentProps<"div"> & {
41
- /** Section contents — typically a `Header` + `Content` pair. */
69
+ /** Section contents — typically an {@link AccordionHeaderProps | `Accordion.Header`}
70
+ * + {@link AccordionContentProps | `Accordion.Content`} pair. */
42
71
  children: ReactNode;
43
- /** Stable value identifying the item; auto-generated via `useId()` when omitted. */
44
- value?: string; // Optional - if not provided, useId() will generate one
72
+ /** Stable identifier matched against the root's expanded set (and against
73
+ * `value` / `defaultValue`). When omitted, a stable id is generated via
74
+ * `useId()` — fine for anonymous items whose state is never driven from
75
+ * outside. */
76
+ value?: string;
45
77
  };
46
78
 
47
79
  /** Props for `Accordion.Trigger` — the button that toggles a section. */
48
80
  export type AccordionTriggerProps<T extends HTMLElement = HTMLButtonElement> =
49
81
  Omit<ComponentProps<"button">, "disabled" | "ref"> & {
50
- /** Trigger label / contents. */
82
+ /** Trigger label / contents — the visible section heading text, plus an
83
+ * optional {@link AccordionTriggerIconProps | `Accordion.TriggerIcon`}. */
51
84
  children: ReactNode;
52
- /** Disable the trigger, removing it from keyboard navigation. */
85
+ /** Disable the trigger. Rendered as `aria-disabled` / `data-disabled`
86
+ * rather than the native `disabled` attribute, so the button stays
87
+ * focusable (discoverable by keyboard) but is excluded from arrow-key
88
+ * navigation and cannot be activated.
89
+ * @default false */
53
90
  disabled?: boolean;
54
- /** Render into the consumer's own element instead of a `<button>`. */
91
+ /** Render the consumer's own element instead of a `<button>`, merging all
92
+ * accordion ARIA attributes, event handlers, and the ref onto it via the
93
+ * {@link Slot} pattern. When combined with `disabled`, `role="button"` is
94
+ * injected so `aria-disabled` is valid on non-button children.
95
+ * @default false */
55
96
  asChild?: boolean;
56
97
  /** Ref to the rendered element. Defaults to `HTMLButtonElement`; when using
57
98
  * `asChild`, specify the child's element type (e.g. `HTMLAnchorElement`). */
@@ -60,23 +101,33 @@ export type AccordionTriggerProps<T extends HTMLElement = HTMLButtonElement> =
60
101
 
61
102
  /** Props for `Accordion.Header` — the heading wrapping a trigger. */
62
103
  export type AccordionHeaderProps = ComponentProps<"h3"> & {
63
- /** Header contents — typically an `Accordion.Trigger`. */
104
+ /** Header contents — typically an
105
+ * {@link AccordionTriggerProps | `Accordion.Trigger`}. */
64
106
  children: ReactNode;
65
- /** Heading level rendered (`h1`–`h6`). Defaults to `3`. */
107
+ /** Heading level to render (`h1`–`h6`). Choose the level that fits the
108
+ * surrounding document outline — the WAI-ARIA Accordion pattern requires
109
+ * each trigger to be wrapped in a heading at the right level.
110
+ * @default 3 */
66
111
  level?: HeadingLevel;
67
112
  };
68
113
 
69
114
  /** Props for `Accordion.Content` — the collapsible panel. */
70
115
  export type AccordionContentProps = ComponentProps<"div"> & {
71
- /** Panel contents. */
116
+ /** Panel contents, revealed when the matching trigger is expanded. */
72
117
  children: ReactNode;
73
- /** Keep the panel mounted even while collapsed. */
118
+ /** Keep the panel mounted in the DOM even while collapsed (instead of
119
+ * removing it from view with the `hidden` attribute), so CSS open/close
120
+ * transitions can run. While closed it receives `aria-hidden="true"` so
121
+ * assistive tech still ignores it; drive visibility yourself via
122
+ * `[data-state="closed"]`.
123
+ * @default false */
74
124
  forceMount?: boolean;
75
125
  };
76
126
 
77
127
  /** Props for `Accordion.TriggerIcon` — a decorative open/closed indicator. */
78
128
  export type AccordionTriggerIconProps = ComponentProps<"span"> & {
79
- /** Icon contents. */
129
+ /** Icon contents — an inline `<svg>` or any icon component. Rendered inside
130
+ * an `aria-hidden` `<span>` carrying a `data-state` open/close hook. */
80
131
  children: ReactNode;
81
132
  };
82
133
 
@@ -4,7 +4,8 @@ import { Slot } from "../Slot/index.ts";
4
4
  import { AlertProps } from "./types";
5
5
 
6
6
  /**
7
- * An assertive live region for high-priority, time-sensitive messages.
7
+ * An assertive live region for high-priority, time-sensitive messages,
8
+ * implementing the WAI-ARIA `alert` role.
8
9
  *
9
10
  * Renders a `<div role="alert">`. The `alert` role carries an implicit
10
11
  * `aria-live="assertive"` and `aria-atomic="true"`, so assistive
@@ -12,20 +13,48 @@ import { AlertProps } from "./types";
12
13
  * appears — use it for errors and other content the user must see now.
13
14
  * For non-urgent updates, reach for `Status` instead.
14
15
  *
15
- * Render the `Alert` conditionally: the live region announces content
16
- * that appears *after* it is already in the DOM. Mounting an `Alert`
17
- * that already contains its message may not be announced reliably.
16
+ * **Announce on appearance.** A live region announces content that
17
+ * changes *after* the region is already in the DOM. Render `Alert`
18
+ * conditionally so the message appears into an already-mounted tree:
19
+ *
20
+ * ```tsx
21
+ * {submitError && <Alert>{submitError}</Alert>}
22
+ * ```
23
+ *
24
+ * Mounting an `Alert` that already contains its message may not be
25
+ * announced reliably by all assistive technology.
26
+ *
27
+ * **Styling hooks.** The component emits no `data-*` attributes — it
28
+ * carries no internal state. Target the rendered element directly via
29
+ * `className` or the `[role="alert"]` selector.
18
30
  *
19
31
  * **`asChild` composition.** Renders the consumer's element instead of
20
- * a `<div>`, merging `role="alert"` and all other props in via the
21
- * {@link Slot} utility.
32
+ * a `<div>`, merging `role="alert"` and all other props onto it via the
33
+ * {@link Slot} utility. Useful when you already have a semantic element
34
+ * (e.g. `<section>`, `<aside>`) that should also function as an ARIA
35
+ * alert.
36
+ *
37
+ * **Ref forwarding.** Pass a `ref` prop to access the underlying
38
+ * `HTMLDivElement` (or, under `asChild`, the consumer's element):
39
+ *
40
+ * ```tsx
41
+ * const ref = useRef<HTMLDivElement>(null);
42
+ * <Alert ref={ref}>Something went wrong.</Alert>
43
+ * ```
44
+ *
45
+ * @extends HTMLDivElement
22
46
  *
23
47
  * @example Form error
24
48
  * ```tsx
25
49
  * {error && <Alert>{error}</Alert>}
26
50
  * ```
27
51
  *
28
- * @example asChild — keep semantic markup
52
+ * @example Inline styling via className
53
+ * ```tsx
54
+ * <Alert className="alert alert--error">Payment declined.</Alert>
55
+ * ```
56
+ *
57
+ * @example asChild — apply the alert role to a semantic element
29
58
  * ```tsx
30
59
  * <Alert asChild>
31
60
  * <section>Upload failed — try again.</section>
@@ -25,4 +25,9 @@ describe("Alert component", () => {
25
25
  expect(alert.tagName).toBe("SECTION");
26
26
  expect(alert).toHaveTextContent("Upload failed");
27
27
  });
28
+
29
+ it('sets displayName to "Alert" for React DevTools and stack traces', () => {
30
+ // Assert — an empty displayName would render it as an anonymous component.
31
+ expect(Alert.displayName).toBe("Alert");
32
+ });
28
33
  });
@@ -1,6 +1,22 @@
1
- import { ComponentProps } from "react";
1
+ import { ComponentProps, ReactNode } from "react";
2
2
 
3
- /** Props for the `Alert` — a live-region `<div role="alert">`; native `<div>` props plus `asChild`. */
3
+ /**
4
+ * Props for {@link Alert} — all native `<div>` attributes plus the `asChild`
5
+ * escape hatch.
6
+ *
7
+ * The `role` attribute is managed internally and set to `"alert"` regardless
8
+ * of what is passed; there is no need to supply it.
9
+ */
4
10
  export type AlertProps = ComponentProps<"div"> & {
11
+ /**
12
+ * Renders the child element instead of a native `<div>`, merging
13
+ * `role="alert"` and all other props onto it via {@link Slot}.
14
+ * Useful when you already have a semantic element (e.g. `<section>`,
15
+ * `<aside>`) that should also function as an ARIA alert.
16
+ * @default false
17
+ */
5
18
  asChild?: boolean;
19
+ /** The live-region content. Injected after the region is already in the
20
+ * DOM so that assistive technology announces the change. */
21
+ children?: ReactNode;
6
22
  };
@@ -18,10 +18,37 @@ import {
18
18
  * {@link Avatar.Image | `Avatar.Image`} and
19
19
  * {@link Avatar.Fallback | `Avatar.Fallback`}.
20
20
  *
21
+ * **Load-state model.** Root holds the single source of truth for the image's
22
+ * {@link AvatarImageLoadingStatus} in internal state, starting at `"idle"`.
23
+ * The child `Avatar.Image` reports transitions (`"loading"` → `"loaded"` /
24
+ * `"error"`) up through context; `Avatar.Fallback` reads that status to decide
25
+ * whether to render. There is no controlled/uncontrolled split — status is
26
+ * always driven by the image's own load lifecycle.
27
+ *
21
28
  * **Styling hooks.** `data-status="idle" | "loading" | "loaded" | "error"`.
22
29
  *
23
- * **`asChild` prop.** Pass `asChild` to render the consumer's own element as
24
- * the container, with the `data-status` hook merged in.
30
+ * **`asChild` composition.** Pass `asChild` to render the consumer's own
31
+ * element as the container instead of the native `<span>`, with the
32
+ * `data-status` hook merged in via the {@link Slot} pattern.
33
+ *
34
+ * @extends HTMLSpanElement
35
+ *
36
+ * @example Image with a generated-initials fallback
37
+ * ```tsx
38
+ * <Avatar.Root>
39
+ * <Avatar.Image src={user.avatarUrl} alt={user.name} />
40
+ * <Avatar.Fallback>{initials(user.name)}</Avatar.Fallback>
41
+ * </Avatar.Root>
42
+ * ```
43
+ *
44
+ * @example Fallback only (no image source)
45
+ * ```tsx
46
+ * <Avatar.Root>
47
+ * <Avatar.Fallback>
48
+ * <UserIcon aria-hidden />
49
+ * </Avatar.Fallback>
50
+ * </Avatar.Root>
51
+ * ```
25
52
  */
26
53
  export function AvatarRoot({
27
54
  asChild = false,
@@ -45,19 +72,39 @@ export function AvatarRoot({
45
72
  );
46
73
  }
47
74
 
48
- /** @internal */
75
+ // Runtime-dead: the compound alias below (same object via Object.assign)
76
+ // overwrites this to "Avatar" at load, so the value is never observable. The
77
+ // assignment stays because it declares `displayName` on `typeof AvatarRoot`,
78
+ // which TAvatarCompound extends.
79
+ // Stryker disable next-line StringLiteral: overwritten by the compound alias — an equivalent mutant.
49
80
  AvatarRoot.displayName = "AvatarRoot";
50
81
 
51
82
  /**
52
83
  * The image of an Avatar — an `<img>` that reports its load lifecycle to the
53
84
  * parent {@link Avatar.Root}.
54
85
  *
86
+ * **Cache-hit safe.** The internal callback ref inspects `naturalWidth` on
87
+ * attach, so an image already decoded in the browser cache (whose `load` event
88
+ * fires before React can bind `onLoad`) is still reported as `"loaded"`. Every
89
+ * other image reports `"loading"` until its `load` / `error` event resolves it.
90
+ *
55
91
  * **Styling hooks.** `data-status` mirrors the root's status. The image stays
56
- * mounted on error; hide a broken image with CSS, e.g.
92
+ * mounted on error rather than unmounting; hide a broken image with CSS, e.g.
57
93
  * `img:not([data-status="loaded"]) { display: none }`.
58
94
  *
59
- * **`asChild` prop.** Pass `asChild` to render the consumer's own `<img>`,
60
- * with the load handlers, ref, and `data-status` hook merged in.
95
+ * **`asChild` composition.** Pass `asChild` to render the consumer's own
96
+ * `<img>`, with the load handlers, ref, and `data-status` hook merged in via
97
+ * the {@link Slot} pattern.
98
+ *
99
+ * @extends HTMLImageElement
100
+ *
101
+ * @example
102
+ * ```tsx
103
+ * <Avatar.Root>
104
+ * <Avatar.Image src="/ada.png" alt="Ada Lovelace" />
105
+ * <Avatar.Fallback>AL</Avatar.Fallback>
106
+ * </Avatar.Root>
107
+ * ```
61
108
  *
62
109
  * @throws if rendered outside an `Avatar.Root`.
63
110
  */
@@ -92,14 +139,28 @@ AvatarImage.displayName = "AvatarImage";
92
139
 
93
140
  /**
94
141
  * The fallback of an Avatar — a `<span>` shown while the parent
95
- * {@link Avatar.Root}'s image is anything other than `"loaded"` (missing,
96
- * loading, or failed). Once the image loads, the fallback unmounts.
142
+ * {@link Avatar.Root}'s image is anything other than `"loaded"` (i.e. `"idle"`,
143
+ * `"loading"`, or `"error"`). Once the image loads, the fallback returns `null`
144
+ * and unmounts. Its children are the consumer's stand-in content — typically
145
+ * generated initials or a placeholder silhouette icon.
146
+ *
147
+ * Pass {@link AvatarFallbackProps.delayMs | `delayMs`} to withhold the fallback
148
+ * for that many milliseconds after mount, avoiding a flash of fallback content
149
+ * when the image loads quickly. Omit it to render the fallback straight away.
97
150
  *
98
- * Pass `delayMs` to withhold the fallback for that many milliseconds after
99
- * mount, avoiding a flash of fallback content when the image loads quickly.
151
+ * **`asChild` composition.** Pass `asChild` to render the consumer's own
152
+ * element as the fallback instead of the native `<span>`, with the
153
+ * `data-status` hook merged in via the {@link Slot} pattern.
100
154
  *
101
- * **`asChild` prop.** Pass `asChild` to render the consumer's own element as
102
- * the fallback, with the `data-status` hook merged in.
155
+ * @extends HTMLSpanElement
156
+ *
157
+ * @example Deferred to avoid a flash on fast connections
158
+ * ```tsx
159
+ * <Avatar.Root>
160
+ * <Avatar.Image src={user.avatarUrl} alt={user.name} />
161
+ * <Avatar.Fallback delayMs={600}>{initials(user.name)}</Avatar.Fallback>
162
+ * </Avatar.Root>
163
+ * ```
103
164
  *
104
165
  * @throws if rendered outside an `Avatar.Root`.
105
166
  */
@@ -113,6 +174,9 @@ export function AvatarFallback({
113
174
  const [delayElapsed, setDelayElapsed] = useState(delayMs === undefined);
114
175
 
115
176
  useEffect(() => {
177
+ // Stryker disable next-line all: when delayMs is undefined, delayElapsed is
178
+ // already initialised true, so skipping vs. running this branch only sets a
179
+ // timer whose setDelayElapsed(true) is a no-op — no observable difference.
116
180
  if (delayMs === undefined) {
117
181
  return;
118
182
  }
@@ -147,9 +211,29 @@ export type TAvatarCompound = typeof AvatarRoot & {
147
211
  * Headless, accessible **Avatar** — a compound component for a user image
148
212
  * with a graceful fallback. Zero styles ship.
149
213
  *
150
- * - {@link Avatar.Root | `Avatar.Root`} — container, owns loading status.
151
- * - {@link Avatar.Image | `Avatar.Image`} — the `<img>`, reports its status.
152
- * - {@link Avatar.Fallback | `Avatar.Fallback`} — shown until the image loads.
214
+ * `Avatar` is both callable (an alias of {@link AvatarRoot | `Avatar.Root`})
215
+ * and carries its sub-components as static properties.
216
+ *
217
+ * - {@link Avatar.Root | `Avatar.Root`} — `<span>` container, owns the image
218
+ * loading status, context provider.
219
+ * - {@link Avatar.Image | `Avatar.Image`} — the `<img>`, reports its load
220
+ * status (cache-hit safe).
221
+ * - {@link Avatar.Fallback | `Avatar.Fallback`} — stand-in content shown until
222
+ * the image loads, with an optional `delayMs` anti-flash guard.
223
+ *
224
+ * @example Minimal usage
225
+ * ```tsx
226
+ * import { Avatar } from "@primitiv-ui/react";
227
+ *
228
+ * <Avatar.Root>
229
+ * <Avatar.Image src={user.avatarUrl} alt={user.name} />
230
+ * <Avatar.Fallback>{initials(user.name)}</Avatar.Fallback>
231
+ * </Avatar.Root>
232
+ * ```
233
+ *
234
+ * @see {@link AvatarRoot} for the load-state model and `asChild`.
235
+ * @see {@link AvatarImage} for cache-hit handling and error behaviour.
236
+ * @see {@link AvatarFallback} for the `delayMs` anti-flash guard.
153
237
  */
154
238
  const AvatarCompound: TAvatarCompound = Object.assign(AvatarRoot, {
155
239
  Root: AvatarRoot,
@@ -1,6 +1,7 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
 
3
3
  import { Avatar } from "../Avatar";
4
+ import { AvatarContext } from "../AvatarContext";
4
5
 
5
6
  describe("Avatar basic rendering", () => {
6
7
  it("renders Avatar.Root as a <span>", () => {
@@ -11,4 +12,13 @@ describe("Avatar basic rendering", () => {
11
12
  // Assert
12
13
  expect(root.tagName).toBe("SPAN");
13
14
  });
15
+
16
+ it("sets a displayName on the compound, sub-components, and context", () => {
17
+ // Assert — empty displayNames would render each as anonymous in DevTools.
18
+ // Root aliases the compound (Object.assign), so its name is "Avatar".
19
+ expect(Avatar.displayName).toBe("Avatar");
20
+ expect(Avatar.Image.displayName).toBe("AvatarImage");
21
+ expect(Avatar.Fallback.displayName).toBe("AvatarFallback");
22
+ expect(AvatarContext.displayName).toBe("AvatarContext");
23
+ });
14
24
  });
@@ -71,5 +71,35 @@ describe("Avatar fallback", () => {
71
71
  // Assert — shown once the delay elapses
72
72
  expect(screen.getByText("AL")).toBeInTheDocument();
73
73
  });
74
+
75
+ it("resets the timer when delayMs changes", () => {
76
+ // Arrange — start a 300ms delay...
77
+ const { rerender } = render(
78
+ <Avatar.Root>
79
+ <Avatar.Fallback delayMs={300}>AL</Avatar.Fallback>
80
+ </Avatar.Root>,
81
+ );
82
+
83
+ // Act — ...then extend it to 1000ms before the first delay elapses. The
84
+ // effect must clear the old timer and start a new one; a stale timer (no
85
+ // cleanup) or a frozen dependency ([] deps) would fire at 300ms.
86
+ rerender(
87
+ <Avatar.Root>
88
+ <Avatar.Fallback delayMs={1000}>AL</Avatar.Fallback>
89
+ </Avatar.Root>,
90
+ );
91
+ act(() => {
92
+ vi.advanceTimersByTime(300);
93
+ });
94
+
95
+ // Assert — still withheld at 300ms because the delay was reset to 1000ms
96
+ expect(screen.queryByText("AL")).not.toBeInTheDocument();
97
+
98
+ // ...and shown once the new delay elapses
99
+ act(() => {
100
+ vi.advanceTimersByTime(700);
101
+ });
102
+ expect(screen.getByText("AL")).toBeInTheDocument();
103
+ });
74
104
  });
75
105
  });
@@ -21,6 +21,41 @@ describe("Avatar image loading", () => {
21
21
  expect(img).toHaveAttribute("src", "/ada.png");
22
22
  });
23
23
 
24
+ it('reports data-status="loading" on mount before any load event', () => {
25
+ // Arrange & Act — jsdom images report naturalWidth 0, so the attach-time
26
+ // inspection sees no cache hit and reports "loading" until load/error.
27
+ render(
28
+ <Avatar.Root data-testid="root">
29
+ <Avatar.Image src="/ada.png" alt="Ada" data-testid="img" />
30
+ </Avatar.Root>,
31
+ );
32
+
33
+ // Assert
34
+ expect(screen.getByTestId("root")).toHaveAttribute("data-status", "loading");
35
+ expect(screen.getByTestId("img")).toHaveAttribute("data-status", "loading");
36
+ });
37
+
38
+ it("tolerates the image leaving the tree (callback ref receives null)", () => {
39
+ // Arrange — an Avatar whose Image can be swapped out for a Fallback.
40
+ const { rerender } = render(
41
+ <Avatar.Root data-testid="root">
42
+ <Avatar.Image src="/ada.png" alt="Ada" />
43
+ </Avatar.Root>,
44
+ );
45
+
46
+ // Act — removing the Image detaches its callback ref, calling it with null.
47
+ // Without the `img === null` guard this dereferences null.naturalWidth and
48
+ // throws during the commit, failing the render.
49
+ rerender(
50
+ <Avatar.Root data-testid="root">
51
+ <Avatar.Fallback>AL</Avatar.Fallback>
52
+ </Avatar.Root>,
53
+ );
54
+
55
+ // Assert
56
+ expect(screen.getByText("AL")).toBeInTheDocument();
57
+ });
58
+
24
59
  it('reports data-status="loaded" on the Root and Image once the image loads', () => {
25
60
  // Arrange
26
61
  render(
@@ -24,16 +24,32 @@ export function useAvatarImage(
24
24
 
25
25
  const ref = useCallback(
26
26
  (img: HTMLImageElement | null) => {
27
+ // The `img === null` guard is load-bearing: without it, detaching the ref
28
+ // dereferences null.naturalWidth and throws (see the "leaving the tree"
29
+ // test, which kills the BlockStatement twin of this line). Stryker's
30
+ // vitest runner does not reliably attribute that React commit-phase throw
31
+ // for the ConditionalExpression variant, so it can't be scored despite
32
+ // being covered and defended — a runner limitation, not an equivalent
33
+ // mutant. The condition->true variant is killed by the "loading on mount"
34
+ // test.
35
+ // Stryker disable next-line ConditionalExpression
27
36
  if (img === null || reported.current) {
28
37
  return;
29
38
  }
30
39
  reported.current = true;
31
40
  setStatus(img.naturalWidth > 0 ? "loaded" : "loading");
32
41
  },
42
+ // Stryker disable next-line ArrayDeclaration: setStatus is a useState
43
+ // setter with a stable identity, so [] and [setStatus] recreate the
44
+ // callback identically — the dependency is behaviourally inert (equivalent).
33
45
  [setStatus],
34
46
  );
35
47
 
48
+ // Stryker disable next-line ArrayDeclaration: setStatus is a stable useState
49
+ // setter, so the dependency array cannot change the callback — equivalent.
36
50
  const onLoad = useCallback(() => setStatus("loaded"), [setStatus]);
51
+ // Stryker disable next-line ArrayDeclaration: setStatus is a stable useState
52
+ // setter, so the dependency array cannot change the callback — equivalent.
37
53
  const onError = useCallback(() => setStatus("error"), [setStatus]);
38
54
 
39
55
  return { ref, onLoad, onError };
@@ -16,7 +16,11 @@ export type AvatarImageLoadingStatus = "idle" | "loading" | "loaded" | "error";
16
16
  * Props for {@link Avatar.Root} — all `<span>` attributes plus `asChild`.
17
17
  */
18
18
  export type AvatarRootProps = ComponentProps<"span"> & {
19
- /** Render the consumer's own element instead of the native `<span>`. */
19
+ /**
20
+ * Render the consumer's own element instead of the native `<span>`, merging
21
+ * the `data-status` hook onto it via {@link Slot}.
22
+ * @default false
23
+ */
20
24
  asChild?: boolean;
21
25
  };
22
26
 
@@ -24,7 +28,11 @@ export type AvatarRootProps = ComponentProps<"span"> & {
24
28
  * Props for {@link Avatar.Image} — all `<img>` attributes plus `asChild`.
25
29
  */
26
30
  export type AvatarImageProps = ComponentProps<"img"> & {
27
- /** Render the consumer's own element instead of the native `<img>`. */
31
+ /**
32
+ * Render the consumer's own `<img>` instead of the native one, merging the
33
+ * load handlers, ref, and `data-status` hook onto it via {@link Slot}.
34
+ * @default false
35
+ */
28
36
  asChild?: boolean;
29
37
  };
30
38
 
@@ -39,6 +47,10 @@ export type AvatarFallbackProps = ComponentProps<"span"> & {
39
47
  * loads almost immediately. Omit to render the fallback straight away.
40
48
  */
41
49
  delayMs?: number;
42
- /** Render the consumer's own element instead of the native `<span>`. */
50
+ /**
51
+ * Render the consumer's own element instead of the native `<span>`, merging
52
+ * the `data-status` hook onto it via {@link Slot}.
53
+ * @default false
54
+ */
43
55
  asChild?: boolean;
44
56
  };