@primitiv-ui/react 0.1.28 → 0.1.30

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 (456) hide show
  1. package/README.md +19 -2
  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 +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -1,20 +1,58 @@
1
- import { ChangeEvent, Children, isValidElement, ReactNode } from "react";
1
+ import {
2
+ ChangeEvent,
3
+ Children,
4
+ isValidElement,
5
+ ReactNode,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ } from "react";
2
10
  import type { ReactElement } from "react";
3
11
 
4
12
  import { useFieldProps } from "../Field/hooks/index.ts";
5
- import { Slot } from "../Slot/index.ts";
13
+ import { composeEventHandlers, Slot } from "../Slot/index.ts";
6
14
 
15
+ import { SelectContext, useSelectContext } from "./SelectContext";
16
+ import {
17
+ SelectItemIndicatorContext,
18
+ useSelectItemIndicatorContext,
19
+ } from "./SelectItemIndicatorContext";
20
+ import { useSelectContent, useSelectRoot } from "./hooks/index.ts";
7
21
  import {
22
+ SelectContentProps,
8
23
  SelectGroupProps,
9
- SelectOptionProps,
24
+ SelectItemIndicatorProps,
25
+ SelectItemProps,
10
26
  SelectPlaceholderProps,
11
27
  SelectRootProps,
28
+ SelectSeparatorProps,
29
+ SelectTriggerProps,
30
+ SelectValueProps,
12
31
  } from "./types";
13
32
 
33
+ const ITEM_INDICATOR_DISPLAY_NAME = "SelectItemIndicator";
34
+
35
+ /** Stable context for {@link SelectValue}'s mirrored copy — see `mirrored`. */
36
+ const MIRRORED_CONTEXT = { selected: false, mirrored: true } as const;
37
+
38
+ /** Off-screen but still submitted — the rich mode's hidden form `<select>`. */
39
+ const VISUALLY_HIDDEN = {
40
+ position: "absolute",
41
+ width: 1,
42
+ height: 1,
43
+ padding: 0,
44
+ margin: -1,
45
+ overflow: "hidden",
46
+ clip: "rect(0, 0, 0, 0)",
47
+ whiteSpace: "nowrap",
48
+ border: 0,
49
+ } as const;
50
+
14
51
  const PLACEHOLDER_DISPLAY_NAME = "SelectPlaceholder";
15
52
 
