@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,149 @@
1
+ import {
2
+ ReactNode,
3
+ useCallback,
4
+ useEffect,
5
+ useId,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+
11
+ import { useControllableState } from "../../hooks/index.ts";
12
+ import type { SelectContextValue } from "../SelectContext";
13
+
14
+ type UseSelectRootArgs = {
15
+ defaultOpen?: boolean;
16
+ open?: boolean;
17
+ onOpenChange?: (open: boolean) => void;
18
+ value?: string;
19
+ defaultValue?: string;
20
+ onValueChange?: (value: string) => void;
21
+ };
22
+
23
+ /**
24
+ * State owner for the rich (non-`native`) Select. Manages the open/close
25
+ * state of the listbox popover — controlled (`open` + `onOpenChange`) or
26
+ * uncontrolled (`defaultOpen`) — and derives the trigger/content ids.
27
+ * Mirrors {@link useDropdownRoot}, including the `openRef` de-dupe so a
28
+ * single light-dismiss that fires both a `toggle` event and our own close
29
+ * does not double-notify `onOpenChange`.
30
+ */
31
+ export function useSelectRoot({
32
+ defaultOpen = false,
33
+ open: controlledOpen,
34
+ onOpenChange,
35
+ value: controlledValue,
36
+ defaultValue,
37
+ onValueChange,
38
+ }: UseSelectRootArgs): {
39
+ contextValue: SelectContextValue;
40
+ value: string;
41
+ itemValues: string[];
42
+ } {
43
+ const contentId = useId();
44
+ const triggerId = useId();
45
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
46
+ const [open, setOpenBase] = useControllableState<boolean>(
47
+ controlledOpen,
48
+ defaultOpen,
49
+ onOpenChange,
50
+ );
51
+ const [value, setValue] = useControllableState<string>(
52
+ controlledValue,
53
+ defaultValue ?? "",
54
+ onValueChange,
55
+ );
56
+
57
+ const openRef = useRef(open);
58
+ useEffect(() => {
59
+ openRef.current = open;
60
+ });
61
+
62
+ const setOpen = useCallback(
63
+ (next: boolean) => {
64
+ if (openRef.current === next) return;
65
+ openRef.current = next;
66
+ setOpenBase(next);
67
+ },
68
+ [setOpenBase],
69
+ );
70
+
71
+ // Commit a selection: update the value, close the listbox, and return
72
+ // focus to the trigger (the standard listbox close-on-select behaviour).
73
+ const select = useCallback(
74
+ (next: string) => {
75
+ setValue(next);
76
+ setOpenBase(false);
77
+ openRef.current = false;
78
+ // Stryker disable next-line OptionalChaining: unreachable — the trigger is always mounted when an item is selected (selection happens inside the open Content).
79
+ triggerRef.current?.focus();
80
+ },
81
+ [setValue, setOpenBase],
82
+ );
83
+
84
+ // Item registry — lets Select.Value mirror the selected item's content
85
+ // (written once on the Item). Children live in a ref (updated silently
86
+ // every render, no re-render churn); `itemVersion` bumps only when the set
87
+ // of registered values changes (mount/unmount), so Value re-renders when
88
+ // items appear or disappear. See the react-component-patterns skill.
89
+ const itemChildrenRef = useRef<Map<string, ReactNode>>(new Map());
90
+ const [itemVersion, setItemVersion] = useState(0);
91
+
92
+ const registerItem = useCallback((itemValue: string, node: ReactNode) => {
93
+ const isNew = !itemChildrenRef.current.has(itemValue);
94
+ itemChildrenRef.current.set(itemValue, node);
95
+ // Stryker disable next-line ArithmeticOperator,ArrowFunction: equivalent — the version is an opaque re-render trigger; any updater (v+1, v-1, or `() => undefined`) changes state and re-renders identically.
96
+ if (isNew) setItemVersion((v) => v + 1);
97
+ }, []);
98
+
99
+ const unregisterItem = useCallback((itemValue: string) => {
100
+ itemChildrenRef.current.delete(itemValue);
101
+ // Stryker disable next-line ArithmeticOperator,ArrowFunction: equivalent — the version is an opaque re-render trigger; any updater changes state and re-renders identically.
102
+ setItemVersion((v) => v + 1);
103
+ }, []);
104
+
105
+ const getItemChildren = useCallback(
106
+ (itemValue: string) => itemChildrenRef.current.get(itemValue),
107
+ [],
108
+ );
109
+
110
+ // Registered values, recomputed whenever items mount/unmount, so Root can
111
+ // render a matching <option> in the hidden form <select>.
112
+ const itemValues = useMemo(
113
+ () => Array.from(itemChildrenRef.current.keys()),
114
+ // eslint-disable-next-line react-hooks/exhaustive-deps
115
+ [itemVersion],
116
+ );
117
+
118
+ const contextValue = useMemo(
119
+ () => ({
120
+ open,
121
+ setOpen,
122
+ value,
123
+ select,
124
+ registerItem,
125
+ unregisterItem,
126
+ getItemChildren,
127
+ contentId,
128
+ triggerId,
129
+ triggerRef,
130
+ }),
131
+ // itemVersion is intentionally in the dep list (not the value) so the
132
+ // context identity changes when items mount/unmount, re-rendering
133
+ // Select.Value even though the callbacks are stable.
134
+ [
135
+ open,
136
+ setOpen,
137
+ value,
138
+ select,
139
+ registerItem,
140
+ unregisterItem,
141
+ getItemChildren,
142
+ contentId,
143
+ triggerId,
144
+ itemVersion,
145
+ ],
146
+ );
147
+
148
+ return { contextValue, value, itemValues };
149
+ }
@@ -1,15 +1,24 @@
1
- import { ChangeEventHandler, ComponentProps, ReactNode, Ref } from "react";
1
+ import {
2
+ ChangeEventHandler,
3
+ ComponentProps,
4
+ HTMLAttributes,
5
+ ReactNode,
6
+ Ref,
7
+ } from "react";
2
8
 
3
9
  /**
4
10
  * Shared base for both {@link SelectRootProps} variants — the native
5
- * `<select>` attributes (minus the state-owning ones), the `asChild`
6
- * escape hatch, and the raw `onChange` passthrough.
11
+ * `<select>` attributes (minus the state-owning and multiple-selection
12
+ * ones), the `asChild` escape hatch, and the raw `onChange` passthrough.
7
13
  */
8
14
  export type SelectRootBaseProps = Omit<
9
15
  ComponentProps<"select">,
10
16
  "value" | "defaultValue" | "multiple" | "onChange"
11
17
  > & {
18
+ /** Content of the select — typically {@link SelectOption}, {@link SelectGroup},
19
+ * and optionally a leading {@link SelectPlaceholder}. */
12
20
  children?: ReactNode;
21
+ /** Forwarded to the underlying `HTMLSelectElement`. */
13
22
  ref?: Ref<HTMLSelectElement>;
14
23
  /**
15
24
  * Native `change` handler. Fires alongside `onValueChange` whenever the
@@ -20,11 +29,52 @@ export type SelectRootBaseProps = Omit<
20
29
  /**
21
30
  * When `true`, Root delegates to a single consumer-supplied element
22
31
  * (expected to render a `<select>`) and merges its own props onto it
23
- * via the {@link Slot} pattern. The placeholder-detection inside Root
24
- * walks direct children only, so placeholder + `asChild` requires the
25
- * consumer to set `defaultValue=""` explicitly.
32
+ * via the {@link Slot} pattern. Placeholder-detection inside Root
33
+ * walks direct children only in this mode, so the `asChild` +
34
+ * placeholder combination requires the consumer to set
35
+ * `defaultValue=""` explicitly.
36
+ * @default false
26
37
  */
27
38
  asChild?: boolean;
39
+ /**
40
+ * Selects the render path:
41
+ *
42
+ * - `false` (the default) — the **rich** render path: a fully-styleable
43
+ * Popover-API listbox built from {@link SelectTrigger | `Select.Trigger`},
44
+ * {@link SelectValue | `Select.Value`},
45
+ * {@link SelectContent | `Select.Content`} and
46
+ * {@link SelectItem | `Select.Item`}. Icons and other rich content on an
47
+ * item render as authored.
48
+ * - `true` — the **native** render path: a thin wrapper over a real
49
+ * `<select>` / `<option>` / `<optgroup>`, for flat, OS-native cases
50
+ * (mobile wheel pickers, maximum-compatibility forms). Under `native`,
51
+ * {@link SelectItem | `Select.Item`} renders an `<option>` from its
52
+ * string/number children only — element children (icons, indicators)
53
+ * are dropped.
54
+ *
55
+ * Both modes share the same `value` / `onValueChange` / `disabled` / form
56
+ * `name` API.
57
+ * @default false
58
+ */
59
+ native?: boolean;
60
+ /**
61
+ * Rich mode only — whether the listbox popover is open on first render.
62
+ * Ignored under `native` (the browser owns the popup). Uncontrolled: pass
63
+ * this (or omit for closed) and let the component manage the state.
64
+ * @default false
65
+ */
66
+ defaultOpen?: boolean;
67
+ /**
68
+ * Rich mode only — the controlled open state of the listbox popover. Pass
69
+ * together with {@link SelectRootBaseProps.onOpenChange | `onOpenChange`}
70
+ * to own the state from the parent.
71
+ */
72
+ open?: boolean;
73
+ /**
74
+ * Rich mode only — called with the next open state whenever the listbox
75
+ * opens or closes (trigger click, selection, Escape, light-dismiss).
76
+ */
77
+ onOpenChange?: (open: boolean) => void;
28
78
  };
29
79
 
30
80
  /**
@@ -33,8 +83,15 @@ export type SelectRootBaseProps = Omit<
33
83
  * and `value` is forbidden.
34
84
  */
35
85
  export type SelectRootUncontrolledProps = SelectRootBaseProps & {
86
+ /** Value of the option selected on first render. When omitted and a
87
+ * {@link SelectPlaceholder} is present among Root's direct children,
88
+ * Root infers `""` automatically so the placeholder is the initial
89
+ * selection. */
36
90
  defaultValue?: string;
91
+ /** Forbidden in uncontrolled mode — use `defaultValue` instead. */
37
92
  value?: never;
93
+ /** Called with the new option value whenever the user changes the
94
+ * selection. Optional in uncontrolled mode. */
38
95
  onValueChange?: (value: string) => void;
39
96
  };
40
97
 
@@ -44,61 +101,172 @@ export type SelectRootUncontrolledProps = SelectRootBaseProps & {
44
101
  * forbidden.
45
102
  */
46
103
  export type SelectRootControlledProps = SelectRootBaseProps & {
104
+ /** Forbidden in controlled mode — use `value` instead. */
47
105
  defaultValue?: never;
106
+ /** The currently selected option value. Must be kept in sync by the
107
+ * parent via `onValueChange`. */
48
108
  value: string;
109
+ /** Called with the new option value whenever the user changes the
110
+ * selection. Required in controlled mode. */
49
111
  onValueChange: (value: string) => void;
50
112
  };
51
113
 
52
114
  /**
53
- * Props for {@link Select.Root}.
54
- *
55
- * Two state modes are statically discriminated at the type level so only
56
- * one shape is accepted by TypeScript:
115
+ * Props for {@link SelectRoot | `Select.Root`}.
57
116
  *
58
- * - **Uncontrolled** — pass `defaultValue` (or omit it). The browser owns
59
- * the selection state. `onValueChange` is optional.
60
- * - **Controlled** — pass `value` and `onValueChange` together. The
61
- * parent owns the selection; the component defers every transition
62
- * back through the callback.
63
- *
64
- * Native `multiple`-selection mode is not supported in v1.
117
+ * Resolves to either {@link SelectRootUncontrolledProps} or
118
+ * {@link SelectRootControlledProps} — only one shape is accepted by
119
+ * TypeScript at a time. Native `multiple`-selection mode is not supported
120
+ * in v1.
65
121
  */
66
122
  export type SelectRootProps =
67
123
  | SelectRootUncontrolledProps
68
124
  | SelectRootControlledProps;
69
125
 
70
126
  /**
71
- * Props for {@link Select.Option} — all `OptionHTMLAttributes` on the
72
- * underlying `<option>` element, plus a typed `ref`.
127
+ * Props for {@link SelectItem | `Select.Item`}.
128
+ *
129
+ * `Select.Item` renders different elements per mode — an `<option>` under
130
+ * `native`, a `<div role="option">` in rich mode — so it extends the
131
+ * element-agnostic `HTMLAttributes<HTMLElement>` rather than an
132
+ * option-specific type.
73
133
  */
74
- export type SelectOptionProps = ComponentProps<"option"> & {
134
+ export type SelectItemProps = HTMLAttributes<HTMLElement> & {
135
+ /** The value this item represents; committed as the Select's value when
136
+ * chosen, and matched against the current value for the selected state. */
137
+ value: string;
138
+ /** Marks the item unselectable while still visible. */
139
+ disabled?: boolean;
140
+ /** The item content. In `native` mode only its string/number parts are
141
+ * kept as the `<option>` text (element children are dropped); in rich mode
142
+ * arbitrary content (icons, indicators) renders as authored. */
75
143
  children?: ReactNode;
76
- ref?: Ref<HTMLOptionElement>;
77
144
  };
78
145
 
79
146
  /**
80
- * Props for {@link Select.Group} — all `OptgroupHTMLAttributes` on the
81
- * underlying `<optgroup>` element, plus a typed `ref`. `label` is
82
- * required by the native element and is what assistive technology
83
- * announces for the group.
147
+ * Props for {@link SelectGroup | `Select.Group`} — all
148
+ * `OptgroupHTMLAttributes` on the underlying `<optgroup>` element (with
149
+ * `label` narrowed to a required `string`), plus a typed `ref`.
84
150
  */
85
- export type SelectGroupProps = ComponentProps<"optgroup"> & {
151
+ export type SelectGroupProps = HTMLAttributes<HTMLElement> & {
152
+ /**
153
+ * The group heading. In `native` mode it is the `<optgroup>`'s `label`
154
+ * attribute; in rich mode it becomes the `<div role="group">`'s
155
+ * `aria-label`. Either way it is the group's accessible name. Required —
156
+ * a group without one is inaccessible.
157
+ */
86
158
  label: string;
159
+ /** The {@link SelectItem} elements belonging to this group. */
87
160
  children?: ReactNode;
88
- ref?: Ref<HTMLOptGroupElement>;
89
161
  };
90
162
 
91
163
  /**
92
- * Props for {@link Select.Placeholder}.
164
+ * Props for {@link SelectPlaceholder | `Select.Placeholder`}.
93
165
  *
94
- * `value`, `disabled`, and `hidden` are owned by the component — the
95
- * placeholder always has `value=""`, is always disabled, and is always
96
- * hidden from the dropdown — so they can't be set by the consumer.
166
+ * `value`, `disabled`, and `hidden` are owned by the component —
167
+ * the placeholder always renders with `value=""`, `disabled`, and `hidden`
168
+ * — so they are excluded from this type and cannot be set by the consumer.
97
169
  */
98
170
  export type SelectPlaceholderProps = Omit<
99
171
  ComponentProps<"option">,
100
172
  "value" | "disabled" | "hidden"
101
173
  > & {
174
+ /** The placeholder hint text shown in the closed select before the user
175
+ * makes a selection (e.g. `"Choose a fruit…"`). */
102
176
  children?: ReactNode;
177
+ /** Forwarded to the underlying `HTMLOptionElement`. */
103
178
  ref?: Ref<HTMLOptionElement>;
104
179
  };
180
+
181
+ /**
182
+ * Props for {@link SelectTrigger | `Select.Trigger`} — the rich-mode button
183
+ * that opens the listbox. Extends the native `<button>` attributes.
184
+ */
185
+ export type SelectTriggerProps = ComponentProps<"button"> & {
186
+ /** Trigger content — typically a {@link SelectValue | `Select.Value`}. */
187
+ children?: ReactNode;
188
+ /**
189
+ * Render the composed child element instead of a `<button>`, merging the
190
+ * trigger's ARIA attributes and click handler via the {@link Slot}
191
+ * pattern.
192
+ * @default false
193
+ */
194
+ asChild?: boolean;
195
+ /** Forwarded to the underlying `HTMLButtonElement`. */
196
+ ref?: Ref<HTMLButtonElement>;
197
+ };
198
+
199
+ /**
200
+ * Props for {@link SelectValue | `Select.Value`} — the rich-mode element,
201
+ * placed inside {@link SelectTrigger | `Select.Trigger`}, that displays the
202
+ * current selection (mirrored from the selected item in a later cycle) or a
203
+ * placeholder when nothing is selected. Extends `<span>` attributes minus
204
+ * `children`, which the component owns.
205
+ */
206
+ export type SelectValueProps = Omit<ComponentProps<"span">, "children"> & {
207
+ /** Shown when no value is selected (e.g. `"Select a framework…"`). */
208
+ placeholder?: ReactNode;
209
+ /** Forwarded to the underlying `HTMLSpanElement`. */
210
+ ref?: Ref<HTMLSpanElement>;
211
+ };
212
+
213
+ /**
214
+ * Props for {@link SelectItemIndicator | `Select.ItemIndicator`} — the
215
+ * selection mark inside a rich {@link SelectItem | `Select.Item`}. Extends
216
+ * the native `<span>` attributes.
217
+ */
218
+ export type SelectItemIndicatorProps = ComponentProps<"span"> & {
219
+ /** The mark to render (e.g. a checkmark glyph or icon). */
220
+ children?: ReactNode;
221
+ /**
222
+ * Render the composed child element instead of a `<span>` via the
223
+ * {@link Slot} pattern.
224
+ * @default false
225
+ */
226
+ asChild?: boolean;
227
+ /**
228
+ * Keep the indicator mounted even when the item is not selected (it still
229
+ * exposes `data-state="unchecked"`), useful for CSS enter/exit animation.
230
+ * @default false
231
+ */
232
+ forceMount?: boolean;
233
+ /** Forwarded to the underlying `HTMLSpanElement`. */
234
+ ref?: Ref<HTMLSpanElement>;
235
+ };
236
+
237
+ /**
238
+ * Props for {@link SelectContent | `Select.Content`} — the rich-mode
239
+ * Popover-API listbox panel. Extends the native `<div>` attributes.
240
+ */
241
+ export type SelectContentProps = ComponentProps<"div"> & {
242
+ /** Listbox content — {@link SelectItem | `Select.Item`} /
243
+ * {@link SelectGroup | `Select.Group`} elements. */
244
+ children?: ReactNode;
245
+ /**
246
+ * Render the composed child element instead of a `<div>`, merging the
247
+ * listbox props via the {@link Slot} pattern.
248
+ * @default false
249
+ */
250
+ asChild?: boolean;
251
+ /** Forwarded to the underlying `HTMLDivElement`. */
252
+ ref?: Ref<HTMLDivElement>;
253
+ };
254
+
255
+ /**
256
+ * Props for {@link SelectSeparator | `Select.Separator`} — a non-interactive
257
+ * divider between groups of options in rich mode. Extends the native `<div>`
258
+ * attributes and adds `asChild`.
259
+ */
260
+ export type SelectSeparatorProps = Omit<ComponentProps<"div">, "role"> & {
261
+ /** Optional content; separators are usually empty, and only render (via
262
+ * the {@link Slot} pattern) when `asChild` composes onto a custom element. */
263
+ children?: ReactNode;
264
+ /** Forwarded to the underlying `HTMLDivElement`. */
265
+ ref?: Ref<HTMLDivElement>;
266
+ /**
267
+ * Render the composed child element (with separator semantics) instead of
268
+ * the default `<div role="separator">`.
269
+ * @default false
270
+ */
271
+ asChild?: boolean;
272
+ };
@@ -5,29 +5,42 @@ import { SkipNavContentProps, SkipNavLinkProps } from "./types";
5
5
  const DEFAULT_CONTENT_ID = "primitiv-skip-nav";
6
6
 
7
7
  /**
8
- * The skip link itself — an `<a>` that jumps keyboard and screen-reader
9
- * users past repeated navigation straight to the main content.
8
+ * The skip link — an `<a>` that jumps keyboard and screen-reader users past
9
+ * repeated navigation straight to the main content.
10
10
  *
11
11
  * Place it as the very first focusable element on the page (typically the
12
- * first child of `<body>`). It is a normal in-page anchor: its `href` is a
12
+ * first child of `<body>`). It is a plain in-page anchor: its `href` is a
13
13
  * URL fragment pointing at the `id` of the matching {@link SkipNavContent |
14
14
  * `SkipNav.Content`}, so following it moves focus there with no JavaScript.
15
15
  *
16
16
  * **`contentId`.** The `href` is derived as `#${contentId}` and defaults to
17
- * a shared id, so an unconfigured `SkipNav.Link` / `SkipNav.Content` pair
18
- * works out of the box. Override `contentId` on the link only if you also
19
- * set a matching `id` on the content.
17
+ * a shared id (`"primitiv-skip-nav"`), so an unconfigured
18
+ * `SkipNav.Link` / `SkipNav.Content` pair works out of the box. Override
19
+ * `contentId` on the link only if you also set a matching `id` on
20
+ * {@link SkipNavContent | `SkipNav.Content`}.
20
21
  *
21
- * **Visibility.** The link ships with no styles. The conventional pattern
22
- * is to keep it visually hidden until focused — see the component README
23
- * for the canonical CSS.
22
+ * **Visibility.** No styles ship with the component. The conventional
23
+ * pattern keeps the link visually hidden until it receives focus — position
24
+ * it off-screen by default and restore it on `:focus`. Do **not** use
25
+ * `display: none` or `visibility: hidden`, as those remove it from the tab
26
+ * order entirely:
27
+ *
28
+ * ```css
29
+ * a[href="#primitiv-skip-nav"] { position: absolute; left: -9999px; }
30
+ * a[href="#primitiv-skip-nav"]:focus { left: 0; top: 0; }
31
+ * ```
32
+ *
33
+ * Alternatively, pass a `className` and target that class in your
34
+ * stylesheet.
35
+ *
36
+ * @extends HTMLAnchorElement
24
37
  *
25
38
  * @example Default — shared content id
26
39
  * ```tsx
27
40
  * <SkipNav.Link>Skip to main content</SkipNav.Link>
28
41
  * ```
29
42
  *
30
- * @example Custom content id
43
+ * @example Custom content id (must match the `id` on `SkipNav.Content`)
31
44
  * ```tsx
32
45
  * <SkipNav.Link contentId="main">Skip to main content</SkipNav.Link>
33
46
  * ```
@@ -47,23 +60,36 @@ function SkipNavLink({
47
60
  SkipNavLink.displayName = "SkipNavLink";
48
61
 
49
62
  /**
50
- * The skip target — a `<div>` placed at the start of the main content that
51
- * {@link SkipNavLink | `SkipNav.Link`} jumps to.
63
+ * The skip target — a `<div tabIndex={-1}>` placed at the start of the main
64
+ * content that {@link SkipNavLink | `SkipNav.Link`} jumps to.
52
65
  *
53
- * It carries `tabIndex={-1}` so it is a valid focus destination for an
54
- * in-page fragment navigation: when the link is followed, the browser
55
- * scrolls to this element *and* moves focus into it, so the next Tab press
56
- * continues from the main content rather than the top of the page.
66
+ * `tabIndex={-1}` makes the element a valid programmatic focus destination:
67
+ * when the link is followed the browser scrolls to this element *and* moves
68
+ * focus into it, so the next <kbd>Tab</kbd> press continues from within the
69
+ * main content rather than cycling back to the top of the page. No
70
+ * JavaScript runs; it is pure anchor navigation.
57
71
  *
58
- * **`id`.** Defaults to the same shared id as `SkipNav.Link`'s `contentId`.
59
- * Pass a custom `id` only if you set a matching `contentId` on the link.
72
+ * **`id`.** Defaults to the same shared id (`"primitiv-skip-nav"`) as
73
+ * {@link SkipNavLink | `SkipNav.Link`}'s `contentId`. Pass a custom `id`
74
+ * via the native `id` prop only if you set a matching `contentId` on the
75
+ * link — the spread props override the default, so the two always stay in
76
+ * sync.
60
77
  *
61
- * @example
78
+ * @extends HTMLDivElement
79
+ *
80
+ * @example Default — wraps the main page content
62
81
  * ```tsx
63
82
  * <SkipNav.Content>
64
83
  * <main>…</main>
65
84
  * </SkipNav.Content>
66
85
  * ```
86
+ *
87
+ * @example Custom id (must match `contentId` on `SkipNav.Link`)
88
+ * ```tsx
89
+ * <SkipNav.Content id="main">
90
+ * <main>…</main>
91
+ * </SkipNav.Content>
92
+ * ```
67
93
  */
68
94
  function SkipNavContent({
69
95
  children,
@@ -81,18 +107,22 @@ SkipNavContent.displayName = "SkipNavContent";
81
107
  /**
82
108
  * Headless, accessible **Skip Nav** — a "skip to main content" link and its
83
109
  * focus target, letting keyboard and screen-reader users bypass repeated
84
- * navigation.
110
+ * navigation. Implements the
111
+ * [WCAG 2.4.1 Bypass Blocks](https://www.w3.org/WAI/WCAG22/Understanding/bypass-blocks.html)
112
+ * technique with no JavaScript — pure in-page anchor navigation.
85
113
  *
86
- * It is a stateless pair of sub-components, used as siblings rather than
87
- * nested:
114
+ * `SkipNav` is a stateless pair of sub-components used as siblings (not
115
+ * nested). Render the link first; place the content wrapper at the start of
116
+ * the main content area:
88
117
  *
89
- * - {@link SkipNavLink | `SkipNav.Link`} — the `<a>` skip link; render it
90
- * first on the page.
91
- * - {@link SkipNavContent | `SkipNav.Content`} — the focusable `<div>`
92
- * target wrapping the main content.
118
+ * - {@link SkipNavLink | `SkipNav.Link`} — the `<a>` skip link; must be
119
+ * the first focusable element on the page.
120
+ * - {@link SkipNavContent | `SkipNav.Content`} — the `<div tabIndex={-1}>`
121
+ * target; wrap it around (or immediately before) your main content.
93
122
  *
94
- * The two connect by a shared content id with a sensible default, so the
95
- * common case needs no configuration.
123
+ * The two sub-components are connected by a shared content id
124
+ * (`"primitiv-skip-nav"`) with no configuration required. Override both
125
+ * sides when you need a custom id.
96
126
  *
97
127
  * @example Minimal usage
98
128
  * ```tsx
@@ -111,7 +141,17 @@ SkipNavContent.displayName = "SkipNavContent";
111
141
  * }
112
142
  * ```
113
143
  *
114
- * @see {@link SkipNavLink} for the `contentId` contract and the visually-hidden-until-focused CSS pattern.
144
+ * @example Custom id — both sides must match
145
+ * ```tsx
146
+ * <SkipNav.Link contentId="main">Skip to main content</SkipNav.Link>
147
+ * <SkipNav.Content id="main">
148
+ * <main>…</main>
149
+ * </SkipNav.Content>
150
+ * ```
151
+ *
152
+ * @see {@link SkipNavLink} for the `contentId` contract and the
153
+ * visually-hidden-until-focused CSS pattern.
154
+ * @see {@link SkipNavContent} for the `tabIndex={-1}` focus-target behaviour.
115
155
  */
116
156
  const SkipNav = {
117
157
  Link: SkipNavLink,
@@ -20,4 +20,10 @@ describe("SkipNav component", () => {
20
20
  expect(content).toHaveAttribute("id", "primitiv-skip-nav");
21
21
  expect(content).toHaveAttribute("tabindex", "-1");
22
22
  });
23
+
24
+ it("sets displayName on each sub-component for React DevTools and stack traces", () => {
25
+ // Assert — an empty displayName would render each as anonymous.
26
+ expect(SkipNav.Link.displayName).toBe("SkipNavLink");
27
+ expect(SkipNav.Content.displayName).toBe("SkipNavContent");
28
+ });
23
29
  });
@@ -1,26 +1,34 @@
1
1
  import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
2
2
 
3
3
  /**
4
- * Props for {@link SkipNav.Link} — `children`, an optional `contentId`,
5
- * plus all `AnchorHTMLAttributes` on the rendered `<a>`.
4
+ * Props for {@link SkipNavLink | `SkipNav.Link`} — `children`, an optional
5
+ * `contentId`, plus all native `<a>` attributes.
6
6
  */
7
7
  export type SkipNavLinkProps = {
8
+ /** The visible link text (e.g. `"Skip to main content"`). */
8
9
  children?: ReactNode;
9
10
  /**
10
- * Id of the matching {@link SkipNav.Content} target. The link's `href`
11
- * is derived as `#${contentId}`. Defaults to a shared id, so an
12
- * unconfigured `SkipNav.Link` / `SkipNav.Content` pair works as-is.
11
+ * Id of the matching {@link SkipNavContent | `SkipNav.Content`} target.
12
+ * The link's `href` is derived as `#${contentId}`. Defaults to the shared
13
+ * id (`"primitiv-skip-nav"`), so an unconfigured
14
+ * `SkipNav.Link` / `SkipNav.Content` pair works out of the box. Override
15
+ * only when you also set a matching native `id` prop on `SkipNav.Content`.
16
+ * @default "primitiv-skip-nav"
13
17
  */
14
18
  contentId?: string;
15
19
  } & AnchorHTMLAttributes<HTMLAnchorElement>;
16
20
 
17
21
  /**
18
- * Props for {@link SkipNav.Content} — `children` plus all `HTMLAttributes`
19
- * on the rendered `<div>`.
22
+ * Props for {@link SkipNavContent | `SkipNav.Content`} — `children` plus
23
+ * all native `<div>` attributes.
20
24
  *
21
- * Pass a custom `id` only if you set a matching `contentId` on
22
- * {@link SkipNav.Link}; it defaults to the same shared id.
25
+ * The component always renders with `tabIndex={-1}` (making it a valid
26
+ * programmatic focus target) and sets `id` to `"primitiv-skip-nav"` by
27
+ * default. Pass a native `id` prop to override the default; when you do,
28
+ * set the same value as `contentId` on {@link SkipNavLink | `SkipNav.Link`}
29
+ * so the anchor's `href` resolves correctly.
23
30
  */
24
31
  export type SkipNavContentProps = {
32
+ /** The main page content that follows the skip target. */
25
33
  children?: ReactNode;
26
34
  } & HTMLAttributes<HTMLDivElement>;