@primitiv-ui/react 0.1.28 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/README.md +4 -0
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +25 -4
  7. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  8. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  9. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  10. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  11. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  12. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  13. package/src/Accordion/types.ts +72 -21
  14. package/src/Alert/Alert.tsx +36 -7
  15. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  16. package/src/Alert/types.ts +18 -2
  17. package/src/Avatar/Avatar.tsx +99 -15
  18. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  19. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  20. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  21. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  22. package/src/Avatar/types.ts +15 -3
  23. package/src/Breadcrumb/Breadcrumb.tsx +136 -18
  24. package/src/Breadcrumb/README.md +47 -0
  25. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  27. package/src/Breadcrumb/types.ts +73 -7
  28. package/src/Button/Button.tsx +2 -0
  29. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  30. package/src/Button/types.ts +21 -0
  31. package/src/Carousel/Carousel.tsx +263 -15
  32. package/src/Carousel/README.md +749 -72
  33. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  34. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  35. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  36. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  37. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  38. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  39. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +88 -0
  40. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  41. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  42. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  43. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  44. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  45. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  46. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  47. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  48. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  49. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  50. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  51. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  52. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  53. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  54. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  55. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  56. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  57. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  58. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  59. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  60. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  61. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  62. package/src/Carousel/hooks/index.ts +1 -0
  63. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  64. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  65. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  66. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  67. package/src/Carousel/loopEngine.ts +54 -0
  68. package/src/Carousel/types.ts +457 -35
  69. package/src/Checkbox/Checkbox.tsx +9 -1
  70. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  71. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  72. package/src/Checkbox/types.ts +19 -0
  73. package/src/CheckboxCard/CheckboxCard.tsx +13 -4
  74. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  75. package/src/CheckboxCard/types.ts +65 -17
  76. package/src/Collapsible/Collapsible.tsx +49 -3
  77. package/src/Collapsible/README.md +40 -0
  78. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  79. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  80. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  81. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  82. package/src/Collapsible/types.ts +89 -22
  83. package/src/ContextMenu/ContextMenu.tsx +270 -4
  84. package/src/ContextMenu/types.ts +148 -6
  85. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  86. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  87. package/src/DirectionProvider/types.ts +28 -3
  88. package/src/Divider/Divider.tsx +24 -10
  89. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  90. package/src/Divider/types.ts +10 -3
  91. package/src/Drawer/Drawer.tsx +315 -0
  92. package/src/Drawer/README.md +124 -0
  93. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  94. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  95. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  96. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  97. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  98. package/src/Drawer/index.ts +2 -0
  99. package/src/Drawer/types.ts +66 -0
  100. package/src/Dropdown/Dropdown.tsx +117 -0
  101. package/src/Dropdown/types.ts +241 -11
  102. package/src/EmptyState/EmptyState.tsx +74 -4
  103. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  104. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  105. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  106. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  107. package/src/EmptyState/types.ts +49 -6
  108. package/src/Field/Field.tsx +23 -0
  109. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  110. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  111. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  112. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  113. package/src/Field/types.ts +60 -12
  114. package/src/Fieldset/Fieldset.tsx +54 -18
  115. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  116. package/src/Fieldset/types.ts +20 -2
  117. package/src/Input/Input.tsx +14 -9
  118. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  119. package/src/Input/types.ts +19 -2
  120. package/src/InputGroup/InputGroup.tsx +11 -3
  121. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  122. package/src/InputGroup/types.ts +26 -5
  123. package/src/MillerColumns/MillerColumns.tsx +86 -19
  124. package/src/MillerColumns/types.ts +83 -27
  125. package/src/Modal/Modal.tsx +31 -4
  126. package/src/Modal/README.md +19 -10
  127. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  128. package/src/Modal/hooks/useModalContent.ts +32 -0
  129. package/src/Modal/types.ts +110 -29
  130. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  131. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  132. package/src/NavigationMenu/README.md +355 -0
  133. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  134. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  135. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  136. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  137. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  138. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  139. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  140. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  141. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  142. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  143. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  144. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  145. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  146. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  147. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  148. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  149. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  150. package/src/NavigationMenu/constants.ts +5 -0
  151. package/src/NavigationMenu/hooks/index.ts +7 -0
  152. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  153. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  154. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  155. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  156. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  157. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  158. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  159. package/src/NavigationMenu/index.ts +3 -0
  160. package/src/NavigationMenu/types.ts +259 -0
  161. package/src/NavigationMenu/utils.ts +89 -0
  162. package/src/Popover/Popover.tsx +427 -0
  163. package/src/Popover/PopoverContext.ts +13 -0
  164. package/src/Popover/README.md +106 -0
  165. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  166. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  167. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  168. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  169. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  170. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  171. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  172. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  173. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  174. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  175. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  176. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  177. package/src/Popover/hooks/index.ts +3 -0
  178. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  179. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  180. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  181. package/src/Popover/index.ts +2 -0
  182. package/src/Popover/types.ts +197 -0
  183. package/src/Portal/Portal.tsx +50 -10
  184. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  185. package/src/Portal/types.ts +5 -0
  186. package/src/Progress/Progress.tsx +33 -1
  187. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  188. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  189. package/src/Progress/types.ts +20 -4
  190. package/src/Radio/Radio.tsx +9 -1
  191. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  192. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  193. package/src/Radio/types.ts +20 -0
  194. package/src/RadioCard/RadioCard.tsx +11 -0
  195. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  196. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  197. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  198. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  199. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  200. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  201. package/src/RadioCard/types.ts +95 -22
  202. package/src/RadioGroup/RadioGroup.tsx +11 -0
  203. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  204. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  205. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  206. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  207. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  208. package/src/RadioGroup/types.ts +73 -9
  209. package/src/SegmentedControl/README.md +154 -0
  210. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  211. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  212. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  213. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  214. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  215. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  216. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  217. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  218. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  219. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  220. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  221. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  222. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  223. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  224. package/src/SegmentedControl/hooks/index.ts +2 -0
  225. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  226. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +86 -0
  227. package/src/SegmentedControl/index.ts +2 -0
  228. package/src/SegmentedControl/types.ts +138 -0
  229. package/src/Select/README.md +122 -157
  230. package/src/Select/Select.tsx +595 -68
  231. package/src/Select/SelectContext.ts +38 -0
  232. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  233. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  234. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  235. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  236. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  237. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  238. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  239. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  240. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  241. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  242. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  243. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  244. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  245. package/src/Select/__tests__/Select.placeholder.test.tsx +4 -4
  246. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  247. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  248. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  249. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  250. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  251. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  252. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  253. package/src/Select/constants.ts +8 -0
  254. package/src/Select/hooks/index.ts +2 -0
  255. package/src/Select/hooks/useSelectContent.ts +149 -0
  256. package/src/Select/hooks/useSelectRoot.ts +149 -0
  257. package/src/Select/types.ts +199 -31
  258. package/src/SkipNav/SkipNav.tsx +69 -29
  259. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  260. package/src/SkipNav/types.ts +17 -9
  261. package/src/Slider/Slider.tsx +33 -1
  262. package/src/Slider/SliderContext.ts +28 -1
  263. package/src/Slider/types.ts +157 -9
  264. package/src/Slot/Slot.tsx +94 -19
  265. package/src/Slot/__tests__/Slot.test.tsx +110 -0
  266. package/src/Slot/composeEventHandlers.ts +26 -2
  267. package/src/Slot/types.ts +16 -0
  268. package/src/Status/Status.tsx +36 -15
  269. package/src/Status/__tests__/Status.test.tsx +5 -0
  270. package/src/Status/types.ts +12 -1
  271. package/src/Switch/Switch.tsx +15 -2
  272. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  273. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  274. package/src/Switch/types.ts +49 -22
  275. package/src/Table/Table.tsx +44 -10
  276. package/src/Table/types.ts +66 -17
  277. package/src/Tabs/Tabs.tsx +8 -0
  278. package/src/Tabs/types.ts +29 -4
  279. package/src/Textarea/Textarea.tsx +9 -1
  280. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  281. package/src/Textarea/types.ts +20 -3
  282. package/src/Toggle/Toggle.tsx +51 -13
  283. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  284. package/src/Toggle/types.ts +54 -4
  285. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  286. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  287. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  288. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  289. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  290. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  291. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  292. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  293. package/src/ToggleGroup/types.ts +55 -2
  294. package/src/Tooltip/Tooltip.tsx +98 -20
  295. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  296. package/src/Tooltip/hooks/useTooltipProvider.ts +13 -1
  297. package/src/Tooltip/types.ts +89 -8
  298. package/src/Tree/Tree.tsx +187 -33
  299. package/src/Tree/types.ts +54 -12
  300. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  301. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  302. package/src/VisuallyHidden/types.ts +11 -1
  303. package/src/index.ts +4 -0
  304. package/src/test/intersectionObserverPolyfill.ts +7 -0
  305. package/src/test/resizeObserverPolyfill.ts +80 -0
  306. package/src/utils/focusable.ts +15 -0
  307. package/src/utils/index.ts +1 -0