16
53
  function hasPlaceholderChild(children: ReactNode): boolean {
17
54
  return Children.toArray(children).some((child) => {
55
+ // Stryker disable next-line BooleanLiteral: equivalent — a non-element child is never a placeholder; returning true would infer defaultValue="", but with no matching <option value=""> the browser still selects the first option, so the rendered selection is unchanged.
18
56
  if (!isValidElement(child)) return false;
19
57
  const type = child.type as { displayName?: string };
20
58
  return type.displayName === PLACEHOLDER_DISPLAY_NAME;
@@ -22,46 +60,137 @@ function hasPlaceholderChild(children: ReactNode): boolean {
22
60
  }
23
61
 
24
62
  /**
25
- * The root of a Select — renders a native `<select>` element and passes
26
- * all `SelectHTMLAttributes` through to the DOM.
63
+ * The root of a Select — owns the selection (and, in rich mode, the open)
64
+ * state and provides it to the sub-components.
27
65
  *
28
- * Browser-native behaviour is preserved: keyboard navigation (arrow keys,
29
- * Home/End, typeahead), the platform popup, mobile UX, and form
30
- * submission all work without additional JS.
66
+ * **Two render paths, one API** — chosen by {@link SelectRootBaseProps.native | `native`}:
31
67
  *
32
- * Supports two state modes, statically discriminated at the type level:
68
+ * - `native={false}` (**default**) — the rich Popover-API listbox, composed
69
+ * from {@link SelectTrigger}, {@link SelectValue}, {@link SelectContent},
70
+ * {@link SelectItem} and {@link SelectItemIndicator}. Items carry arbitrary
71
+ * content; a visually-hidden `<select name>` is rendered for form
72
+ * submission.
73
+ * - `native={true}` — a thin wrapper over a real `<select>` (implicit
74
+ * `role="combobox"`); the browser owns the popup, keyboard, mobile UX and
75
+ * form submission. {@link SelectItem} becomes an `<option>` (string/number
76
+ * children only), {@link SelectGroup} an `<optgroup>`.
33
77
  *
34
- * - **Uncontrolled** — pass `defaultValue` (or omit it). The browser owns
35
- * the selection. `onValueChange` is optional.
36
- * - **Controlled** — pass `value` and `onValueChange` together. Every
37
- * transition defers back through `onValueChange`.
78
+ * **Controlled vs uncontrolled.** Two selection modes are statically
79
+ * discriminated at the type level so only one shape is accepted by
80
+ * TypeScript at a time:
38
81
  *
39
- * `onValueChange` receives the new selection as a plain string. The
40
- * consumer's own `onChange` (the raw `ChangeEvent`) still fires alongside
41
- * it.
82
+ * - **Uncontrolled** — pass {@link SelectRootUncontrolledProps.defaultValue | `defaultValue`}
83
+ * (or omit it). The browser owns the selection. `onValueChange` is optional.
84
+ * - **Controlled** — pass {@link SelectRootControlledProps.value | `value`}
85
+ * and {@link SelectRootControlledProps.onValueChange | `onValueChange`} together.
86
+ * Every transition defers back through `onValueChange`, which receives the
87
+ * new selection as a plain string. The consumer's own `onChange` (the raw
88
+ * `ChangeEvent`) still fires alongside it.
42
89
  *
43
- * **Placeholder integration.** When a {@link Select.Placeholder} appears
44
- * among the direct children and neither `value` nor `defaultValue` is
45
- * set, Root infers `defaultValue=""` so the placeholder — not the first
46
- * selectable option — is the initial selection.
90
+ * **Placeholder integration.** When a {@link SelectPlaceholder | `Select.Placeholder`}
91
+ * appears among the direct children and neither `value` nor `defaultValue`
92
+ * is set, Root infers `defaultValue=""` so the placeholder — not the first
93
+ * selectable option — is the initial selection. Pair with `required` to
94
+ * make the browser's native form validation catch an unchosen value at
95
+ * submission.
47
96
  *
48
97
  * **Field integration.** When rendered inside a `<Field.Root>`, Select
49
98
  * opts into `FieldContext` and inherits `id`, `aria-describedby`,
50
- * `aria-invalid`, `disabled`, and `required` from the field. Any prop
51
- * the consumer passes wins; `aria-describedby` is composed (consumer
52
- * ids first, then field-supplied description / error ids). Outside a
53
- * `<Field.Root>`, behaviour is unchanged.
99
+ * `aria-invalid`, `disabled`, and `required` from the field. Any prop the
100
+ * consumer passes wins; `aria-describedby` is composed (consumer ids first,
101
+ * then field-supplied description / error ids). Outside a `<Field.Root>`,
102
+ * behaviour is unchanged.
103
+ *
104
+ * **`asChild` composition.** Pass `asChild` to delegate rendering to a
105
+ * single consumer-supplied element (e.g. a styled `<select>` wrapper).
106
+ * Root's `onChange`, `data-disabled`, `value` / `defaultValue`, and other
107
+ * native attributes are merged onto the child via the {@link Slot} pattern.
108
+ * Placeholder-detection walks direct children only in this mode, so
109
+ * `asChild` + placeholder requires the consumer to set `defaultValue=""`
110
+ * explicitly.
111
+ *
112
+ * **Styling hooks.**
113
+ * - `data-disabled=""` — present on the `<select>` when `disabled` is set,
114
+ * so CSS can target `[data-disabled]` without relying on `:disabled`.
115
+ *
116
+ * Root renders no single semantic element of its own in the default (rich)
117
+ * path — it is a context boundary that also emits a visually-hidden form
118
+ * `<select>`. Under `native` it renders the real `<select>`.
119
+ *
120
+ * @example Rich (default)
121
+ * ```tsx
122
+ * <Select.Root value={framework} onValueChange={setFramework}>
123
+ * <Select.Trigger>
124
+ * <Select.Value placeholder="Pick a framework..." />
125
+ * </Select.Trigger>
126
+ * <Select.Content>
127
+ * <Select.Item value="react">
128
+ * <ReactIcon />
129
+ * React
130
+ * <Select.ItemIndicator>✓</Select.ItemIndicator>
131
+ * </Select.Item>
132
+ * <Select.Item value="vue">
133
+ * <VueIcon />
134
+ * Vue
135
+ * <Select.ItemIndicator>✓</Select.ItemIndicator>
136
+ * </Select.Item>
137
+ * </Select.Content>
138
+ * </Select.Root>
139
+ * ```
140
+ *
141
+ * @example Native
142
+ * ```tsx
143
+ * <Select.Root native defaultValue="apple" aria-label="Pick a fruit">
144
+ * <Select.Placeholder>Choose...</Select.Placeholder>
145
+ * <Select.Group label="Fruits">
146
+ * <Select.Item value="apple">Apple</Select.Item>
147
+ * <Select.Item value="banana">Banana</Select.Item>
148
+ * </Select.Group>
149
+ * </Select.Root>
150
+ * ```
151
+ *
152
+ * @example Field integration
153
+ * ```tsx
154
+ * <Field.Root invalid={!!errors.framework}>
155
+ * <Field.Label>Framework</Field.Label>
156
+ * <Select.Root name="framework" value={value} onValueChange={onChange}>
157
+ * <Select.Trigger>
158
+ * <Select.Value placeholder="Choose..." />
159
+ * </Select.Trigger>
160
+ * <Select.Content>
161
+ * <Select.Item value="react">React</Select.Item>
162
+ * </Select.Content>
163
+ * </Select.Root>
164
+ * <Field.ErrorText>{errors.framework?.message}</Field.ErrorText>
165
+ * </Field.Root>
166
+ * ```
54
167
  */
55
168
  export function SelectRoot({
56
169
  children,
170
+ native = false,
57
171
  asChild = false,
58
172
  onChange,
59
173
  onValueChange,
60
174
  value,
61
175
  defaultValue,
176
+ defaultOpen,
177
+ open,
178
+ onOpenChange,
62
179
  ...consumer
63
180
  }: SelectRootProps): ReactElement {
64
181
  const merged = useFieldProps(consumer);
182
+ const {
183
+ contextValue,
184
+ value: selectedValue,
185
+ itemValues,
186
+ } = useSelectRoot({
187
+ defaultOpen,
188
+ open,
189
+ onOpenChange,
190
+ value,
191
+ defaultValue,
192
+ onValueChange,
193
+ });
65
194
 
66
195
  const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
67
196
  onChange?.(event);
@@ -90,6 +219,37 @@ export function SelectRoot({
90
219
  onChange: handleChange,
91
220
  };
92
221
 
222
+ // Rich (non-native) render path — the fully-styleable Popover listbox.
223
+ // Root provides open/selection state to Trigger / Content / Value via
224
+ // context, and renders a visually-hidden native <select> so the selection
225
+ // still submits through the browser (mirroring what `native` gets free).
226
+ // The hidden select is uncontrolled + remounted by `key` on each change,
227
+ // so it needs no onChange and stays warning-free.
228
+ if (!native) {
229
+ return (
230
+ <SelectContext.Provider value={contextValue}>
231
+ {children}
232
+ <select
233
+ key={selectedValue}
234
+ name={merged.name}
235
+ defaultValue={selectedValue}
236
+ required={merged.required}
237
+ disabled={merged.disabled}
238
+ tabIndex={-1}
239
+ aria-hidden
240
+ style={VISUALLY_HIDDEN}
241
+ >
242
+ <option value="" />
243
+ {itemValues
244
+ .filter((itemValue) => itemValue !== "")
245
+ .map((itemValue) => (
246
+ <option key={itemValue} value={itemValue} />
247
+ ))}
248
+ </select>
249
+ </SelectContext.Provider>
250
+ );
251
+ }
252
+
93
253
  if (asChild) {
94
254
  return <Slot {...rootProps}>{children}</Slot>;
95
255
  }
@@ -100,46 +260,390 @@ export function SelectRoot({
100
260
  SelectRoot.displayName = "SelectRoot";
101
261
 
102
262
  /**
103
- * An individual choice inside a Select — renders a native `<option>`
104
- * element and passes all `OptionHTMLAttributes` through to the DOM.
263
+ * An individual choice inside a Select.
264
+ *
265
+ * In `native` mode (the case handled in this cycle) it renders a native
266
+ * `<option>` element with an implicit `role="option"`. Because an
267
+ * `<option>` can only hold text, `Select.Item` keeps **only the
268
+ * string/number parts** of its children — joined as the option's visible
269
+ * text — and **drops every element child** (icons, indicators). This is
270
+ * the inverse of the string-vs-element split used elsewhere in the
271
+ * library, and it means an icon-only item with no text renders an empty,
272
+ * unlabelled `<option>` under `native`.
273
+ *
274
+ * Pass `disabled` to make a single choice unreachable from the dropdown
275
+ * while still visible.
276
+ *
277
+ * @extends HTMLOptionElement
278
+ *
279
+ * @example Native
280
+ * ```tsx
281
+ * <Select.Root native>
282
+ * <Select.Item value="apple">Apple</Select.Item>
283
+ * <Select.Item value="durian" disabled>Durian (sold out)</Select.Item>
284
+ * </Select.Root>
285
+ * ```
286
+ */
287
+ export function SelectItem({
288
+ children,
289
+ value,
290
+ disabled,
291
+ onClick,
292
+ ...rest
293
+ }: SelectItemProps): ReactElement {
294
+ const ctx = useContext(SelectContext);
295
+ const registerItem = ctx?.registerItem;
296
+ const unregisterItem = ctx?.unregisterItem;
297
+ const selected = ctx?.value === value;
298
+
299
+ // Keep the registered content current every render (silent — no bump).
300
+ useEffect(() => {
301
+ registerItem?.(value, children);
302
+ });
303
+ // Drop the registration when the item unmounts or changes value.
304
+ useEffect(() => () => unregisterItem?.(value), [unregisterItem, value]);
305
+
306
+ const indicatorContext = useMemo(
307
+ () => ({ selected, mirrored: false }),
308
+ [selected],
309
+ );
310
+
311
+ // Native mode — no rich context is provided, so render an <option> whose
312
+ // text is only the string/number children (element children are dropped).
313
+ if (!ctx) {
314
+ const text = Children.toArray(children).filter(
315
+ (child): child is string | number =>
316
+ typeof child === "string" || typeof child === "number",
317
+ );
318
+ return (
319
+ <option value={value} disabled={disabled} onClick={onClick} {...rest}>
320
+ {text}
321
+ </option>
322
+ );
323
+ }
324
+
325
+ // Rich mode — a listbox option that can carry arbitrary content. Clicking
326
+ // it (when enabled) commits the selection and closes the listbox.
327
+ const handleClick = () => {
328
+ if (disabled) return;
329
+ ctx.select(value);
330
+ };
331
+ const itemProps = {
332
+ ...rest,
333
+ role: "option" as const,
334
+ tabIndex: -1,
335
+ "aria-selected": selected,
336
+ "aria-disabled": disabled || undefined,
337
+ "data-disabled": disabled ? "" : undefined,
338
+ "data-state": selected ? "checked" : "unchecked",
339
+ onClick: composeEventHandlers(onClick, handleClick),
340
+ };
341
+ return (
342
+ <SelectItemIndicatorContext.Provider value={indicatorContext}>
343
+ <div {...itemProps}>{children}</div>
344
+ </SelectItemIndicatorContext.Provider>
345
+ );
346
+ }
347
+
348
+ /** @internal */
349
+ SelectItem.displayName = "SelectItem";
350
+
351
+ /**
352
+ * The rich-mode trigger button that opens the listbox. Exposes the
353
+ * WAI-ARIA listbox-combobox contract: `aria-haspopup="listbox"`,
354
+ * `aria-expanded` reflecting the open state, and `aria-controls` pointing
355
+ * at the {@link SelectContent | `Select.Content`} id. Place a
356
+ * {@link SelectValue | `Select.Value`} inside it to show the current
357
+ * selection.
358
+ *
359
+ * Renders a `<button type="button">` by default; pass `asChild` to compose
360
+ * the ARIA attributes and click handler onto a consumer element via the
361
+ * {@link Slot} pattern.
362
+ *
363
+ * @extends HTMLButtonElement
364
+ *
365
+ * @example
366
+ * ```tsx
367
+ * <Select.Trigger>
368
+ * <Select.Value placeholder="Pick a framework..." />
369
+ * </Select.Trigger>
370
+ * ```
371
+ */
372
+ export function SelectTrigger({
373
+ children,
374
+ onClick,
375
+ asChild = false,
376
+ ...rest
377
+ }: SelectTriggerProps): ReactElement {
378
+ const { open, setOpen, contentId, triggerId, triggerRef } = useSelectContext();
379
+
380
+ const triggerProps = {
381
+ ...rest,
382
+ ref: triggerRef,
383
+ id: triggerId,
384
+ type: "button" as const,
385
+ "aria-haspopup": "listbox" as const,
386
+ "aria-expanded": open,
387
+ "aria-controls": contentId,
388
+ onClick: composeEventHandlers(onClick, () => setOpen(!open)),
389
+ };
390
+
391
+ if (asChild) {
392
+ return <Slot {...triggerProps}>{children}</Slot>;
393
+ }
394
+ return <button {...triggerProps}>{children}</button>;
395
+ }
396
+
397
+ /** @internal */
398
+ SelectTrigger.displayName = "SelectTrigger";
399
+
400
+ /**
401
+ * The rich-mode display of the current selection, placed inside a
402
+ * {@link SelectTrigger | `Select.Trigger`}. Shows the `placeholder` when
403
+ * nothing is selected; mirroring the selected item's content is layered on
404
+ * in a later cycle.
405
+ *
406
+ * Renders a `<span>`.
407
+ *
408
+ * **Styling hooks.**
409
+ * - `data-placeholder=""` — present while nothing is selected (the
410
+ * `placeholder` is what's rendered), absent as soon as an item's content is
411
+ * mirrored. This is the hook the styled layer uses to render the muted
412
+ * placeholder colour versus the filled one.
413
+ *
414
+ * @extends HTMLSpanElement
415
+ *
416
+ * @example
417
+ * ```tsx
418
+ * <Select.Value placeholder="Select a framework..." />
419
+ * ```
420
+ */
421
+ export function SelectValue({
422
+ placeholder,
423
+ ...rest
424
+ }: SelectValueProps): ReactElement {
425
+ const { value, getItemChildren } = useSelectContext();
426
+ const selectedChildren = value ? getItemChildren(value) : undefined;
427
+
428
+ // Mirror the selected item's content — icons, badges, text all carry through.
429
+ // Falls back to the placeholder when nothing is selected (or the selected
430
+ // value has no registered item yet).
431
+ //
432
+ // Indicators are excluded, but NOT by filtering on element type: a styled
433
+ // layer wraps Select.ItemIndicator in its own component, and such a wrapper is
434
+ // opaque to a `child.type` test — the indicator would then be mirrored into
435
+ // the trigger, render outside any Select.Item, and throw on the strict
436
+ // context. Instead the mirror publishes `mirrored: true` and each indicator
437
+ // opts itself out, however deeply it is nested.
438
+ const mirrored = selectedChildren ?? null;
439
+
440
+ return (
441
+ <span {...rest} data-placeholder={mirrored === null ? "" : undefined}>
442
+ {mirrored === null ? (
443
+ placeholder
444
+ ) : (
445
+ <SelectItemIndicatorContext.Provider value={MIRRORED_CONTEXT}>
446
+ {mirrored}
447
+ </SelectItemIndicatorContext.Provider>
448
+ )}
449
+ </span>
450
+ );
451
+ }
452
+
453
+ /** @internal */
454
+ SelectValue.displayName = "SelectValue";
455
+
456
+ /**
457
+ * The selection mark rendered inside a rich {@link SelectItem} — typically a
458
+ * checkmark. Reads its item's selected state from context and renders only
459
+ * when that item is selected (pass `forceMount` to keep it mounted for
460
+ * CSS/animation), exposing `data-state` (`"checked"` / `"unchecked"`).
461
+ *
462
+ * `Select.Value` deliberately excludes the indicator when mirroring the
463
+ * selection into the trigger.
464
+ *
465
+ * Renders a `<span>` by default; pass `asChild` to compose onto any element
466
+ * (commonly an SVG icon) via the {@link Slot} pattern.
105
467
  *
106
- * Native `<option>` only renders text; rich content (icons, descriptions)
107
- * is not supported.
468
+ * @extends HTMLSpanElement
469
+ *
470
+ * @example
471
+ * ```tsx
472
+ * <Select.Item value="react">
473
+ * <ReactIcon />
474
+ * React
475
+ * <Select.ItemIndicator>✓</Select.ItemIndicator>
476
+ * </Select.Item>
477
+ * ```
478
+ */
479
+ export function SelectItemIndicator({
480
+ children,
481
+ asChild = false,
482
+ forceMount = false,
483
+ ...rest
484
+ }: SelectItemIndicatorProps): ReactElement | null {
485
+ const { selected, mirrored } = useSelectItemIndicatorContext();
486
+
487
+ // The trigger's mirrored copy never shows the mark — not even under
488
+ // forceMount, whose job is keeping the mark mounted for CSS animation inside
489
+ // the row.
490
+ if (mirrored) return null;
491
+ if (!forceMount && !selected) return null;
492
+
493
+ const indicatorProps = {
494
+ ...rest,
495
+ "data-state": selected ? "checked" : "unchecked",
496
+ };
497
+ if (asChild) {
498
+ return <Slot {...indicatorProps}>{children}</Slot>;
499
+ }
500
+ return <span {...indicatorProps}>{children}</span>;
501
+ }
502
+
503
+ /** @internal */
504
+ SelectItemIndicator.displayName = ITEM_INDICATOR_DISPLAY_NAME;
505
+
506
+ /**
507
+ * The rich-mode listbox panel, rendered with the native
508
+ * [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
509
+ * (`popover="auto"`) — no portal, no floating-ui. The browser manages the
510
+ * top layer and light-dismiss (outside click / Escape). Opens and closes in
511
+ * sync with the trigger; on open, focus moves into the listbox, and Escape
512
+ * closes it and returns focus to the trigger.
513
+ *
514
+ * Renders a `<div role="listbox">` by default; pass `asChild` to compose the
515
+ * listbox props onto a consumer element via the {@link Slot} pattern.
516
+ *
517
+ * @extends HTMLDivElement
518
+ *
519
+ * @example
520
+ * ```tsx
521
+ * <Select.Content>
522
+ * <Select.Item value="react">React</Select.Item>
523
+ * <Select.Item value="vue">Vue</Select.Item>
524
+ * </Select.Content>
525
+ * ```
108
526
  */
109
- export function SelectOption({
527
+ export function SelectContent({
110
528
  children,
529
+ onKeyDown,
530
+ asChild = false,
111
531
  ...rest
112
- }: SelectOptionProps): ReactElement {
113
- return <option {...rest}>{children}</option>;
532
+ }: SelectContentProps): ReactElement {
533
+ const { contentProps } = useSelectContent({ onKeyDown, restProps: rest });
534
+
535
+ if (asChild) {
536
+ return <Slot {...contentProps}>{children}</Slot>;
537
+ }
538
+ return <div {...contentProps}>{children}</div>;
114
539
  }
115
540
 
116
541
  /** @internal */
117
- SelectOption.displayName = "SelectOption";
542
+ SelectContent.displayName = "SelectContent";
118
543
 
119
544
  /**
120
- * Visually groups related options inside the Select popup — renders a
121
- * native `<optgroup>` element. The `label` is shown by the browser as a
122
- * non-selectable heading and is announced as the group's accessible name.
545
+ * Visually groups related options. In `native` mode it renders an
546
+ * `<optgroup>` (the browser shows {@link SelectGroupProps.label | `label`}
547
+ * as a non-selectable heading); in rich mode it renders a
548
+ * `<div role="group">` with `aria-label={label}`. Either way the `label` is
549
+ * the group's accessible name.
550
+ *
551
+ * @example
552
+ * ```tsx
553
+ * <Select.Group label="Fruits">
554
+ * <Select.Item value="apple">Apple</Select.Item>
555
+ * <Select.Item value="banana">Banana</Select.Item>
556
+ * </Select.Group>
557
+ * ```
123
558
  */
124
559
  export function SelectGroup({
125
560
  children,
561
+ label,
126
562
  ...rest
127
563
  }: SelectGroupProps): ReactElement {
128
- return <optgroup {...rest}>{children}</optgroup>;
564
+ const ctx = useContext(SelectContext);
565
+ if (!ctx) {
566
+ return (
567
+ <optgroup label={label} {...rest}>
568
+ {children}
569
+ </optgroup>
570
+ );
571
+ }
572
+ return (
573
+ <div role="group" aria-label={label} {...rest}>
574
+ {children}
575
+ </div>
576
+ );
129
577
  }
130
578
 
131
579
  /** @internal */
132
580
  SelectGroup.displayName = "SelectGroup";
133
581
 
582
+ /**
583
+ * A visual divider between groups of options in rich mode — typically
584
+ * placed between two {@link SelectGroup | `Select.Group`} elements (or any
585
+ * two {@link SelectItem | `Select.Item`}s). Renders a `<div role="separator">`
586
+ * by default; pass `asChild` to render any element with separator semantics.
587
+ * Non-interactive — {@link SelectContent | `Select.Content`}'s keyboard
588
+ * navigation only ever targets `[role="option"]`, so it is skipped by arrow
589
+ * key traversal without any extra wiring.
590
+ *
591
+ * @extends HTMLDivElement
592
+ *
593
+ * @example
594
+ * ```tsx
595
+ * <Select.Item value="apple">Apple</Select.Item>
596
+ * <Select.Separator />
597
+ * <Select.Item value="banana">Banana</Select.Item>
598
+ * ```
599
+ */
600
+ export function SelectSeparator({
601
+ asChild = false,
602
+ children,
603
+ ...rest
604
+ }: SelectSeparatorProps): ReactElement {
605
+ const separatorProps = { ...rest, role: "separator" as const };
606
+
607
+ if (asChild) {
608
+ return <Slot {...separatorProps}>{children}</Slot>;
609
+ }
610
+
611
+ return <div {...separatorProps} />;
612
+ }
613
+
614
+ /** @internal */
615
+ SelectSeparator.displayName = "SelectSeparator";
616
+
134
617
  /**
135
618
  * A non-selectable hint shown as the initial selection of a Select.
136
619
  * Renders a native `<option value="" disabled hidden>` so the browser
137
- * displays it before the user picks anything but makes it unreachable
138
- * from the dropdown afterwards. Render it as the first child of
139
- * {@link Select.Root} (above any `Select.Option` or `Select.Group`).
620
+ * displays it in the closed control before the user picks anything but
621
+ * makes it unreachable from the dropdown afterwards.
140
622
  *
141
- * Pair with `required` on {@link Select.Root} to make the browser's
142
- * native form validation catch an unchosen value at submission.
623
+ * Always render it as the **first** child of
624
+ * {@link SelectRoot | `Select.Root`}, above any
625
+ * {@link SelectItem | `Select.Item`} or
626
+ * {@link SelectGroup | `Select.Group`}.
627
+ *
628
+ * When `Select.Placeholder` is present among Root's direct children and
629
+ * neither `value` nor `defaultValue` is set, Root automatically infers
630
+ * `defaultValue=""` so the placeholder — not the first selectable option
631
+ * — is the initial selection. Pair `required` on Root to make the
632
+ * browser's native form validation catch an unchosen value at submission.
633
+ *
634
+ * `value=""`, `disabled`, and `hidden` are fixed by the component and
635
+ * cannot be overridden. See {@link SelectPlaceholderProps}.
636
+ *
637
+ * @extends HTMLOptionElement
638
+ *
639
+ * @example
640
+ * ```tsx
641
+ * <Select.Root native required aria-label="Pick a fruit">
642
+ * <Select.Placeholder>Choose a fruit...</Select.Placeholder>
643
+ * <Select.Item value="apple">Apple</Select.Item>
644
+ * <Select.Item value="banana">Banana</Select.Item>
645
+ * </Select.Root>
646
+ * ```
143
647
  */
144
648
  export function SelectPlaceholder({
145
649
  children,
@@ -158,59 +662,82 @@ SelectPlaceholder.displayName = "SelectPlaceholder";
158
662
  /** Type of the {@link Select} compound: the root callable plus its attached sub-components. */
159
663
  export type TSelectCompound = typeof SelectRoot & {
160
664
  Root: typeof SelectRoot;
161
- Option: typeof SelectOption;
665
+ Trigger: typeof SelectTrigger;
666
+ Value: typeof SelectValue;
667
+ Content: typeof SelectContent;
668
+ Item: typeof SelectItem;
669
+ ItemIndicator: typeof SelectItemIndicator;
162
670
  Group: typeof SelectGroup;
671
+ Separator: typeof SelectSeparator;
163
672
  Placeholder: typeof SelectPlaceholder;
164
673
  };
165
674
 
166
675
  /**
167
- * Headless **Select** — a compound component wrapping the native
168
- * `<select>` / `<option>` / `<optgroup>` elements. Zero styles ship.
169
- *
170
- * Because the underlying element is the real `<select>`, the browser
171
- * owns the popup, keyboard interaction (arrow keys, Home/End,
172
- * typeahead), mobile UX (wheel pickers), and form submission. No
173
- * positioning JS or Portal is involved.
676
+ * Headless, accessible single-select **Select** — a compound with two render
677
+ * paths behind one API, chosen by {@link SelectRootBaseProps.native | `native`}:
678
+ * a rich Popover-API listbox (default) or a native `<select>` wrapper. Zero
679
+ * styles ship.
174
680
  *
175
681
  * `Select` is both callable (an alias of {@link SelectRoot | `Select.Root`})
176
- * and carries its sub-components as static properties.
682
+ * and carries its sub-components as static properties. Prefer the namespaced
683
+ * form in application code:
177
684
  *
178
- * - {@link SelectRoot | `Select.Root`} — state owner, renders `<select>`.
179
- * - {@link SelectOption | `Select.Option`} — renders `<option>`.
180
- * - {@link SelectGroup | `Select.Group`} — renders `<optgroup label>`.
181
- * - {@link SelectPlaceholder | `Select.Placeholder`} — disabled+hidden
182
- * first option used as the initial hint.
685
+ * - {@link SelectRoot | `Select.Root`} — state owner + context boundary.
686
+ * - {@link SelectTrigger | `Select.Trigger`} — rich mode: the listbox button.
687
+ * - {@link SelectValue | `Select.Value`} — rich mode: mirrors the selection.
688
+ * - {@link SelectContent | `Select.Content`} — rich mode: the popover listbox.
689
+ * - {@link SelectItem | `Select.Item`} — an option (rich `<div>` / native `<option>`).
690
+ * - {@link SelectItemIndicator | `Select.ItemIndicator`} — rich mode: the selected mark.
691
+ * - {@link SelectGroup | `Select.Group`} — a group (rich `role="group"` / native `<optgroup>`).
692
+ * - {@link SelectSeparator | `Select.Separator`} — rich mode: a divider between groups/items.
693
+ * - {@link SelectPlaceholder | `Select.Placeholder`} — native mode: the initial hint.
183
694
  *
184
- * @example Minimal usage
695
+ * @example Rich (default)
185
696
  * ```tsx
186
697
  * import { Select } from "@primitiv-ui/react";
187
698
  *
188
- * <Select.Root defaultValue="apple" aria-label="Pick a fruit">
189
- * <Select.Option value="apple">Apple</Select.Option>
190
- * <Select.Option value="banana">Banana</Select.Option>
699
+ * <Select.Root value={framework} onValueChange={setFramework}>
700
+ * <Select.Trigger>
701
+ * <Select.Value placeholder="Pick a framework..." />
702
+ * </Select.Trigger>
703
+ * <Select.Content>
704
+ * <Select.Item value="react">
705
+ * React
706
+ * <Select.ItemIndicator>✓</Select.ItemIndicator>
707
+ * </Select.Item>
708
+ * <Select.Item value="vue">
709
+ * Vue
710
+ * <Select.ItemIndicator>✓</Select.ItemIndicator>
711
+ * </Select.Item>
712
+ * </Select.Content>
191
713
  * </Select.Root>
192
714
  * ```
193
715
  *
194
- * @example With placeholder and groups
716
+ * @example Native
195
717
  * ```tsx
196
- * <Select.Root required aria-label="Pick a food">
197
- * <Select.Placeholder>Choose…</Select.Placeholder>
718
+ * <Select.Root native defaultValue="apple" aria-label="Pick a fruit">
719
+ * <Select.Placeholder>Choose...</Select.Placeholder>
198
720
  * <Select.Group label="Fruits">
199
- * <Select.Option value="apple">Apple</Select.Option>
200
- * </Select.Group>
201
- * <Select.Group label="Vegetables">
202
- * <Select.Option value="carrot">Carrot</Select.Option>
721
+ * <Select.Item value="apple">Apple</Select.Item>
722
+ * <Select.Item value="banana">Banana</Select.Item>
203
723
  * </Select.Group>
204
724
  * </Select.Root>
205
725
  * ```
206
726
  *
207
- * @see {@link SelectRoot} for state modes, placeholder integration, and `asChild`.
208
- * @see {@link SelectPlaceholder} for the placeholder + `defaultValue` interaction.
727
+ * @see {@link SelectRoot} for the mode + state contract and Field integration.
728
+ * @see {@link SelectValue} for how the selection mirrors into the trigger.
729
+ * @see {@link SelectContent} for the listbox keyboard contract.
730
+ * @see {@link SelectPlaceholder} for the native placeholder + `defaultValue` interaction.
209
731
  */
210
732
  const SelectCompound: TSelectCompound = Object.assign(SelectRoot, {
211
733
  Root: SelectRoot,
212
- Option: SelectOption,
734
+ Trigger: SelectTrigger,
735
+ Value: SelectValue,
736
+ Content: SelectContent,
737
+ Item: SelectItem,
738
+ ItemIndicator: SelectItemIndicator,
213
739
  Group: SelectGroup,
740
+ Separator: SelectSeparator,
214
741
  Placeholder: SelectPlaceholder,
215
742
  });
216
743