@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,158 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import type { KeyboardEvent, MouseEvent, PointerEvent, RefObject } from "react";
3
+
4
+ import { composeEventHandlers } from "../../Slot/index.ts";
5
+ import { PANEL_FOCUSABLE_SELECTOR } from "../constants";
6
+ import { useNavigationMenuContext } from "../NavigationMenuContext";
7
+ import type { NavigationMenuTriggerProps } from "../types";
8
+ import { getTriggerAnchorName } from "../utils";
9
+
10
+ import { useNavigationMenuEntry } from "./useNavigationMenuEntry";
11
+ import { useNavigationMenuTopLevelEntry } from "./useNavigationMenuTopLevelEntry";
12
+
13
+ export function useNavigationMenuTrigger({
14
+ onClick,
15
+ onPointerEnter,
16
+ onPointerLeave,
17
+ onKeyDown,
18
+ }: Pick<
19
+ NavigationMenuTriggerProps,
20
+ "onClick" | "onPointerEnter" | "onPointerLeave" | "onKeyDown"
21
+ >): {
22
+ triggerRef: RefObject<HTMLButtonElement | null>;
23
+ triggerId: string;
24
+ panelId: string;
25
+ open: boolean;
26
+ state: "open" | "closed";
27
+ /** This trigger's own `anchor-name`, so the shared Viewport can position
28
+ * its panel relative to whichever trigger opened it. */
29
+ anchorName: string;
30
+ handleClick: (event: MouseEvent<HTMLButtonElement>) => void;
31
+ handlePointerEnter: (event: PointerEvent<HTMLButtonElement>) => void;
32
+ handlePointerLeave: (event: PointerEvent<HTMLButtonElement>) => void;
33
+ handleKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
34
+ } {
35
+ const {
36
+ orientation,
37
+ dir,
38
+ setOpenValue,
39
+ openOnHover,
40
+ openWithIntent,
41
+ cancelOpen,
42
+ } = useNavigationMenuContext();
43
+ const { value, triggerId, panelId, open, state } = useNavigationMenuEntry();
44
+ const { entryRef: triggerRef, handleKeyDown: handleArrowKeyDown } =
45
+ useNavigationMenuTopLevelEntry<HTMLButtonElement>();
46
+
47
+ // What `open` was when the pointer arrived, or `undefined` when no pointer
48
+ // is over the trigger. A pointer that comes to click first fires
49
+ // `pointerenter`, which — with hover-to-open — opens the panel *before* the
50
+ // click lands. Toggling against the live `open` would then close it again on
51
+ // that same click. Toggling against the pre-hover value is what makes a
52
+ // click mean "toggle what I saw", not "undo my own hover".
53
+ const openOnPointerEnterRef = useRef<boolean | undefined>(undefined);
54
+
55
+ const toggle = useCallback(() => {
56
+ // A click is a decision, so it bypasses hover intent entirely — including
57
+ // any open timer already ticking from the pointer that arrived to click.
58
+ cancelOpen();
59
+ const wasOpen = openOnPointerEnterRef.current ?? open;
60
+ // Cleared so a second click on a still-hovered trigger toggles against
61
+ // live state rather than replaying the stale arrival value.
62
+ openOnPointerEnterRef.current = undefined;
63
+ setOpenValue(wasOpen ? "" : value);
64
+ }, [cancelOpen, open, setOpenValue, value]);
65
+
66
+ const hoverOpen = useCallback(() => {
67
+ openOnPointerEnterRef.current = open;
68
+ if (openOnHover) openWithIntent(value);
69
+ }, [open, openOnHover, openWithIntent, value]);
70
+
71
+ const pointerLeave = useCallback(
72
+ () => {
73
+ openOnPointerEnterRef.current = undefined;
74
+ cancelOpen();
75
+ },
76
+ // `cancelOpen` is a `useCallback(…, [])` on the Root, so its identity is
77
+ // fixed for the nav's lifetime — emptying this array yields the identical
78
+ // memoised function.
79
+ // Stryker disable next-line ArrayDeclaration: equivalent — stable dependency.
80
+ [cancelOpen],
81
+ );
82
+
83
+ // The arrow that "goes into" the panel is the one pointing across the list's
84
+ // axis: down out of a horizontal bar, sideways out of a vertical rail (and
85
+ // that sideways key mirrors under RTL).
86
+ const enterPanelKey =
87
+ orientation === "horizontal"
88
+ ? "ArrowDown"
89
+ : dir === "rtl"
90
+ ? "ArrowLeft"
91
+ : "ArrowRight";
92
+
93
+ // Focus can only land in the panel once it has actually rendered unhidden, so
94
+ // the keypress records the intent and this effect spends it.
95
+ const enterPanelPendingRef = useRef(false);
96
+ useEffect(() => {
97
+ if (!enterPanelPendingRef.current || !open) return;
98
+ enterPanelPendingRef.current = false;
99
+ document
100
+ .getElementById(panelId)
101
+ ?.querySelector<HTMLElement>(PANEL_FOCUSABLE_SELECTOR)
102
+ ?.focus();
103
+ }, [open, panelId]);
104
+
105
+ const enterPanel = useCallback(
106
+ (event: KeyboardEvent<HTMLButtonElement>) => {
107
+ if (event.key !== enterPanelKey) return;
108
+ event.preventDefault();
109
+ cancelOpen();
110
+ enterPanelPendingRef.current = true;
111
+ // Already-open panels never re-run the effect (`open` doesn't change), so
112
+ // move focus straight away in that case.
113
+ if (open) {
114
+ enterPanelPendingRef.current = false;
115
+ document
116
+ .getElementById(panelId)
117
+ ?.querySelector<HTMLElement>(PANEL_FOCUSABLE_SELECTOR)
118
+ ?.focus();
119
+ return;
120
+ }
121
+ setOpenValue(value);
122
+ },
123
+ [cancelOpen, enterPanelKey, open, panelId, setOpenValue, value],
124
+ );
125
+
126
+ const handleClick = composeEventHandlers<MouseEvent<HTMLButtonElement>>(
127
+ onClick,
128
+ toggle,
129
+ );
130
+ const handlePointerEnter = composeEventHandlers<
131
+ PointerEvent<HTMLButtonElement>
132
+ >(onPointerEnter, hoverOpen);
133
+ const handlePointerLeave = composeEventHandlers<
134
+ PointerEvent<HTMLButtonElement>
135
+ >(onPointerLeave, pointerLeave);
136
+ // enterPanel runs first and claims its arrow with preventDefault; the travel
137
+ // keymap only sees keys it left alone.
138
+ const handleKeyDown = composeEventHandlers<KeyboardEvent<HTMLButtonElement>>(
139
+ onKeyDown,
140
+ composeEventHandlers<KeyboardEvent<HTMLButtonElement>>(
141
+ enterPanel,
142
+ handleArrowKeyDown,
143
+ ),
144
+ );
145
+
146
+ return {
147
+ triggerRef,
148
+ triggerId,
149
+ panelId,
150
+ open,
151
+ state,
152
+ anchorName: getTriggerAnchorName(triggerId),
153
+ handleClick,
154
+ handlePointerEnter,
155
+ handlePointerLeave,
156
+ handleKeyDown,
157
+ };
158
+ }
@@ -0,0 +1,101 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import type { CSSProperties } from "react";
3
+
4
+ import { useNavigationMenuContext } from "../NavigationMenuContext";
5
+
6
+ type Size = { width: number; height: number } | null;
7
+
8
+ /**
9
+ * Measures the open panel and hands its size back as custom properties.
10
+ *
11
+ * The same division of labour as the Indicator: the library ships no styles, so
12
+ * the Viewport can't size itself — but only it can see which panel is open and
13
+ * how big that panel is. Publishing the measurement as
14
+ * `--primitiv-navigation-menu-viewport-width` / `-height` lets a stylesheet
15
+ * transition the shared box between panels (the one-box morph), while leaving it
16
+ * free to ignore both and let the box hug its content instead.
17
+ *
18
+ * The measurement is **kept through the close**: clearing it would collapse the
19
+ * box to nothing at the very moment the exit animation needs its size, so the
20
+ * last open panel's dimensions stay published until another panel opens.
21
+ */
22
+ export function useNavigationMenuViewport(): {
23
+ open: boolean;
24
+ openValue: string;
25
+ orientation: "horizontal" | "vertical";
26
+ style: CSSProperties;
27
+ } {
28
+ const { orientation, openValue, viewport } = useNavigationMenuContext();
29
+ const open = openValue !== "";
30
+ const [size, setSize] = useState<Size>(null);
31
+
32
+ const measure = useCallback(() => {
33
+ // Nothing open: hold the last measurement so the exit animates from the size
34
+ // it was, rather than snapping to zero before it starts.
35
+ if (openValue === "") return;
36
+ if (!viewport) {
37
+ setSize(null);
38
+ return;
39
+ }
40
+ // Only a Content carries data-state inside the viewport, and only direct
41
+ // children are projected panels — so this finds the open panel without the
42
+ // headless layer needing a class to look for.
43
+ const panel = Array.from(viewport.children).find(
44
+ (child): child is HTMLElement =>
45
+ child instanceof HTMLElement && child.dataset.state === "open",
46
+ );
47
+ if (!panel) {
48
+ setSize(null);
49
+ return;
50
+ }
51
+ setSize({ width: panel.offsetWidth, height: panel.offsetHeight });
52
+ }, [openValue, viewport]);
53
+
54
+ useEffect(() => {
55
+ measure();
56
+ // A panel's very first paint can still be mid a browser-internal multi-pass
57
+ // layout — confirmed live: a multi-line description's true wrapped height
58
+ // was consistently one frame behind this effect's own synchronous read (a
59
+ // CSS Grid intrinsic-sizing quirk on a genuinely new subtree), publishing a
60
+ // too-short height that only self-corrected on the next real open. One
61
+ // rAF-deferred re-measurement catches that up; it's harmless to repeat on
62
+ // an already-settled panel, since re-measuring just republishes the same
63
+ // numbers.
64
+ const raf = requestAnimationFrame(measure);
65
+ return () => cancelAnimationFrame(raf);
66
+ }, [measure]);
67
+
68
+ // Panel size is layout-dependent, so a reflow invalidates it — the same reason
69
+ // the Indicator re-measures here.
70
+ useEffect(() => {
71
+ window.addEventListener("resize", measure);
72
+ return () => window.removeEventListener("resize", measure);
73
+ }, [measure]);
74
+
75
+ // A web font (e.g. Google Fonts' display=swap) can still be loading when this
76
+ // effect's own measurement runs, and the swap's reflow lands asynchronously,
77
+ // after — nothing else invalidates the published size when that happens, so it
78
+ // stays locked to the pre-swap layout until some unrelated event (a resize, or
79
+ // the next open) happens to re-measure. `document.fonts` is absent in jsdom
80
+ // (and, historically, some very old browsers), hence the guard.
81
+ useEffect(() => {
82
+ const fonts = document.fonts;
83
+ if (!fonts) return;
84
+ fonts.addEventListener("loadingdone", measure);
85
+ return () => fonts.removeEventListener("loadingdone", measure);
86
+ }, [measure]);
87
+
88
+ // `undefined` is dropped by React, so a viewport that has never hosted an open
89
+ // panel publishes nothing at all rather than a zeroed size the stylesheet would
90
+ // have to special-case.
91
+ const style = {
92
+ "--primitiv-navigation-menu-viewport-width": size
93
+ ? `${size.width}px`
94
+ : undefined,
95
+ "--primitiv-navigation-menu-viewport-height": size
96
+ ? `${size.height}px`
97
+ : undefined,
98
+ } as CSSProperties;
99
+
100
+ return { open, openValue, orientation, style };
101
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./NavigationMenu";
2
+ export * from "./NavigationMenuContext";
3
+ export * from "./types";
@@ -0,0 +1,259 @@
1
+ import { ComponentProps, Ref } from "react";
2
+
3
+ /** Layout axis of the navigation list. `"horizontal"` binds
4
+ * ArrowLeft/ArrowRight between top-level entries; `"vertical"` binds
5
+ * ArrowUp/ArrowDown. */
6
+ export type NavigationMenuOrientation = "horizontal" | "vertical";
7
+
8
+ /** Reading direction of the navigation list. In `"rtl"` the horizontal arrow
9
+ * pair is mirrored so navigation follows the visual order. */
10
+ export type NavigationMenuReadingDirection = "ltr" | "rtl";
11
+
12
+ /** Uncontrolled `NavigationMenu.Root` props: the component owns which panel is
13
+ * open, seeded by an optional `defaultValue`. */
14
+ export type UncontrolledNavigationMenuRootProps = {
15
+ /** Value of the entry whose panel is open on first render. Omit (or pass
16
+ * `""`) to start with everything closed. */
17
+ defaultValue?: string;
18
+ value?: never;
19
+ onValueChange?: never;
20
+ };
21
+
22
+ /** Controlled `NavigationMenu.Root` props: the caller owns which panel is open
23
+ * via `value`, and is notified of open/close requests through
24
+ * `onValueChange`. */
25
+ export type ControlledNavigationMenuRootProps = {
26
+ /** Value of the entry whose panel is open. `""` means every panel is
27
+ * closed. */
28
+ value: string;
29
+ /** Called with the requested open value — the entry's value to open it, or
30
+ * `""` to close whatever is open. */
31
+ onValueChange: (value: string) => void;
32
+ defaultValue?: never;
33
+ };
34
+
35
+ /**
36
+ * Props for {@link NavigationMenuRoot | `NavigationMenu.Root`} — the `<nav>`
37
+ * landmark and the owner of which panel is open. Extends the native `<nav>`
38
+ * props and resolves to either the
39
+ * {@link UncontrolledNavigationMenuRootProps | uncontrolled} or the
40
+ * {@link ControlledNavigationMenuRootProps | controlled} shape, never a mix.
41
+ *
42
+ * `defaultValue` and `dir` are `Omit`-ted from the inherited `<nav>` props
43
+ * before being re-declared: both narrow a same-named native attribute
44
+ * (`defaultValue` to the open-panel value, `dir` to `"ltr" | "rtl"`), and
45
+ * leaving the native declarations in place would resolve them to intersection
46
+ * artifacts that leak into consumer types and the generated prop tables.
47
+ *
48
+ * @extends HTMLElement
49
+ */
50
+ export type NavigationMenuRootProps = Omit<
51
+ ComponentProps<"nav">,
52
+ "defaultValue" | "dir"
53
+ > & {
54
+ /** Layout axis; see {@link NavigationMenuOrientation}.
55
+ * @default "horizontal" */
56
+ orientation?: NavigationMenuOrientation;
57
+ /** Reading direction; see {@link NavigationMenuReadingDirection}. Inherited
58
+ * from the nearest `DirectionProvider` when omitted, falling back to
59
+ * `"ltr"`. */
60
+ dir?: NavigationMenuReadingDirection;
61
+ /** Whether hovering a trigger opens its panel. Set `false` for a
62
+ * click-only nav — hover-to-open is the desktop convention, but it has no
63
+ * touch equivalent and some products prefer to opt out.
64
+ * @default true */
65
+ openOnHover?: boolean;
66
+ /** Milliseconds a trigger must be hovered before its panel opens — the
67
+ * hover-intent delay that stops a panel flashing open as the pointer
68
+ * crosses the nav on its way somewhere else. `0` opens immediately.
69
+ * Ignored once a panel is already open: moving between triggers always
70
+ * switches without delay. Ignored entirely when
71
+ * {@link NavigationMenuRootProps.openOnHover | `openOnHover`} is `false`.
72
+ * @default 200 */
73
+ delayDuration?: number;
74
+ /** Milliseconds the open panel survives after the pointer leaves the
75
+ * `<nav>`, so a pointer that clips the edge of a panel on its way back
76
+ * doesn't dismiss it. Returning to the nav within the window cancels the
77
+ * close.
78
+ * @default 150 */
79
+ closeDelay?: number;
80
+ } & (
81
+ | UncontrolledNavigationMenuRootProps
82
+ | ControlledNavigationMenuRootProps
83
+ );
84
+
85
+ /**
86
+ * Props for {@link NavigationMenuList | `NavigationMenu.List`} — all native
87
+ * `<ul>` attributes. No props are added; content is
88
+ * {@link NavigationMenuItem | `NavigationMenu.Item`}s.
89
+ *
90
+ * @extends HTMLUListElement
91
+ */
92
+ export type NavigationMenuListProps = ComponentProps<"ul">;
93
+
94
+ /**
95
+ * Props for {@link NavigationMenuItem | `NavigationMenu.Item`} — all native
96
+ * `<li>` attributes plus the entry's `value`.
97
+ *
98
+ * `value` is `Omit`-ted from the inherited `<li>` props before being
99
+ * re-declared: `<li value>` is a native ordinal number attribute, and leaving
100
+ * it in place resolves this prop to an intersection artifact
101
+ * (`string | (readonly string[] & string)`) that leaks into consumer hovers and
102
+ * the generated prop tables.
103
+ *
104
+ * @extends HTMLLIElement
105
+ */
106
+ export type NavigationMenuItemProps = Omit<ComponentProps<"li">, "value"> & {
107
+ /** Identifies the entry's panel. Required for an entry that has a
108
+ * `NavigationMenu.Trigger`; omit it for a plain link entry. */
109
+ value?: string;
110
+ };
111
+
112
+ /**
113
+ * Props for {@link NavigationMenuTrigger | `NavigationMenu.Trigger`} — the
114
+ * `<button>` that opens an entry's panel. The element type defaults to
115
+ * `HTMLButtonElement` and can be overridden through `asChild` and the `ref`
116
+ * type parameter.
117
+ *
118
+ * `ref` is `Omit`-ted from the inherited `<button>` props before being
119
+ * re-declared, so narrowing it to a different element under `asChild` yields a
120
+ * usable `Ref<T>` rather than an unusable intersection of both.
121
+ *
122
+ * @extends HTMLButtonElement
123
+ */
124
+ export type NavigationMenuTriggerProps<
125
+ T extends HTMLElement = HTMLButtonElement,
126
+ > = Omit<ComponentProps<"button">, "ref"> & {
127
+ /** Renders the child element instead of the default `<button>`. All
128
+ * disclosure ARIA attributes, the hover-intent and keyboard handlers, and
129
+ * the internal ref are merged onto the child via {@link Slot}. The child
130
+ * must be a single React element that accepts a `ref`.
131
+ * @default false */
132
+ asChild?: boolean;
133
+ /** Ref to the rendered element. Defaults to `HTMLButtonElement`; when using
134
+ * `asChild`, specify the child's element type. */
135
+ ref?: Ref<T>;
136
+ };
137
+
138
+ /**
139
+ * Props for {@link NavigationMenuContent | `NavigationMenu.Content`} — all
140
+ * native `<div>` attributes plus `forceMount`.
141
+ *
142
+ * @extends HTMLDivElement
143
+ */
144
+ export type NavigationMenuContentProps = ComponentProps<"div"> & {
145
+ /** Keeps the closed panel out of the `hidden` state so CSS can animate it
146
+ * in and out, marking it `aria-hidden` instead so assistive technology
147
+ * still ignores it. Without this the panel is `hidden` when closed, which no
148
+ * transition can animate away from.
149
+ * @default false */
150
+ forceMount?: boolean;
151
+ };
152
+
153
+ /**
154
+ * Props for {@link NavigationMenuViewport | `NavigationMenu.Viewport`} — all
155
+ * native `<div>` attributes plus `forceMount`.
156
+ *
157
+ * @extends HTMLDivElement
158
+ */
159
+ export type NavigationMenuViewportProps = ComponentProps<"div"> & {
160
+ /** Keeps the viewport unhidden while nothing is open so CSS can animate the
161
+ * box collapsing and expanding. Mirrors
162
+ * {@link NavigationMenuContentProps.forceMount | `Content`'s `forceMount`}.
163
+ * @default false */
164
+ forceMount?: boolean;
165
+ };
166
+
167
+ /**
168
+ * Props for {@link NavigationMenuIndicator | `NavigationMenu.Indicator`} — all
169
+ * native `<div>` attributes plus `forceMount`. The measured geometry arrives as
170
+ * inline custom properties on `style`; a consumer `style` is merged over the
171
+ * top, so it can override everything except the two geometry properties it
172
+ * doesn't set.
173
+ *
174
+ * @extends HTMLDivElement
175
+ */
176
+ export type NavigationMenuIndicatorProps = ComponentProps<"div"> & {
177
+ /** Keeps the indicator unhidden while nothing is open so CSS can animate it
178
+ * out rather than having it vanish.
179
+ * @default false */
180
+ forceMount?: boolean;
181
+ /** Renders the child element instead of the default `<div>`, merging the
182
+ * `data-*` state hooks and the geometry custom properties onto it via
183
+ * {@link Slot}. Use it to make the marker an icon — an `<svg>` arrow, a
184
+ * `<Chevron />` component — rather than a styled box. The child must be a
185
+ * single React element that accepts a `ref`.
186
+ * @default false */
187
+ asChild?: boolean;
188
+ };
189
+
190
+ /**
191
+ * Props for {@link NavigationMenuLink | `NavigationMenu.Link`} — all native
192
+ * `<a>` attributes plus `active` and the `asChild` escape hatch.
193
+ *
194
+ * @extends HTMLAnchorElement
195
+ */
196
+ export type NavigationMenuLinkProps = ComponentProps<"a"> & {
197
+ /** Marks this link as the page the user is currently on, setting
198
+ * `aria-current="page"` and the `data-active` styling hook. The component
199
+ * does no route matching of its own — the consumer owns the router, so it
200
+ * owns the comparison.
201
+ * @default false */
202
+ active?: boolean;
203
+ /** Renders the child element instead of a native `<a>`, merging all of
204
+ * `NavigationMenu.Link`'s props — `aria-current`, `data-active`, the
205
+ * panel-dismissing click handler, `ref` — onto it via {@link Slot}. Use for
206
+ * routing-library link components.
207
+ * @default false */
208
+ asChild?: boolean;
209
+ };
210
+
211
+ /** Whether the surrounding subtree is inside a `NavigationMenu.Content`
212
+ * panel. A `NavigationMenu.Link` uses it to tell a top-level entry (which
213
+ * joins the arrow-key travel order) from a link inside a panel (which does
214
+ * not). */
215
+ export type NavigationMenuPanelContextValue = boolean;
216
+
217
+ /** The value shared by `NavigationMenu.Root` with its descendants. */
218
+ export type NavigationMenuContextValue = {
219
+ orientation: NavigationMenuOrientation;
220
+ dir: NavigationMenuReadingDirection;
221
+ navigationMenuId: string;
222
+ openValue: string;
223
+ /** The entry that was open immediately before {@link openValue}, or `""` when
224
+ * the menu was closed. Together with `entryKeys` this is what lets a panel
225
+ * derive its travel direction; see `getPanelMotion`. */
226
+ previousValue: string;
227
+ setOpenValue: (next: string) => void;
228
+ openOnHover: boolean;
229
+ /** Opens `value` after the hover-intent delay, or immediately when another
230
+ * panel is already open. */
231
+ openWithIntent: (value: string) => void;
232
+ /** Abandons a hover-intent open that hasn't fired yet. */
233
+ cancelOpen: () => void;
234
+ /** Adds (or, with `null`, removes) a top-level entry's element from the
235
+ * registry that arrow-key navigation walks. */
236
+ registerEntry: (key: string, element: HTMLElement | null) => void;
237
+ /** Registry keys in DOM order — the arrow-key travel order. */
238
+ entryKeys: string[];
239
+ /** Adds (or, with `null`, removes) a disclosure entry's `<li>` from the
240
+ * value-keyed registry. */
241
+ registerItem: (value: string, element: HTMLLIElement | null) => void;
242
+ /** The disclosure entries' `value`s in DOM order. Distinct from
243
+ * {@link NavigationMenuContextValue.entryKeys | `entryKeys`}, which is keyed by
244
+ * generated id so plain link entries can join it — this one is what a panel
245
+ * uses to work out which direction the open entry moved. */
246
+ itemValues: string[];
247
+ /** Moves DOM focus to a registered top-level entry. */
248
+ focusEntry: (key: string) => void;
249
+ /** The mounted `NavigationMenu.Viewport` element, or `null` when the nav
250
+ * doesn't use one. Panels project into it when it exists. */
251
+ viewport: HTMLDivElement | null;
252
+ /** Ref callback a `NavigationMenu.Viewport` hands its element to. */
253
+ registerViewport: (element: HTMLDivElement | null) => void;
254
+ };
255
+
256
+ /** The value shared by `NavigationMenu.Item` with its descendants. */
257
+ export type NavigationMenuItemContextValue = {
258
+ value: string | undefined;
259
+ };
@@ -0,0 +1,89 @@
1
+ import { deriveId } from "../utils/index.ts";
2
+
3
+ /** Derives the paired trigger / panel ids for one entry, so `aria-controls`
4
+ * and `aria-labelledby` line up without either side constructing the
5
+ * template literal itself. */
6
+ export function getTriggerAndPanelIds(
7
+ navigationMenuId: string,
8
+ value: string,
9
+ ): { triggerId: string; panelId: string } {
10
+ return {
11
+ triggerId: deriveId(navigationMenuId, "trigger", value),
12
+ panelId: deriveId(navigationMenuId, "panel", value),
13
+ };
14
+ }
15
+
16
+ /** Turns an id into a CSS `<custom-ident>`-safe fragment for use inside an
17
+ * `anchor-name`. `deriveId`'s output carries React 18's colon-bracketed
18
+ * `useId()` root (e.g. `:r0:-trigger-concepts`), and colons aren't valid
19
+ * there — each character outside `[A-Za-z0-9_-]` becomes a hyphen instead. */
20
+ export function toAnchorIdentFragment(id: string): string {
21
+ return id.replace(/[^A-Za-z0-9_-]/g, "-");
22
+ }
23
+
24
+ /** The `anchor-name` a disclosure trigger publishes, and the value the
25
+ * shared Viewport's `position-anchor` targets once that trigger's panel is
26
+ * open. Derived from the trigger's own id, so it stays unique per entry and
27
+ * per `NavigationMenu.Root` instance without any extra bookkeeping. */
28
+ export function getTriggerAnchorName(triggerId: string): string {
29
+ return `--primitiv-navigation-menu-trigger-${toAnchorIdentFragment(triggerId)}`;
30
+ }
31
+
32
+ /** Which way a panel is travelling as the open entry changes — the hook a
33
+ * stylesheet needs to slide panels in the direction of the pointer rather than
34
+ * cross-fading every switch. `from-*` is an entering panel, `to-*` a leaving
35
+ * one, and the suffix is the side it moves from / toward. */
36
+ export type NavigationMenuPanelMotion =
37
+ | "from-start"
38
+ | "from-end"
39
+ | "to-start"
40
+ | "to-end";
41
+
42
+ /**
43
+ * Derives a panel's motion from the disclosure-entry order (`itemValues`, in DOM
44
+ * order) and which entry was open before.
45
+ *
46
+ * Returns `undefined` — meaning "no direction, just fade" — for the two cases
47
+ * that genuinely have none: the first open (nothing to travel from) and the full
48
+ * close (nothing to travel to). Also for an entry missing from the registry,
49
+ * since a direction derived from an unknown position would be a guess.
50
+ */
51
+ export function getPanelMotion({
52
+ value,
53
+ openValue,
54
+ previousValue,
55
+ itemValues,
56
+ }: {
57
+ value: string;
58
+ openValue: string;
59
+ previousValue: string;
60
+ itemValues: readonly string[];
61
+ }): NavigationMenuPanelMotion | undefined {
62
+ const opening = value === openValue;
63
+ const closing = value === previousValue;
64
+ // A panel that is neither the current nor the outgoing one is a bystander.
65
+ if (!opening && !closing) return undefined;
66
+
67
+ // No `value !== ""` guard: the empty string is never a registered entry, so the
68
+ // lookup below rejects it on the same path as any other unknown value.
69
+ const selfIndex = itemValues.indexOf(value);
70
+ if (selfIndex === -1) return undefined;
71
+
72
+ if (opening) {
73
+ const fromIndex = itemValues.indexOf(previousValue);
74
+ if (fromIndex === -1) return undefined;
75
+ // Travelling toward the end means the new panel arrives from the end side.
76
+ // The two indices can only be equal if previousValue === openValue, and Root
77
+ // records a previous value only when the open value *changes* — so `<` and `<=`
78
+ // agree on every input that can actually reach here.
79
+ // Stryker disable next-line EqualityOperator: equivalent — see above.
80
+ return fromIndex < selfIndex ? "from-end" : "from-start";
81
+ }
82
+
83
+ const toIndex = itemValues.indexOf(openValue);
84
+ if (toIndex === -1) return undefined;
85
+ // The outgoing panel leaves opposite the direction of travel. Equal indices are
86
+ // unreachable for the same reason as the entering branch above.
87
+ // Stryker disable next-line EqualityOperator: equivalent — see above.
88
+ return toIndex > selfIndex ? "to-start" : "to-end";
89
+ }