@primitiv-ui/react 0.1.27 → 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,38 @@
1
+ import { ReactNode, RefObject } from "react";
2
+
3
+ import { createStrictContext } from "../utils/index.ts";
4
+
5
+ /**
6
+ * Context shared by the rich (non-`native`) Select sub-components —
7
+ * {@link SelectTrigger}, {@link SelectContent}, and (in later cycles)
8
+ * {@link SelectValue} / {@link SelectItem}. Provided by the rich render
9
+ * path of {@link SelectRoot}. The `native` path does not provide it, so
10
+ * the strict hook throwing outside a rich `<Select.Root>` is correct.
11
+ */
12
+ export type SelectContextValue = {
13
+ /** Whether the listbox popover is open. */
14
+ open: boolean;
15
+ /** Open/close the listbox. De-dupes repeat transitions. */
16
+ setOpen: (open: boolean) => void;
17
+ /** The currently selected item value (`""` when nothing is selected). */
18
+ value: string;
19
+ /** Commit a selection: sets the value, closes the listbox, focuses the trigger. */
20
+ select: (value: string) => void;
21
+ /** Register (or refresh) a rich item's content for value-mirroring. */
22
+ registerItem: (value: string, children: ReactNode) => void;
23
+ /** Remove a rich item's registration on unmount. */
24
+ unregisterItem: (value: string) => void;
25
+ /** Read the registered content for a value (used by Select.Value). */
26
+ getItemChildren: (value: string) => ReactNode | undefined;
27
+ /** `id` of the listbox content, wired to the trigger's `aria-controls`. */
28
+ contentId: string;
29
+ /** `id` of the trigger, available for labelling. */
30
+ triggerId: string;
31
+ /** Ref to the trigger button so focus can return to it on close. */
32
+ triggerRef: RefObject<HTMLButtonElement | null>;
33
+ };
34
+
35
+ export const [SelectContext, useSelectContext] =
36
+ createStrictContext<SelectContextValue>(
37
+ "Select rich sub-components (Trigger, Value, Content, Item) must be rendered inside a <Select.Root>.",
38
+ );
@@ -0,0 +1,29 @@
1
+ import { createStrictContext } from "../utils/index.ts";
2
+
3
+ /**
4
+ * Context published by a rich {@link SelectItem} to its
5
+ * {@link SelectItemIndicator} child, carrying whether the item is currently
6
+ * selected so the indicator can render (and expose `data-state`).
7
+ */
8
+ export type SelectItemIndicatorContextValue = {
9
+ /** Whether the enclosing item is the selected one. */
10
+ selected: boolean;
11
+ /**
12
+ * `true` while the subtree is being rendered by {@link SelectValue} as the
13
+ * mirrored copy of the selected item's content, rather than inside the item
14
+ * itself. Indicators render nothing there: the mark answers "which row is
15
+ * selected", which is redundant on the trigger it already represents.
16
+ *
17
+ * The mirror cannot identify indicators by element type — a styled layer
18
+ * wraps `Select.ItemIndicator` in its own component (the registry does), and
19
+ * any such wrapper is opaque to a `child.type` test. Publishing the intent
20
+ * through context instead lets the indicator opt itself out at any nesting
21
+ * depth.
22
+ */
23
+ mirrored: boolean;
24
+ };
25
+
26
+ export const [SelectItemIndicatorContext, useSelectItemIndicatorContext] =
27
+ createStrictContext<SelectItemIndicatorContextValue>(
28
+ "Select.ItemIndicator must be rendered inside a <Select.Item>.",
29
+ );
@@ -18,9 +18,9 @@ describe("Select asChild", () => {
18
18
  it("delegates to a consumer-supplied <select> wrapper, merging Root's props onto it", () => {
19
19
  // Arrange & Act
20
20
  render(
21
- <Select.Root asChild disabled>
21
+ <Select.Root native asChild disabled>
22
22
  <StyledSelect>
23
- <Select.Option value="apple">Apple</Select.Option>
23
+ <Select.Item value="apple">Apple</Select.Item>
24
24
  </StyledSelect>
25
25
  </Select.Root>,
26
26
  );
@@ -3,15 +3,34 @@ import { render, screen } from "@testing-library/react";
3
3
  import { Select } from "../Select";
4
4
 
5
5
  describe("Select basic rendering", () => {
6
- it("renders an <option> for each Select.Option child so the value is in the DOM", () => {
6
+ it("renders an <option> for each Select.Item child so the value is in the DOM", () => {
7
7
  // Arrange & Act
8
8
  render(
9
- <Select.Root>
10
- <Select.Option value="apple">Apple</Select.Option>
9
+ <Select.Root native>
10
+ <Select.Item value="apple">Apple</Select.Item>
11
11
  </Select.Root>,
12
12
  );
13
13
 
14
14
  // Assert
15
15
  expect(screen.getByRole("option", { name: "Apple" })).toBeInTheDocument();
16
16
  });
17
+
18
+ it("sets a displayName on the compound and each sub-component", () => {
19
+ // Assert — empty displayNames would render each as anonymous in DevTools.
20
+ // `Select`, `Select.Root` and the underlying function are one object
21
+ // (Object.assign compound), so the compound's "Select" alias is the
22
+ // observable Root displayName; the sub-components are distinct objects.
23
+ // SelectPlaceholder's name is load-bearing beyond DevTools: Root detects a
24
+ // placeholder among its direct children by it (see the placeholder tests),
25
+ // and so must any consumer wrapper that wants that inference.
26
+ expect(Select.displayName).toBe("Select");
27
+ expect(Select.Trigger.displayName).toBe("SelectTrigger");
28
+ expect(Select.Value.displayName).toBe("SelectValue");
29
+ expect(Select.Content.displayName).toBe("SelectContent");
30
+ expect(Select.Item.displayName).toBe("SelectItem");
31
+ expect(Select.ItemIndicator.displayName).toBe("SelectItemIndicator");
32
+ expect(Select.Group.displayName).toBe("SelectGroup");
33
+ expect(Select.Separator.displayName).toBe("SelectSeparator");
34
+ expect(Select.Placeholder.displayName).toBe("SelectPlaceholder");
35
+ });
17
36
  });
@@ -17,9 +17,9 @@ describe("Select controlled state", () => {
17
17
  setValue(next);
18
18
  };
19
19
  return (
20
- <Select.Root value={value} onValueChange={handle}>
21
- <Select.Option value="apple">Apple</Select.Option>
22
- <Select.Option value="banana">Banana</Select.Option>
20
+ <Select.Root native value={value} onValueChange={handle}>
21
+ <Select.Item value="apple">Apple</Select.Item>
22
+ <Select.Item value="banana">Banana</Select.Item>
23
23
  </Select.Root>
24
24
  );
25
25
  }
@@ -43,7 +43,7 @@ describe("Select controlled state", () => {
43
43
  function Wrapper() {
44
44
  const [value, setValue] = useState("apple");
45
45
  return (
46
- <Select.Root
46
+ <Select.Root native
47
47
  value={value}
48
48
  onChange={(event) => {
49
49
  onChange(event.target.value);
@@ -51,8 +51,8 @@ describe("Select controlled state", () => {
51
51
  }}
52
52
  onValueChange={onValueChange}
53
53
  >
54
- <Select.Option value="apple">Apple</Select.Option>
55
- <Select.Option value="banana">Banana</Select.Option>
54
+ <Select.Item value="apple">Apple</Select.Item>
55
+ <Select.Item value="banana">Banana</Select.Item>
56
56
  </Select.Root>
57
57
  );
58
58
  }
@@ -6,8 +6,8 @@ describe("Select data attributes", () => {
6
6
  it("renders data-disabled='' on the root <select> when disabled is true so CSS can style the disabled state", () => {
7
7
  // Arrange & Act
8
8
  render(
9
- <Select.Root disabled>
10
- <Select.Option value="apple">Apple</Select.Option>
9
+ <Select.Root native disabled>
10
+ <Select.Item value="apple">Apple</Select.Item>
11
11
  </Select.Root>,
12
12
  );
13
13
 
@@ -18,8 +18,8 @@ describe("Select data attributes", () => {
18
18
  it("omits data-disabled entirely when disabled is false or absent so the enabled state has nothing to override", () => {
19
19
  // Arrange & Act
20
20
  render(
21
- <Select.Root>
22
- <Select.Option value="apple">Apple</Select.Option>
21
+ <Select.Root native>
22
+ <Select.Item value="apple">Apple</Select.Item>
23
23
  </Select.Root>,
24
24
  );
25
25
 
@@ -7,8 +7,8 @@ function renderSelect(children?: React.ReactNode) {
7
7
  return (
8
8
  <>
9
9
  {children}
10
- <Select.Option value="apple">Apple</Select.Option>
11
- <Select.Option value="banana">Banana</Select.Option>
10
+ <Select.Item value="apple">Apple</Select.Item>
11
+ <Select.Item value="banana">Banana</Select.Item>
12
12
  </>
13
13
  );
14
14
  }
@@ -18,7 +18,7 @@ describe("Select — Field integration", () => {
18
18
  // Arrange & Act
19
19
  render(
20
20
  <Field.Root id="fruit">
21
- <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
21
+ <Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
22
22
  </Field.Root>,
23
23
  );
24
24
 
@@ -33,7 +33,7 @@ describe("Select — Field integration", () => {
33
33
  // Arrange & Act
34
34
  render(
35
35
  <Field.Root id="fruit">
36
- <Select.Root id="my-fruit" aria-label="Fruit">
36
+ <Select.Root native id="my-fruit" aria-label="Fruit">
37
37
  {renderSelect()}
38
38
  </Select.Root>
39
39
  </Field.Root>,
@@ -50,7 +50,7 @@ describe("Select — Field integration", () => {
50
50
  // Arrange & Act
51
51
  render(
52
52
  <Field.Root id="fruit">
53
- <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
53
+ <Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
54
54
  </Field.Root>,
55
55
  );
56
56
 
@@ -65,7 +65,7 @@ describe("Select — Field integration", () => {
65
65
  // Arrange & Act
66
66
  render(
67
67
  <Field.Root id="fruit" invalid>
68
- <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
68
+ <Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
69
69
  </Field.Root>,
70
70
  );
71
71
 
@@ -81,7 +81,7 @@ describe("Select — Field integration", () => {
81
81
  // Arrange & Act
82
82
  render(
83
83
  <Field.Root id="fruit">
84
- <Select.Root aria-label="Fruit" aria-describedby="extra-hint">
84
+ <Select.Root native aria-label="Fruit" aria-describedby="extra-hint">
85
85
  {renderSelect()}
86
86
  </Select.Root>
87
87
  </Field.Root>,
@@ -99,7 +99,7 @@ describe("Select — Field integration", () => {
99
99
  // Arrange & Act
100
100
  render(
101
101
  <Field.Root id="fruit" invalid>
102
- <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
102
+ <Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
103
103
  </Field.Root>,
104
104
  );
105
105
 
@@ -114,7 +114,7 @@ describe("Select — Field integration", () => {
114
114
  // Arrange & Act
115
115
  render(
116
116
  <Field.Root id="fruit" disabled>
117
- <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
117
+ <Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
118
118
  </Field.Root>,
119
119
  );
120
120
 
@@ -126,7 +126,7 @@ describe("Select — Field integration", () => {
126
126
  // Arrange & Act
127
127
  render(
128
128
  <Field.Root id="fruit" required>
129
- <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
129
+ <Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>
130
130
  </Field.Root>,
131
131
  );
132
132
 
@@ -137,7 +137,7 @@ describe("Select — Field integration", () => {
137
137
  it("Select outside Field.Root behaves identically to before — no field-derived attributes", () => {
138
138
  // Arrange & Act
139
139
  render(
140
- <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>,
140
+ <Select.Root native aria-label="Fruit">{renderSelect()}</Select.Root>,
141
141
  );
142
142
  const select = screen.getByRole("combobox", { name: "Fruit" });
143
143
 
@@ -0,0 +1,38 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Select } from "../Select";
5
+
6
+ describe("Select rich form integration", () => {
7
+ it("submits the selected value through a hidden native <select>", async () => {
8
+ const user = userEvent.setup();
9
+ let submitted: string | null = null;
10
+ render(
11
+ <form
12
+ onSubmit={(event) => {
13
+ event.preventDefault();
14
+ submitted = new FormData(event.currentTarget).get(
15
+ "framework",
16
+ ) as string;
17
+ }}
18
+ >
19
+ <Select.Root name="framework">
20
+ <Select.Trigger>
21
+ <Select.Value placeholder="Pick" />
22
+ </Select.Trigger>
23
+ <Select.Content>
24
+ <Select.Item value="react">React</Select.Item>
25
+ <Select.Item value="vue">Vue</Select.Item>
26
+ </Select.Content>
27
+ </Select.Root>
28
+ <button type="submit">Submit</button>
29
+ </form>,
30
+ );
31
+
32
+ await user.click(screen.getByRole("button", { name: "Pick" }));
33
+ await user.click(screen.getByText("Vue"));
34
+ await user.click(screen.getByRole("button", { name: "Submit" }));
35
+
36
+ expect(submitted).toBe("vue");
37
+ });
38
+ });
@@ -6,13 +6,13 @@ describe("Select group", () => {
6
6
  it("renders an <optgroup> with the given label so options can be visually grouped", () => {
7
7
  // Arrange & Act
8
8
  render(
9
- <Select.Root>
9
+ <Select.Root native>
10
10
  <Select.Group label="Fruits">
11
- <Select.Option value="apple">Apple</Select.Option>
12
- <Select.Option value="banana">Banana</Select.Option>
11
+ <Select.Item value="apple">Apple</Select.Item>
12
+ <Select.Item value="banana">Banana</Select.Item>
13
13
  </Select.Group>
14
14
  <Select.Group label="Vegetables">
15
- <Select.Option value="carrot">Carrot</Select.Option>
15
+ <Select.Item value="carrot">Carrot</Select.Item>
16
16
  </Select.Group>
17
17
  </Select.Root>,
18
18
  );
@@ -28,9 +28,9 @@ describe("Select group", () => {
28
28
  it("nests its Option children inside the rendered <optgroup>", () => {
29
29
  // Arrange & Act
30
30
  render(
31
- <Select.Root>
31
+ <Select.Root native>
32
32
  <Select.Group label="Fruits">
33
- <Select.Option value="apple">Apple</Select.Option>
33
+ <Select.Item value="apple">Apple</Select.Item>
34
34
  </Select.Group>
35
35
  </Select.Root>,
36
36
  );
@@ -0,0 +1,95 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Select } from "../Select";
5
+
6
+ function renderSelect() {
7
+ return render(
8
+ <Select.Root>
9
+ <Select.Trigger>
10
+ <Select.Value placeholder="Pick one" />
11
+ </Select.Trigger>
12
+ <Select.Content>
13
+ <Select.Item value="react">React</Select.Item>
14
+ <Select.Item value="vue">Vue</Select.Item>
15
+ <Select.Item value="solid">Solid</Select.Item>
16
+ </Select.Content>
17
+ </Select.Root>,
18
+ );
19
+ }
20
+
21
+ describe("Select keyboard edge cases", () => {
22
+ it("focuses the listbox itself and no-ops arrow keys when there are no options", async () => {
23
+ const user = userEvent.setup();
24
+ render(
25
+ <Select.Root>
26
+ <Select.Trigger>
27
+ <Select.Value placeholder="Empty" />
28
+ </Select.Trigger>
29
+ <Select.Content />
30
+ </Select.Root>,
31
+ );
32
+
33
+ await user.click(screen.getByRole("button"));
34
+ const list = screen.getByRole("listbox", { hidden: true });
35
+ expect(list).toHaveFocus();
36
+
37
+ await user.keyboard("{ArrowDown}");
38
+ expect(list).toHaveFocus();
39
+ expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
40
+ });
41
+
42
+ it("ignores unhandled multi-character keys", async () => {
43
+ const user = userEvent.setup();
44
+ renderSelect();
45
+
46
+ await user.click(screen.getByRole("button"));
47
+ await user.keyboard("{Shift}"); // key.length > 1, not a nav/select key → no-op
48
+
49
+ expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
50
+ expect(screen.getByRole("option", { name: "React", hidden: true })).toHaveFocus();
51
+ });
52
+
53
+ it("Enter is a no-op when focus is on the listbox rather than an option", async () => {
54
+ const user = userEvent.setup();
55
+ renderSelect();
56
+
57
+ await user.click(screen.getByRole("button"));
58
+ const list = screen.getByRole("listbox", { hidden: true });
59
+ list.focus();
60
+ await user.keyboard("{Enter}");
61
+
62
+ expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true");
63
+ });
64
+
65
+ it("typeahead handles a repeated character and a no-option starting point", async () => {
66
+ const user = userEvent.setup();
67
+ renderSelect();
68
+
69
+ await user.click(screen.getByRole("button"));
70
+ screen.getByRole("listbox", { hidden: true }).focus();
71
+
72
+ await user.keyboard("s"); // from the listbox (no option focused) → Solid
73
+ expect(screen.getByRole("option", { name: "Solid", hidden: true })).toHaveFocus();
74
+
75
+ await user.keyboard("s"); // repeated char → cycles, stays on the only "s" option
76
+ expect(screen.getByRole("option", { name: "Solid", hidden: true })).toHaveFocus();
77
+ });
78
+ });
79
+
80
+ describe("Select typeahead reset", () => {
81
+ it("clears the accumulated query after the reset window", async () => {
82
+ const user = userEvent.setup();
83
+ renderSelect();
84
+
85
+ await user.click(screen.getByRole("button"));
86
+ await user.keyboard("s"); // → Solid
87
+ expect(screen.getByRole("option", { name: "Solid", hidden: true })).toHaveFocus();
88
+
89
+ // Wait past the typeahead reset window, then start a fresh search.
90
+ await new Promise((resolve) => setTimeout(resolve, 600));
91
+
92
+ await user.keyboard("v"); // fresh query → Vue
93
+ expect(screen.getByRole("option", { name: "Vue", hidden: true })).toHaveFocus();
94
+ });
95
+ });
@@ -0,0 +1,124 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { vi } from "vitest";
4
+
5
+ import { Select } from "../Select";
6
+
7
+ function renderSelect(props: Parameters<typeof Select.Root>[0] = {}) {
8
+ return render(
9
+ <Select.Root {...props}>
10
+ <Select.Trigger>
11
+ <Select.Value placeholder="Pick one" />
12
+ </Select.Trigger>
13
+ <Select.Content>
14
+ <Select.Item value="react">React</Select.Item>
15
+ <Select.Item value="vue">Vue</Select.Item>
16
+ <Select.Item value="svelte" disabled>
17
+ Svelte
18
+ </Select.Item>
19
+ <Select.Item value="solid">Solid</Select.Item>
20
+ </Select.Content>
21
+ </Select.Root>,
22
+ );
23
+ }
24
+
25
+ function option(name: RegExp | string) {
26
+ return screen.getByRole("option", { name, hidden: true });
27
+ }
28
+
29
+ describe("Select keyboard navigation", () => {
30
+ it("focuses the first enabled option on open when nothing is selected", async () => {
31
+ const user = userEvent.setup();
32
+ renderSelect();
33
+
34
+ await user.click(screen.getByRole("button"));
35
+
36
+ expect(option("React")).toHaveFocus();
37
+ });
38
+
39
+ it("focuses the selected option on open", async () => {
40
+ const user = userEvent.setup();
41
+ renderSelect({ defaultValue: "vue" });
42
+
43
+ await user.click(screen.getByRole("button"));
44
+
45
+ expect(option("Vue")).toHaveFocus();
46
+ });
47
+
48
+ it("moves focus with ArrowDown/ArrowUp, skipping disabled and wrapping", async () => {
49
+ const user = userEvent.setup();
50
+ renderSelect();
51
+
52
+ await user.click(screen.getByRole("button"));
53
+ await user.keyboard("{ArrowDown}"); // React -> Vue
54
+ expect(option("Vue")).toHaveFocus();
55
+
56
+ await user.keyboard("{ArrowDown}"); // Vue -> Solid (skips disabled Svelte)
57
+ expect(option("Solid")).toHaveFocus();
58
+
59
+ await user.keyboard("{ArrowDown}"); // Solid -> React (wrap)
60
+ expect(option("React")).toHaveFocus();
61
+
62
+ await user.keyboard("{ArrowUp}"); // React -> Solid (wrap back)
63
+ expect(option("Solid")).toHaveFocus();
64
+
65
+ await user.keyboard("{ArrowUp}"); // Solid -> Vue (previous, no wrap)
66
+ expect(option("Vue")).toHaveFocus();
67
+ });
68
+
69
+ it("jumps to first/last enabled option with Home/End", async () => {
70
+ const user = userEvent.setup();
71
+ renderSelect();
72
+
73
+ await user.click(screen.getByRole("button"));
74
+ await user.keyboard("{End}");
75
+ expect(option("Solid")).toHaveFocus();
76
+
77
+ await user.keyboard("{Home}");
78
+ expect(option("React")).toHaveFocus();
79
+ });
80
+
81
+ it("selects the focused option with Enter and closes", async () => {
82
+ const user = userEvent.setup();
83
+ const onValueChange = vi.fn();
84
+ renderSelect({ onValueChange });
85
+
86
+ await user.click(screen.getByRole("button"));
87
+ await user.keyboard("{ArrowDown}"); // focus Vue
88
+ await user.keyboard("{Enter}");
89
+
90
+ expect(onValueChange).toHaveBeenCalledWith("vue");
91
+ expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "false");
92
+ });
93
+
94
+ it("selects the first option with Enter when it is the focused one", async () => {
95
+ const user = userEvent.setup();
96
+ const onValueChange = vi.fn();
97
+ render(
98
+ <Select.Root onValueChange={onValueChange}>
99
+ <Select.Trigger>
100
+ <Select.Value placeholder="Pick one" />
101
+ </Select.Trigger>
102
+ <Select.Content>
103
+ <Select.Item value="react">React</Select.Item>
104
+ <Select.Item value="vue">Vue</Select.Item>
105
+ </Select.Content>
106
+ </Select.Root>,
107
+ );
108
+
109
+ await user.click(screen.getByRole("button")); // opens, focuses React (index 0)
110
+ await user.keyboard("{Enter}"); // index 0 is a valid selection, not "< 0"
111
+
112
+ expect(onValueChange).toHaveBeenCalledWith("react");
113
+ });
114
+
115
+ it("typeahead focuses the option whose label starts with the typed text", async () => {
116
+ const user = userEvent.setup();
117
+ renderSelect();
118
+
119
+ await user.click(screen.getByRole("button"));
120
+ await user.keyboard("so");
121
+
122
+ expect(option("Solid")).toHaveFocus();
123
+ });
124
+ });