package/src/Tree/Tree.tsx CHANGED
@@ -36,11 +36,39 @@ import type {
36
36
  * `<div role="tree">` and provides every descendant with the expansion
37
37
  * set, selection set, and roving-tabstop coordination.
38
38
  *
39
- * Expansion is controlled/uncontrolled via the `expandedValues` /
40
- * `defaultExpandedValues` pair (with `onExpandedChange`). Selection
41
- * follows the same shape under `selectionMode="single"` (default) or
42
- * `"multiple"`, the two modes statically discriminated so passing the
43
- * wrong prop pair is a type error.
39
+ * **State ownership.** Root owns two independent axes, each controllable
40
+ * or uncontrolled:
41
+ *
42
+ * - **Expansion** — which branches are open. Controlled via
43
+ * {@link TreeRootControlledExpansionProps.expandedValues | `expandedValues`}
44
+ * + {@link TreeRootControlledExpansionProps.onExpandedChange | `onExpandedChange`};
45
+ * uncontrolled via
46
+ * {@link TreeRootUncontrolledExpansionProps.defaultExpandedValues | `defaultExpandedValues`}.
47
+ * - **Selection** — which node(s) are selected, gated by
48
+ * {@link SelectionMode | `selectionMode`} (`"single"` default, or
49
+ * `"multiple"`). Each mode has its own controlled / uncontrolled
50
+ * `value` / `defaultValue` / `onChange` prop trio, statically
51
+ * discriminated so passing the wrong prop pair for the active mode is
52
+ * a type error.
53
+ *
54
+ * **Roving tabindex.** The whole tree exposes a **single tab stop** — at
55
+ * most one `treeitem` carries `tabIndex={0}` (the last-focused visible
56
+ * item, else the first non-disabled item in visible depth-first order);
57
+ * every other item is `tabIndex={-1}`. `Tab` therefore enters and leaves
58
+ * the tree once, and the arrow keys move focus *within* it. See
59
+ * {@link TreeItem} / {@link TreeBranch} for the full key table.
60
+ *
61
+ * **Context.** Root provides the `TreeContext` (expansion/selection
62
+ * state, the node registry, roving bookkeeping, path resolver) and seeds
63
+ * the `TreeLevelContext` at `{ depth: 0, parentValue: null }`, which each
64
+ * {@link TreeBranchContent} deepens for its children.
65
+ *
66
+ * **Styling hooks.**
67
+ * - `role="tree"` on the rendered container.
68
+ * - `aria-multiselectable="true"` only when `selectionMode="multiple"`.
69
+ * - `data-selection-mode="single" | "multiple"`.
70
+ *
71
+ * @extends HTMLDivElement
44
72
  *
45
73
  * @example Uncontrolled, single selection
46
74
  * ```tsx
@@ -120,18 +148,57 @@ export function TreeRoot(props: TreeRootProps): ReactElement {
120
148
  TreeRoot.displayName = "TreeRoot";
121
149
 
122
150
  /**
123
- * A leaf treeitem — a selectable, focusable node that has no children.
124
- * Renders a `<div role="treeitem">` by default; pass `asChild` to merge
125
- * Tree behaviour onto a consumer element (e.g. `<a>`).
151
+ * A leaf treeitem — a selectable, focusable node with no children.
152
+ * Renders a `<div role="treeitem">` by default; pass
153
+ * {@link TreeItemProps.asChild | `asChild`} to merge the tree behaviour
154
+ * onto a consumer element instead (e.g. an `<a>` for a navigable file).
155
+ *
156
+ * **Selection.** Clicking the item *replaces* the selection in single
157
+ * mode. In multiple mode, `Ctrl`/`Cmd`+click toggles it into or out of
158
+ * the set, and `Shift`+click selects the contiguous range (in visible
159
+ * depth-first order, disabled nodes excluded) between the previous
160
+ * selection anchor and this item.
161
+ *
162
+ * **Depth & registry.** The item reads its nesting depth and enclosing
163
+ * branch from `TreeLevelContext`, emits `aria-level={depth + 1}`, and
164
+ * registers itself (value, {@link TreeItemProps.label | `label`}, depth,
165
+ * parent) in the tree's node registry. The registry keeps the last-seen
166
+ * entry per value even after unmount, so `Tree.SelectionPath` and
167
+ * {@link useTreePath} can resolve ancestry through a collapsed branch.
168
+ *
169
+ * **Keyboard** (while focused; movement is over *visible* items in
170
+ * depth-first order, skipping collapsed subtrees and disabled nodes):
126
171
  *
127
- * Clicking the item replaces the selection in single mode and toggles
128
- * it under `Ctrl`/`Cmd` in multiple mode; `Shift+click` selects the
129
- * range between the previous click and this one.
172
+ * | Key | Behaviour |
173
+ * | ----------------------- | --------------------------------------------- |
174
+ * | `ArrowDown` / `ArrowUp` | Move focus to the next / previous visible item |
175
+ * | `ArrowLeft` | Move focus to the parent branch (no-op at root)|
176
+ * | `Home` / `End` | Focus the first / last visible item |
177
+ * | `Enter` / `Space` | Select the item |
178
+ *
179
+ * A `disabled` item is removed from the roving order and ignores
180
+ * selection, but `Home`/`End` still land on non-disabled neighbours.
181
+ *
182
+ * **Styling hooks.**
183
+ * - `role="treeitem"`, `aria-level`, `aria-selected`, and `aria-disabled`
184
+ * (omitted when not disabled).
185
+ * - `data-leaf=""` (always), `data-depth="{n}"`.
186
+ * - `data-selected=""` when selected, `data-disabled=""` when disabled
187
+ * (both omitted otherwise).
188
+ *
189
+ * @extends HTMLDivElement
130
190
  *
131
191
  * @example
132
192
  * ```tsx
133
193
  * <Tree.Item value="readme">readme</Tree.Item>
134
194
  * ```
195
+ *
196
+ * @example asChild — a navigable leaf
197
+ * ```tsx
198
+ * <Tree.Item asChild value="readme">
199
+ * <a href="/readme">readme</a>
200
+ * </Tree.Item>
201
+ * ```
135
202
  */
136
203
  export function TreeItem({
137
204
  value,
@@ -212,10 +279,43 @@ TreeItem.displayName = "TreeItem";
212
279
  * with `partitionBranchChildren`.
213
280
  *
214
281
  * The branch carries `aria-expanded`, `aria-selected`, `aria-level`,
215
- * and is labelled by its control row via `aria-labelledby` so its
216
- * accessible name is not polluted by descendant text. Content is
217
- * unmounted while collapsed unless the inner `Tree.BranchContent`
218
- * opts in to `forceMount`.
282
+ * and is labelled by its control row via `aria-labelledby` (an id
283
+ * derived from the root id + this branch's `value`), so its accessible
284
+ * name is the control text alone and not polluted by descendant text.
285
+ * Content is unmounted while collapsed unless the inner
286
+ * {@link TreeBranchContent} opts in to
287
+ * {@link TreeBranchContentProps.forceMount | `forceMount`}.
288
+ *
289
+ * **Composition.** A `Tree.Branch` must contain exactly one
290
+ * {@link TreeBranchControl} and at most one {@link TreeBranchContent};
291
+ * both are matched however deeply they are wrapped in fragments (so
292
+ * `{open && <BranchContent/>}` partitions the same as a bare element).
293
+ * Violating that throws a descriptive error during render. The branch
294
+ * provides `TreeItemContext` (its value, expanded/disabled state, and
295
+ * control id) to those two sub-components.
296
+ *
297
+ * **Keyboard** (adds branch semantics on top of the roving keys shared
298
+ * with {@link TreeItem}):
299
+ *
300
+ * | Key | Behaviour |
301
+ * | ------------ | ------------------------------------------------------------- |
302
+ * | `ArrowRight` | Expand if collapsed; if already open, move focus to first child|
303
+ * | `ArrowLeft` | Collapse if open; otherwise move focus to the parent branch |
304
+ * | `Enter` / `Space` | Toggle expansion **and** select the branch in one gesture |
305
+ *
306
+ * A `disabled` branch keeps its row and current content rendered but
307
+ * ignores expansion, selection, and the branch arrow keys, and is
308
+ * skipped by roving navigation.
309
+ *
310
+ * **Styling hooks.**
311
+ * - `role="treeitem"`, `aria-level`, `aria-expanded`, `aria-selected`,
312
+ * `aria-labelledby`, and `aria-disabled` (omitted when not disabled).
313
+ * - `data-branch=""` (always), `data-depth="{n}"`,
314
+ * `data-state="open" | "closed"`.
315
+ * - `data-selected=""` / `data-disabled=""` when applicable (omitted
316
+ * otherwise).
317
+ *
318
+ * @extends HTMLDivElement
219
319
  *
220
320
  * @example
221
321
  * ```tsx
@@ -307,11 +407,21 @@ export function TreeBranch({
307
407
  TreeBranch.displayName = "TreeBranch";
308
408
 
309
409
  /**
310
- * The clickable row of a `Tree.Branch`. Renders a `<div>` by default
311
- * (or any element via `asChild`) and is identified by the id the
312
- * surrounding branch points `aria-labelledby` at. Clicking the row
313
- * toggles the branch's expansion and selects it in a single gesture;
314
- * the inner `Tree.BranchIndicator` is decorative.
410
+ * The clickable row of a {@link TreeBranch | `Tree.Branch`}. Renders a
411
+ * `<div>` by default, or any element via
412
+ * {@link TreeBranchControlProps.asChild | `asChild`}. It carries the
413
+ * `id` that the surrounding branch points `aria-labelledby` at, so its
414
+ * text becomes the branch's accessible name — keep the label content
415
+ * here rather than on the branch.
416
+ *
417
+ * Clicking the row toggles the branch's expansion **and** selects it in
418
+ * a single gesture (with the same `Ctrl`/`Cmd` / `Shift` modifier rules
419
+ * as {@link TreeItem}); the inner {@link TreeBranchIndicator} is purely
420
+ * decorative. A disabled branch's control ignores the click. Reads its
421
+ * value / disabled state / id from the branch's `TreeItemContext`, so it
422
+ * must be rendered inside a `Tree.Branch`.
423
+ *
424
+ * @extends HTMLDivElement
315
425
  *
316
426
  * @example
317
427
  * ```tsx
@@ -356,13 +466,30 @@ export function TreeBranchControl({
356
466
  TreeBranchControl.displayName = "TreeBranchControl";
357
467
 
358
468
  /**
359
- * The nested group of items inside a `Tree.Branch`. Renders a
360
- * `<div role="group">` whose children sit at one deeper nesting level.
469
+ * The nested group of items inside a {@link TreeBranch | `Tree.Branch`}.
470
+ * Renders a `<div role="group">` and deepens the `TreeLevelContext` for
471
+ * its children (`depth + 1`, `parentValue` = the enclosing branch), so
472
+ * they emit the correct `aria-level` and register under the right parent.
473
+ *
474
+ * By default the content is **unmounted** while the branch is collapsed —
475
+ * the lean-DOM strategy. Pass
476
+ * {@link TreeBranchContentProps.forceMount | `forceMount`} to keep it
477
+ * mounted while collapsed with `aria-hidden="true"` and
478
+ * `data-state="closed"`, so CSS can animate it in and out without the
479
+ * browser tearing the subtree down.
480
+ *
481
+ * **Styling hooks.**
482
+ * - `role="group"`, `data-depth="{n}"`, `data-state="open" | "closed"`.
483
+ * - `aria-hidden="true"` only while force-mounted and collapsed.
361
484
  *
362
- * By default the content is unmounted while the branch is collapsed —
363
- * the lean DOM strategy. Pass `forceMount` to keep it mounted with
364
- * `aria-hidden="true"` and `data-state="closed"`, so CSS can animate
365
- * it in and out without the browser tearing the subtree down.
485
+ * @extends HTMLDivElement
486
+ *
487
+ * @example forceMount for CSS transitions
488
+ * ```tsx
489
+ * <Tree.BranchContent forceMount>
490
+ * <Tree.Item value="index">index.ts</Tree.Item>
491
+ * </Tree.BranchContent>
492
+ * ```
366
493
  */
367
494
  export function TreeBranchContent({
368
495
  children,
@@ -398,10 +525,15 @@ TreeBranchContent.displayName = "TreeBranchContent";
398
525
  * `"closed"` so a consumer's CSS can rotate or swap the glyph. Only
399
526
  * meaningful inside a `Tree.BranchControl`.
400
527
  *
401
- * Pass `asChild` to merge the indicator's props — including
402
- * `aria-hidden` and `data-state` — onto the supplied child element.
403
- * This is useful when passing an icon component that must receive
404
- * `data-state` directly rather than inheriting it from a wrapper:
528
+ * Pass {@link TreeBranchIndicatorProps.asChild | `asChild`} to merge the
529
+ * indicator's props — including `aria-hidden` and `data-state` — onto the
530
+ * supplied child element. This is useful when passing an icon component
531
+ * that must receive `data-state` directly rather than inheriting it from
532
+ * a wrapper. Reads its `expanded` state from the branch's
533
+ * `TreeItemContext`, so it must be rendered inside a
534
+ * {@link TreeBranchControl}.
535
+ *
536
+ * @extends HTMLSpanElement
405
537
  *
406
538
  * @example Default (wraps children in a `<span>`)
407
539
  * ```tsx
@@ -456,9 +588,24 @@ TreeBranchIndicator.displayName = "TreeBranchIndicator";
456
588
  *
457
589
  * Pass `separator` to customise the divider glyph between segments.
458
590
  *
459
- * Pass a function as `children` to take full control of rendering
460
- * (e.g. wire up router links). The function receives the resolved
461
- * paths and replaces the default markup entirely.
591
+ * Pass {@link TreeSelectionPathProps.separator | `separator`} to
592
+ * customise the divider glyph between segments, or a function as
593
+ * `children` to take full control of rendering (e.g. wire up router
594
+ * links). The function receives the resolved
595
+ * {@link TreeSelectionPathRenderProps.paths | `paths`} and replaces the
596
+ * default markup entirely.
597
+ *
598
+ * Backed by {@link useTreeSelectionPaths}, which reads ancestry from the
599
+ * persistent node registry — so trails stay resolvable even when an
600
+ * ancestor branch has collapsed and its descendants have unmounted.
601
+ *
602
+ * **Styling hooks.**
603
+ * - `data-tree-selection-path=""` on the wrapper (always).
604
+ * - `data-empty=""` on the wrapper when nothing is selected.
605
+ * - `data-tree-selection-segment=""` + `data-value` (and `data-disabled`
606
+ * when the node is disabled) on each rendered segment.
607
+ *
608
+ * @extends HTMLDivElement
462
609
  *
463
610
  * @example Default rendering
464
611
  * ```tsx
@@ -600,6 +747,13 @@ export type TreeCompound = typeof TreeRoot & {
600
747
  * </Tree.Branch>
601
748
  * </Tree.Root>;
602
749
  * ```
750
+ *
751
+ * @see {@link TreeRoot} for the expansion / selection state modes and the
752
+ * single tree-wide roving tab stop.
753
+ * @see {@link TreeItem} and {@link TreeBranch} for the keyboard tables and
754
+ * selection modifiers.
755
+ * @see {@link useTreePath} / {@link useTreeSelectionPaths} for reading a
756
+ * node's ancestry outside `Tree.SelectionPath`.
603
757
  */
604
758
  const TreeCompound: TreeCompound = Object.assign(TreeRoot, {
605
759
  Root: TreeRoot,
package/src/Tree/types.ts CHANGED
@@ -7,26 +7,37 @@ export type TreeRootBaseProps = ComponentProps<"div"> & {
7
7
 
8
8
  /** Uncontrolled expansion — initial expanded branches via `defaultExpandedValues`. */
9
9
  export type TreeRootUncontrolledExpansionProps = {
10
- /** Branch values expanded on first render when uncontrolled. */
10
+ /** Branch values expanded on first render when uncontrolled. Omit for a
11
+ * fully-collapsed tree. */
11
12
  defaultExpandedValues?: string[];
12
13
  expandedValues?: never;
14
+ /** Called with the full set of expanded branch values whenever a branch
15
+ * is toggled. Optional in uncontrolled mode. */
13
16
  onExpandedChange?: (values: string[]) => void;
14
17
  };
15
18
 
16
19
  /** Controlled expansion — the expanded set is owned by the consumer. */
17
20
  export type TreeRootControlledExpansionProps = {
18
21
  defaultExpandedValues?: never;
19
- /** The set of expanded branch values, owned by the consumer. */
22
+ /** The set of expanded branch values, owned by the consumer. Keep in
23
+ * sync via `onExpandedChange`. */
20
24
  expandedValues: string[];
25
+ /** Called with the full set of expanded branch values the user requested
26
+ * on each toggle. Required in controlled mode. */
21
27
  onExpandedChange: (values: string[]) => void;
22
28
  };
23
29
 
24
30
  /** Single-selection mode, uncontrolled — initial value via `defaultSelectedValue`. */
25
31
  export type TreeRootSingleUncontrolledSelectionProps = {
32
+ /** At most one node may be selected; see {@link SelectionMode}.
33
+ * @default "single" */
26
34
  selectionMode?: "single";
27
- /** The value selected on first render when uncontrolled. */
35
+ /** The value selected on first render when uncontrolled. `null` selects
36
+ * nothing. */
28
37
  defaultSelectedValue?: string | null;
29
38
  selectedValue?: never;
39
+ /** Called with the newly-selected value (or `null` when cleared) on each
40
+ * user selection. Optional in uncontrolled mode. */
30
41
  onSelectedValueChange?: (value: string | null) => void;
31
42
  defaultSelectedValues?: never;
32
43
  selectedValues?: never;
@@ -35,10 +46,15 @@ export type TreeRootSingleUncontrolledSelectionProps = {
35
46
 
36
47
  /** Single-selection mode, controlled — the selected value is owned by the consumer. */
37
48
  export type TreeRootSingleControlledSelectionProps = {
49
+ /** At most one node may be selected; see {@link SelectionMode}.
50
+ * @default "single" */
38
51
  selectionMode?: "single";
39
52
  defaultSelectedValue?: never;
40
- /** The selected value, owned by the consumer. */
53
+ /** The selected value, owned by the consumer (or `null` for no
54
+ * selection). Keep it in sync via `onSelectedValueChange`. */
41
55
  selectedValue: string | null;
56
+ /** Called with the value the user requested to select (or `null` when
57
+ * cleared). Required in controlled mode. */
42
58
  onSelectedValueChange: (value: string | null) => void;
43
59
  defaultSelectedValues?: never;
44
60
  selectedValues?: never;
@@ -47,10 +63,14 @@ export type TreeRootSingleControlledSelectionProps = {
47
63
 
48
64
  /** Multiple-selection mode, uncontrolled — initial values via `defaultSelectedValues`. */
49
65
  export type TreeRootMultipleUncontrolledSelectionProps = {
66
+ /** Many nodes may be selected via `Ctrl`/`Cmd`+click and `Shift`+click
67
+ * ranges; see {@link SelectionMode}. */
50
68
  selectionMode: "multiple";
51
69
  /** The values selected on first render when uncontrolled. */
52
70
  defaultSelectedValues?: string[];
53
71
  selectedValues?: never;
72
+ /** Called with the full selected set (in selection order) on each
73
+ * change. Optional in uncontrolled mode. */
54
74
  onSelectedValuesChange?: (values: string[]) => void;
55
75
  defaultSelectedValue?: never;
56
76
  selectedValue?: never;
@@ -59,10 +79,15 @@ export type TreeRootMultipleUncontrolledSelectionProps = {
59
79
 
60
80
  /** Multiple-selection mode, controlled — the selected values are owned by the consumer. */
61
81
  export type TreeRootMultipleControlledSelectionProps = {
82
+ /** Many nodes may be selected via `Ctrl`/`Cmd`+click and `Shift`+click
83
+ * ranges; see {@link SelectionMode}. */
62
84
  selectionMode: "multiple";
63
85
  defaultSelectedValues?: never;
64
- /** The selected values, owned by the consumer. */
86
+ /** The selected values, owned by the consumer. Keep in sync via
87
+ * `onSelectedValuesChange`. */
65
88
  selectedValues: string[];
89
+ /** Called with the full selected set (in selection order) the user
90
+ * requested. Required in controlled mode. */
66
91
  onSelectedValuesChange: (values: string[]) => void;
67
92
  defaultSelectedValue?: never;
68
93
  selectedValue?: never;
@@ -86,7 +111,9 @@ export type TreeRootProps = TreeRootBaseProps &
86
111
 
87
112
  /** Props for `Tree.Item` — a selectable, focusable leaf treeitem. */
88
113
  export type TreeItemProps = ComponentProps<"div"> & {
89
- /** Stable identifier for this item, unique within the tree. */
114
+ /** Stable identifier for this item, unique within the tree. Links the
115
+ * item to selection / expansion state and to its
116
+ * {@link TreePathSegment} in a resolved path. */
90
117
  value: string;
91
118
  /**
92
119
  * Optional display label for this item. Stored alongside the value
@@ -95,16 +122,23 @@ export type TreeItemProps = ComponentProps<"div"> & {
95
122
  * Has no effect on what `Tree.Item` renders.
96
123
  */
97
124
  label?: string;
98
- /** Disable selection and remove the item from roving navigation. */
125
+ /** Disable selection and remove the item from roving navigation
126
+ * (`Home`/`End` still skip past it to a non-disabled neighbour).
127
+ * @default false */
99
128
  disabled?: boolean;
100
- /** Render the item as the supplied child element instead of `<div>`. */
129
+ /** Render the item as the supplied child element instead of `<div>`,
130
+ * merging the treeitem behaviour onto it via the {@link Slot} pattern.
131
+ * The child must accept a `ref`.
132
+ * @default false */
101
133
  asChild?: boolean;
102
134
  children: ReactNode;
103
135
  };
104
136
 
105
137
  /** Props for `Tree.Branch` — an expandable treeitem with nested children. */
106
138
  export type TreeBranchProps = Omit<ComponentProps<"div">, "ref"> & {
107
- /** Stable identifier for this branch, unique within the tree. */
139
+ /** Stable identifier for this branch, unique within the tree. Also seeds
140
+ * the `parentValue` of its {@link TreeBranchContent}'s children and the
141
+ * derived `aria-labelledby` control id. */
108
142
  value: string;
109
143
  /**
110
144
  * Optional display label for this branch. Stored alongside the value
@@ -116,6 +150,7 @@ export type TreeBranchProps = Omit<ComponentProps<"div">, "ref"> & {
116
150
  /**
117
151
  * Disable selection, expansion-toggling, and roving navigation for
118
152
  * this branch. The branch and its current content remain rendered.
153
+ * @default false
119
154
  */
120
155
  disabled?: boolean;
121
156
  children: ReactNode;
@@ -123,7 +158,10 @@ export type TreeBranchProps = Omit<ComponentProps<"div">, "ref"> & {
123
158
 
124
159
  /** Props for `Tree.BranchControl` — the branch's selectable, toggling control row. */
125
160
  export type TreeBranchControlProps = ComponentProps<"div"> & {
126
- /** Render the control as the supplied child element instead of `<div>`. */
161
+ /** Render the control as the supplied child element instead of `<div>`,
162
+ * merging its props (including the `aria-labelledby` id) onto the child
163
+ * via the {@link Slot} pattern. The child must accept a `ref`.
164
+ * @default false */
127
165
  asChild?: boolean;
128
166
  children: ReactNode;
129
167
  };
@@ -134,14 +172,18 @@ export type TreeBranchContentProps = ComponentProps<"div"> & {
134
172
  /**
135
173
  * Keep the content mounted while the branch is collapsed so CSS can
136
174
  * animate it in and out. When collapsed it is hidden from assistive
137
- * technology with `aria-hidden`.
175
+ * technology with `aria-hidden` and carries `data-state="closed"`.
176
+ * @default false
138
177
  */
139
178
  forceMount?: boolean;
140
179
  };
141
180
 
142
181
  /** Props for `Tree.BranchIndicator` — the expand/collapse affordance. */
143
182
  export type TreeBranchIndicatorProps = ComponentProps<"span"> & {
144
- /** Render as the supplied child element instead of `<span>`. */
183
+ /** Render as the supplied child element instead of `<span>`, merging
184
+ * `aria-hidden` and `data-state` onto it via the {@link Slot} pattern
185
+ * (e.g. an icon that rotates on `data-state="open"`).
186
+ * @default false */
145
187
  asChild?: boolean;
146
188
  };
147
189
 
@@ -19,18 +19,35 @@ const visuallyHiddenStyle: CSSProperties = {
19
19
  /**
20
20
  * Visually hides its children while keeping them in the accessibility tree.
21
21
  *
22
- * Renders a `<span>` carrying the canonical screen-reader-only clip styles:
23
- * the content is removed from the visual layout but still announced by
24
- * assistive technology. Use it for text that gives a control or region an
25
- * accessible name without showing on screen.
22
+ * Renders a `<span>` carrying the canonical screen-reader-only clip styles
23
+ * (WCAG 2.1 Technique C7): the content is removed from the visual layout but
24
+ * still announced by assistive technology. Use it for text that gives a control
25
+ * or region an accessible name without showing on screen.
26
26
  *
27
27
  * **Functional styles.** Unlike other `@primitiv-ui/react` components, the
28
- * clip styles are applied inline because they *are* the component's
29
- * behaviour, not decoration. A consumer `style` is merged on top, so any
30
- * individual property can still be overridden.
28
+ * clip styles are applied inline because they *are* the component's behaviour,
29
+ * not decoration:
30
+ *
31
+ * ```css
32
+ * position: absolute;
33
+ * width: 1px; height: 1px;
34
+ * padding: 0; margin: -1px;
35
+ * overflow: hidden;
36
+ * clip: rect(0 0 0 0);
37
+ * clip-path: inset(50%);
38
+ * white-space: nowrap;
39
+ * border-width: 0;
40
+ * ```
41
+ *
42
+ * A consumer `style` prop is merged on top, so any individual property can
43
+ * still be overridden.
31
44
  *
32
45
  * **`asChild` composition.** Renders the consumer's element instead of a
33
- * `<span>`, merging the clip styles in via the {@link Slot} utility.
46
+ * `<span>`, merging the clip styles onto the child via the {@link Slot}
47
+ * utility. Useful when the hidden content needs specific semantics (e.g. a
48
+ * heading or landmark). The child must be a single React element.
49
+ *
50
+ * @extends HTMLSpanElement
34
51
  *
35
52
  * @example Accessible name for an icon-only button
36
53
  * ```tsx
@@ -40,6 +57,14 @@ const visuallyHiddenStyle: CSSProperties = {
40
57
  * </button>
41
58
  * ```
42
59
  *
60
+ * @example Region label hidden from sighted users
61
+ * ```tsx
62
+ * <nav aria-labelledby="nav-label">
63
+ * <VisuallyHidden id="nav-label">Primary navigation</VisuallyHidden>
64
+ * …
65
+ * </nav>
66
+ * ```
67
+ *
43
68
  * @example asChild — keep semantic markup hidden
44
69
  * ```tsx
45
70
  * <VisuallyHidden asChild>
@@ -15,15 +15,24 @@ describe("VisuallyHidden component", () => {
15
15
  // Arrange
16
16
  render(<VisuallyHidden>Loading complete</VisuallyHidden>);
17
17
 
18
- // Assert
18
+ // Assert — the full WCAG C7 clip set; every property matters, so each is
19
+ // asserted (a missing one lets a mutant blank that value undetected).
19
20
  const hidden = screen.getByText("Loading complete");
20
21
  expect(hidden).toHaveStyle({
21
22
  position: "absolute",
22
23
  width: "1px",
23
24
  height: "1px",
25
+ padding: "0px",
26
+ margin: "-1px",
24
27
  overflow: "hidden",
25
28
  whiteSpace: "nowrap",
29
+ borderWidth: "0px",
26
30
  });
31
+ // jsdom's computed style doesn't surface clip/clip-path, so assert them on
32
+ // the inline style directly.
33
+ // jsdom normalises `rect(0 0 0 0)` to `rect(0px)`.
34
+ expect(hidden.style.clip).toBe("rect(0px)");
35
+ expect(hidden.style.clipPath).toBe("inset(50%)");
27
36
  });
28
37
 
29
38
  it("should merge a consumer style over the clip styles", () => {
@@ -56,4 +65,9 @@ describe("VisuallyHidden component", () => {
56
65
  expect(hidden.tagName).toBe("LABEL");
57
66
  expect(hidden).toHaveStyle({ position: "absolute", overflow: "hidden" });
58
67
  });
68
+
69
+ it('sets displayName to "VisuallyHidden" for React DevTools and stack traces', () => {
70
+ // Assert — an empty displayName would render it as an anonymous component.
71
+ expect(VisuallyHidden.displayName).toBe("VisuallyHidden");
72
+ });
59
73
  });
@@ -1,9 +1,19 @@
1
- import { ComponentProps } from "react";
1
+ import { ComponentProps, ReactNode } from "react";
2
2
 
3
3
  /**
4
4
  * Props for {@link VisuallyHidden} — all native `<span>` attributes plus
5
5
  * the `asChild` escape hatch.
6
6
  */
7
7
  export type VisuallyHiddenProps = ComponentProps<"span"> & {
8
+ /**
9
+ * Renders the child element instead of a native `<span>`, merging the
10
+ * clip styles onto it via {@link Slot}. Useful when the hidden content
11
+ * needs specific semantics (e.g. a heading or landmark element). The
12
+ * child must be a single React element. See the `asChild` example on
13
+ * {@link VisuallyHidden}.
14
+ * @default false
15
+ */
8
16
  asChild?: boolean;
17
+ /** Content to visually hide. Under `asChild`, becomes the single child element whose clip styles are merged. */
18
+ children?: ReactNode;
9
19
  };
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./Collapsible/index.ts";
11
11
  export * from "./ContextMenu/index.ts";
12
12
  export * from "./DirectionProvider/index.ts";
13
13
  export * from "./Divider/index.ts";
14
+ export * from "./Drawer/index.ts";
14
15
  export * from "./Dropdown/index.ts";
15
16
  export * from "./EmptyState/index.ts";
16
17
  export * from "./Field/index.ts";
@@ -19,11 +20,14 @@ export * from "./Input/index.ts";
19
20
  export * from "./InputGroup/index.ts";
20
21
  export * from "./MillerColumns/index.ts";
21
22
  export * from "./Modal/index.ts";
23
+ export * from "./NavigationMenu/index.ts";
24
+ export * from "./Popover/index.ts";
22
25
  export * from "./Portal/index.ts";
23
26
  export * from "./Progress/index.ts";
24
27
  export * from "./Radio/index.ts";
25
28
  export * from "./RadioCard/index.ts";
26
29
  export * from "./RadioGroup/index.ts";
30
+ export * from "./SegmentedControl/index.ts";
27
31
  export * from "./Select/index.ts";
28
32
  export * from "./SkipNav/index.ts";
29
33
  export * from "./Slider/index.ts";
@@ -41,6 +41,13 @@ export class MockIntersectionObserver {
41
41
  }
42
42
 
43
43
  observe(target: Element) {
44
+ // Mirror the browser: `observe` throws a TypeError when handed anything
45
+ // that is not an Element (e.g. `undefined` from a stale slide lookup).
46
+ if (!(target instanceof Element)) {
47
+ throw new TypeError(
48
+ "Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'Element'.",
49
+ );
50
+ }
44
51
  this.observed.add(target);
45
52
  }
46
53
  unobserve(target: Element) {