@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
@@ -0,0 +1,92 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { NavigationMenu } from "../NavigationMenu";
4
+
5
+ describe("NavigationMenu — basic rendering", () => {
6
+ it("renders a navigation landmark named Main by default", () => {
7
+ render(
8
+ <NavigationMenu.Root>
9
+ <NavigationMenu.List>
10
+ <NavigationMenu.Item>
11
+ <NavigationMenu.Link href="/start">Start Here</NavigationMenu.Link>
12
+ </NavigationMenu.Item>
13
+ </NavigationMenu.List>
14
+ </NavigationMenu.Root>,
15
+ );
16
+
17
+ expect(screen.getByRole("navigation", { name: "Main" })).toBeInTheDocument();
18
+ });
19
+
20
+ it("links a trigger to its panel and reports both closed", () => {
21
+ render(
22
+ <NavigationMenu.Root>
23
+ <NavigationMenu.List>
24
+ <NavigationMenu.Item value="concepts">
25
+ <NavigationMenu.Trigger>Concepts</NavigationMenu.Trigger>
26
+ <NavigationMenu.Content data-testid="panel">
27
+ <NavigationMenu.Link href="/tokens">Tokens</NavigationMenu.Link>
28
+ </NavigationMenu.Content>
29
+ </NavigationMenu.Item>
30
+ </NavigationMenu.List>
31
+ </NavigationMenu.Root>,
32
+ );
33
+
34
+ const trigger = screen.getByRole("button", { name: "Concepts" });
35
+ const panel = screen.getByTestId("panel");
36
+
37
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
38
+ expect(trigger).toHaveAttribute("aria-controls", panel.id);
39
+ expect(trigger).toHaveAttribute("data-state", "closed");
40
+ expect(panel).toHaveAttribute("aria-labelledby", trigger.id);
41
+ expect(panel).toHaveAttribute("data-state", "closed");
42
+
43
+ // The ids are a public surface — a consumer targeting them in CSS or a test
44
+ // depends on the role suffix, not just on the two sides agreeing.
45
+ expect(trigger.id).toContain("-trigger-concepts");
46
+ expect(panel.id).toContain("-panel-concepts");
47
+ });
48
+
49
+ it("does not treat an entry whose value is the empty string as open", () => {
50
+ render(
51
+ <NavigationMenu.Root>
52
+ <NavigationMenu.List>
53
+ <NavigationMenu.Item value="">
54
+ <NavigationMenu.Trigger>Blank</NavigationMenu.Trigger>
55
+ <NavigationMenu.Content data-testid="blank-panel" />
56
+ </NavigationMenu.Item>
57
+ </NavigationMenu.List>
58
+ </NavigationMenu.Root>,
59
+ );
60
+
61
+ // `""` is the closed sentinel, so an Item that adopts it as its value must
62
+ // not read back as the open entry.
63
+ expect(screen.getByRole("button", { name: "Blank" })).toHaveAttribute(
64
+ "aria-expanded",
65
+ "false",
66
+ );
67
+ expect(screen.getByTestId("blank-panel")).not.toBeVisible();
68
+ });
69
+
70
+ it("republishes an Item's value when it changes", () => {
71
+ function Nav({ itemValue }: { itemValue: string }) {
72
+ return (
73
+ <NavigationMenu.Root>
74
+ <NavigationMenu.List>
75
+ <NavigationMenu.Item value={itemValue}>
76
+ <NavigationMenu.Trigger>Concepts</NavigationMenu.Trigger>
77
+ <NavigationMenu.Content data-testid="panel" />
78
+ </NavigationMenu.Item>
79
+ </NavigationMenu.List>
80
+ </NavigationMenu.Root>
81
+ );
82
+ }
83
+
84
+ const { rerender } = render(<Nav itemValue="alpha" />);
85
+ rerender(<Nav itemValue="beta" />);
86
+
87
+ expect(screen.getByRole("button", { name: "Concepts" }).id).toContain(
88
+ "-trigger-beta",
89
+ );
90
+ expect(screen.getByTestId("panel").id).toContain("-panel-beta");
91
+ });
92
+ });
@@ -0,0 +1,45 @@
1
+ import { NavigationMenu } from "../NavigationMenu";
2
+ import {
3
+ NavigationMenuContent,
4
+ NavigationMenuIndicator,
5
+ NavigationMenuItem,
6
+ NavigationMenuLink,
7
+ NavigationMenuList,
8
+ NavigationMenuTrigger,
9
+ NavigationMenuViewport,
10
+ } from "../NavigationMenu";
11
+ import {
12
+ NavigationMenuContext,
13
+ NavigationMenuItemContext,
14
+ } from "../NavigationMenuContext";
15
+
16
+ describe("NavigationMenu — public contract", () => {
17
+ it("exposes every sub-component on the compound", () => {
18
+ expect(NavigationMenu.Root).toBe(NavigationMenu);
19
+ expect(NavigationMenu.List).toBe(NavigationMenuList);
20
+ expect(NavigationMenu.Item).toBe(NavigationMenuItem);
21
+ expect(NavigationMenu.Trigger).toBe(NavigationMenuTrigger);
22
+ expect(NavigationMenu.Content).toBe(NavigationMenuContent);
23
+ expect(NavigationMenu.Viewport).toBe(NavigationMenuViewport);
24
+ expect(NavigationMenu.Indicator).toBe(NavigationMenuIndicator);
25
+ expect(NavigationMenu.Link).toBe(NavigationMenuLink);
26
+ });
27
+
28
+ it("names the compound and each sub-component for React DevTools", () => {
29
+ expect(NavigationMenu.displayName).toBe("NavigationMenu");
30
+ expect(NavigationMenuList.displayName).toBe("NavigationMenuList");
31
+ expect(NavigationMenuItem.displayName).toBe("NavigationMenuItem");
32
+ expect(NavigationMenuTrigger.displayName).toBe("NavigationMenuTrigger");
33
+ expect(NavigationMenuContent.displayName).toBe("NavigationMenuContent");
34
+ expect(NavigationMenuViewport.displayName).toBe("NavigationMenuViewport");
35
+ expect(NavigationMenuIndicator.displayName).toBe("NavigationMenuIndicator");
36
+ expect(NavigationMenuLink.displayName).toBe("NavigationMenuLink");
37
+ });
38
+
39
+ it("names both strict contexts for React DevTools", () => {
40
+ expect(NavigationMenuContext.displayName).toBe("NavigationMenuContext");
41
+ expect(NavigationMenuItemContext.displayName).toBe(
42
+ "NavigationMenuItemContext",
43
+ );
44
+ });
45
+ });
@@ -0,0 +1,134 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { useState } from "react";
4
+
5
+ import { ThreeEntryNav } from "./NavigationMenu.fixtures";
6
+
7
+ describe("NavigationMenu — controlled state", () => {
8
+ it("reflects the value prop without owning it", async () => {
9
+ const user = userEvent.setup();
10
+ const onValueChange = vi.fn();
11
+ render(<ThreeEntryNav value="" onValueChange={onValueChange} />);
12
+
13
+ await user.click(screen.getByRole("button", { name: "Concepts" }));
14
+
15
+ expect(onValueChange).toHaveBeenCalledWith("concepts");
16
+ expect(screen.getByTestId("concepts-panel")).not.toBeVisible();
17
+ });
18
+
19
+ it("requests a close with the empty string", async () => {
20
+ const user = userEvent.setup();
21
+ const onValueChange = vi.fn();
22
+ render(<ThreeEntryNav value="concepts" onValueChange={onValueChange} />);
23
+
24
+ await user.click(screen.getByRole("button", { name: "Concepts" }));
25
+
26
+ expect(onValueChange).toHaveBeenCalledWith("");
27
+ });
28
+
29
+ it("requests the close with the empty string on Escape", async () => {
30
+ const user = userEvent.setup();
31
+ const onValueChange = vi.fn();
32
+ render(<ThreeEntryNav value="concepts" onValueChange={onValueChange} />);
33
+
34
+ screen.getByRole("link", { name: "Tokens" }).focus();
35
+ await user.keyboard("{Escape}");
36
+
37
+ expect(onValueChange).toHaveBeenCalledWith("");
38
+ });
39
+
40
+ it("reports nothing when Escape is pressed with every panel closed", async () => {
41
+ const user = userEvent.setup();
42
+ const onValueChange = vi.fn();
43
+ render(<ThreeEntryNav value="" onValueChange={onValueChange} />);
44
+
45
+ screen.getByRole("link", { name: "Changelog" }).focus();
46
+ await user.keyboard("{Escape}");
47
+
48
+ // Nothing is open, so there is nothing to close — a spurious `""` would
49
+ // make a controlled parent re-render (and clear its own state) on every
50
+ // stray Escape anywhere in the nav.
51
+ expect(onValueChange).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it("still closes when the open value names no rendered trigger", async () => {
55
+ const user = userEvent.setup();
56
+ const onValueChange = vi.fn();
57
+ render(<ThreeEntryNav value="ghost" onValueChange={onValueChange} />);
58
+
59
+ screen.getByRole("link", { name: "Changelog" }).focus();
60
+ await user.keyboard("{Escape}");
61
+
62
+ // There is no trigger to return focus to, which must not stop the close.
63
+ expect(onValueChange).toHaveBeenCalledWith("");
64
+ });
65
+
66
+ it("calls the latest onValueChange, not the one from first render", async () => {
67
+ const user = userEvent.setup();
68
+ const first = vi.fn();
69
+ const second = vi.fn();
70
+ const { rerender } = render(
71
+ <ThreeEntryNav value="concepts" onValueChange={first} />,
72
+ );
73
+
74
+ rerender(<ThreeEntryNav value="concepts" onValueChange={second} />);
75
+ screen.getByRole("link", { name: "Tokens" }).focus();
76
+ await user.keyboard("{Escape}");
77
+
78
+ // An inline `onValueChange` is a new function every render, so a handler
79
+ // pinned at mount would report to a closure over stale parent state.
80
+ expect(second).toHaveBeenCalledWith("");
81
+ expect(first).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it("reports nothing when focus moves outside with everything already closed", () => {
85
+ const onValueChange = vi.fn();
86
+ render(<ThreeEntryNav value="" onValueChange={onValueChange} />);
87
+
88
+ const trigger = screen.getByRole("button", { name: "Concepts" });
89
+ fireEvent.blur(trigger, { relatedTarget: document.body });
90
+
91
+ // A spurious "" would make a controlled parent re-render on every stray
92
+ // blur inside the nav, exactly like the Escape case above.
93
+ expect(onValueChange).not.toHaveBeenCalled();
94
+ });
95
+
96
+ it("requests a close with the empty string when focus moves outside", () => {
97
+ const onValueChange = vi.fn();
98
+ render(<ThreeEntryNav value="concepts" onValueChange={onValueChange} />);
99
+
100
+ const trigger = screen.getByRole("button", { name: "Concepts" });
101
+ fireEvent.blur(trigger, { relatedTarget: document.body });
102
+
103
+ expect(onValueChange).toHaveBeenCalledWith("");
104
+ });
105
+
106
+ it("calls the latest onValueChange on a focus-outside close too, not the one from first render", () => {
107
+ const first = vi.fn();
108
+ const second = vi.fn();
109
+ const { rerender } = render(
110
+ <ThreeEntryNav value="concepts" onValueChange={first} />,
111
+ );
112
+
113
+ rerender(<ThreeEntryNav value="concepts" onValueChange={second} />);
114
+ const trigger = screen.getByRole("button", { name: "Concepts" });
115
+ fireEvent.blur(trigger, { relatedTarget: document.body });
116
+
117
+ expect(second).toHaveBeenCalledWith("");
118
+ expect(first).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("opens the panel once the parent commits the new value", async () => {
122
+ const user = userEvent.setup();
123
+
124
+ function Controlled() {
125
+ const [open, setOpen] = useState("");
126
+ return <ThreeEntryNav value={open} onValueChange={setOpen} />;
127
+ }
128
+
129
+ render(<Controlled />);
130
+ await user.click(screen.getByRole("button", { name: "Registry & CLI" }));
131
+
132
+ expect(screen.getByTestId("registry-panel")).toBeVisible();
133
+ });
134
+ });
@@ -0,0 +1,62 @@
1
+ import { render } from "@testing-library/react";
2
+
3
+ import { NavigationMenu } from "../NavigationMenu";
4
+
5
+ const MISSING_VALUE_ERROR =
6
+ "NavigationMenu.Trigger and NavigationMenu.Content require a `value` on their NavigationMenu.Item. " +
7
+ "Omit the value only for a plain link entry, which has no panel to open.";
8
+ const OUTSIDE_ROOT_ERROR =
9
+ "Component must be rendered as a child of NavigationMenu.Root";
10
+ const OUTSIDE_ITEM_ERROR =
11
+ "Component must be rendered as a child of NavigationMenu.Item";
12
+
13
+ describe("NavigationMenu — error handling", () => {
14
+ it("throws when a Trigger's Item has no value", () => {
15
+ expect(() =>
16
+ render(
17
+ <NavigationMenu.Root>
18
+ <NavigationMenu.List>
19
+ <NavigationMenu.Item>
20
+ <NavigationMenu.Trigger>Concepts</NavigationMenu.Trigger>
21
+ </NavigationMenu.Item>
22
+ </NavigationMenu.List>
23
+ </NavigationMenu.Root>,
24
+ ),
25
+ ).toThrow(MISSING_VALUE_ERROR);
26
+ });
27
+
28
+ it("throws when a Content's Item has no value", () => {
29
+ expect(() =>
30
+ render(
31
+ <NavigationMenu.Root>
32
+ <NavigationMenu.List>
33
+ <NavigationMenu.Item>
34
+ <NavigationMenu.Content />
35
+ </NavigationMenu.Item>
36
+ </NavigationMenu.List>
37
+ </NavigationMenu.Root>,
38
+ ),
39
+ ).toThrow(MISSING_VALUE_ERROR);
40
+ });
41
+
42
+ it.each([
43
+ ["List", <NavigationMenu.List key="l" />],
44
+ ["Viewport", <NavigationMenu.Viewport key="v" />],
45
+ ["Indicator", <NavigationMenu.Indicator key="i" />],
46
+ ["Link", <NavigationMenu.Link key="k" href="/x" />],
47
+ ])("throws when %s is rendered outside a Root", (_name, element) => {
48
+ expect(() => render(element)).toThrow(OUTSIDE_ROOT_ERROR);
49
+ });
50
+
51
+ it("throws when a Trigger is rendered outside an Item", () => {
52
+ expect(() =>
53
+ render(
54
+ <NavigationMenu.Root>
55
+ <NavigationMenu.List>
56
+ <NavigationMenu.Trigger>Concepts</NavigationMenu.Trigger>
57
+ </NavigationMenu.List>
58
+ </NavigationMenu.Root>,
59
+ ),
60
+ ).toThrow(OUTSIDE_ITEM_ERROR);
61
+ });
62
+ });
@@ -0,0 +1,79 @@
1
+ import type { ReactElement } from "react";
2
+
3
+ import { NavigationMenu } from "../NavigationMenu";
4
+ import type { NavigationMenuRootProps } from "../types";
5
+
6
+ /** The three-entry nav used across the interaction suites: two panelled
7
+ * entries either side of a plain link entry, which is what exercises the
8
+ * "an Item without a value is a link, not a disclosure" contract. */
9
+ export function ThreeEntryNav(props: NavigationMenuRootProps): ReactElement {
10
+ return (
11
+ <NavigationMenu.Root {...props}>
12
+ <NavigationMenu.List>
13
+ <NavigationMenu.Item value="concepts">
14
+ <NavigationMenu.Trigger>Concepts</NavigationMenu.Trigger>
15
+ <NavigationMenu.Content data-testid="concepts-panel">
16
+ <NavigationMenu.Link href="/tokens">Tokens</NavigationMenu.Link>
17
+ <NavigationMenu.Link href="/themes">Themes</NavigationMenu.Link>
18
+ </NavigationMenu.Content>
19
+ </NavigationMenu.Item>
20
+ <NavigationMenu.Item>
21
+ <NavigationMenu.Link href="/changelog">Changelog</NavigationMenu.Link>
22
+ </NavigationMenu.Item>
23
+ <NavigationMenu.Item value="registry">
24
+ <NavigationMenu.Trigger>Registry &amp; CLI</NavigationMenu.Trigger>
25
+ <NavigationMenu.Content data-testid="registry-panel">
26
+ <NavigationMenu.Link href="/cli">CLI</NavigationMenu.Link>
27
+ </NavigationMenu.Content>
28
+ </NavigationMenu.Item>
29
+ </NavigationMenu.List>
30
+ </NavigationMenu.Root>
31
+ );
32
+ }
33
+
34
+ /** A nav whose two disclosure entries are both dead ends for keyboard focus:
35
+ * `panelless` renders no `Content` at all, and `prose`'s panel holds nothing
36
+ * focusable. Between them they cover the enter-panel arrow's two
37
+ * nothing-to-focus cases — no panel element, and a panel with no target. */
38
+ export function DeadEndNav(props: NavigationMenuRootProps): ReactElement {
39
+ return (
40
+ <NavigationMenu.Root {...props}>
41
+ <NavigationMenu.List>
42
+ <NavigationMenu.Item value="panelless">
43
+ <NavigationMenu.Trigger>Panelless</NavigationMenu.Trigger>
44
+ </NavigationMenu.Item>
45
+ <NavigationMenu.Item value="prose">
46
+ <NavigationMenu.Trigger>Prose</NavigationMenu.Trigger>
47
+ <NavigationMenu.Content data-testid="prose-panel">
48
+ Nothing in this panel can take focus.
49
+ </NavigationMenu.Content>
50
+ </NavigationMenu.Item>
51
+ </NavigationMenu.List>
52
+ </NavigationMenu.Root>
53
+ );
54
+ }
55
+
56
+ /** Arrow-key cases for a horizontal LTR nav. `from` / `expected` are the
57
+ * accessible names of the top-level entries in `ThreeEntryNav`. */
58
+ export const horizontalArrowCases = [
59
+ { key: "{ArrowRight}", from: "Concepts", expected: "Changelog" },
60
+ { key: "{ArrowRight}", from: "Registry & CLI", expected: "Concepts" },
61
+ { key: "{ArrowLeft}", from: "Concepts", expected: "Registry & CLI" },
62
+ { key: "{ArrowLeft}", from: "Changelog", expected: "Concepts" },
63
+ { key: "{Home}", from: "Changelog", expected: "Concepts" },
64
+ { key: "{End}", from: "Concepts", expected: "Registry & CLI" },
65
+ ] as const;
66
+
67
+ /** The same movements under `dir="rtl"`: only the horizontal pair inverts,
68
+ * Home/End stay anchored to the start/end of the list. */
69
+ export const rtlArrowCases = [
70
+ { key: "{ArrowLeft}", from: "Concepts", expected: "Changelog" },
71
+ { key: "{ArrowRight}", from: "Concepts", expected: "Registry & CLI" },
72
+ { key: "{Home}", from: "Changelog", expected: "Concepts" },
73
+ ] as const;
74
+
75
+ /** Vertical orientation binds the up/down pair instead. */
76
+ export const verticalArrowCases = [
77
+ { key: "{ArrowDown}", from: "Concepts", expected: "Changelog" },
78
+ { key: "{ArrowUp}", from: "Concepts", expected: "Registry & CLI" },
79
+ ] as const;
@@ -0,0 +1,52 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { NavigationMenu } from "../NavigationMenu";
5
+
6
+ function Nav({ forceMount }: { forceMount?: boolean }) {
7
+ return (
8
+ <NavigationMenu.Root>
9
+ <NavigationMenu.List>
10
+ <NavigationMenu.Item value="concepts">
11
+ <NavigationMenu.Trigger>Concepts</NavigationMenu.Trigger>
12
+ <NavigationMenu.Content data-testid="panel" forceMount={forceMount}>
13
+ <NavigationMenu.Link href="/tokens">Tokens</NavigationMenu.Link>
14
+ </NavigationMenu.Content>
15
+ </NavigationMenu.Item>
16
+ </NavigationMenu.List>
17
+ </NavigationMenu.Root>
18
+ );
19
+ }
20
+
21
+ describe("NavigationMenu.Content — forceMount", () => {
22
+ it("leaves a closed panel out of the accessibility tree without hiding it", () => {
23
+ render(<Nav forceMount />);
24
+
25
+ const panel = screen.getByTestId("panel");
26
+
27
+ expect(panel).not.toHaveAttribute("hidden");
28
+ expect(panel).toHaveAttribute("aria-hidden", "true");
29
+ expect(panel).toHaveAttribute("data-state", "closed");
30
+ });
31
+
32
+ it("drops aria-hidden once the panel opens", async () => {
33
+ const user = userEvent.setup();
34
+ render(<Nav forceMount />);
35
+
36
+ await user.click(screen.getByRole("button", { name: "Concepts" }));
37
+
38
+ const panel = screen.getByTestId("panel");
39
+
40
+ expect(panel).not.toHaveAttribute("aria-hidden");
41
+ expect(panel).toHaveAttribute("data-state", "open");
42
+ });
43
+
44
+ it("uses hidden when forceMount is not set", () => {
45
+ render(<Nav />);
46
+
47
+ const panel = screen.getByTestId("panel");
48
+
49
+ expect(panel).toHaveAttribute("hidden");
50
+ expect(panel).not.toHaveAttribute("aria-hidden");
51
+ });
52
+ });