@primitiv-ui/react 0.1.0

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 (585) hide show
  1. package/README.md +79 -0
  2. package/package.json +59 -0
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +40 -0
  4. package/src/AccessibleIcon/README.md +42 -0
  5. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +47 -0
  6. package/src/AccessibleIcon/index.ts +2 -0
  7. package/src/AccessibleIcon/types.ts +8 -0
  8. package/src/Accordion/Accordion.tsx +412 -0
  9. package/src/Accordion/AccordionContext.ts +12 -0
  10. package/src/Accordion/README.md +202 -0
  11. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +237 -0
  12. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +333 -0
  13. package/src/Accordion/__tests__/Accordion.controlled-state.test.tsx +175 -0
  14. package/src/Accordion/__tests__/Accordion.data-attributes.test.tsx +272 -0
  15. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +311 -0
  16. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +119 -0
  17. package/src/Accordion/__tests__/Accordion.forceMount.test.tsx +119 -0
  18. package/src/Accordion/__tests__/Accordion.keyboard-interaction.test.tsx +736 -0
  19. package/src/Accordion/__tests__/Accordion.mouse-interaction.test.tsx +212 -0
  20. package/src/Accordion/__tests__/Accordion.multiple-mode.test.tsx +90 -0
  21. package/src/Accordion/__tests__/Accordion.reading-direction.test.tsx +139 -0
  22. package/src/Accordion/__tests__/Accordion.uncontrolled-state.test.tsx +154 -0
  23. package/src/Accordion/hooks/index.ts +6 -0
  24. package/src/Accordion/hooks/useAccordionContext.ts +1 -0
  25. package/src/Accordion/hooks/useAccordionHeaderContext.ts +10 -0
  26. package/src/Accordion/hooks/useAccordionItem.ts +22 -0
  27. package/src/Accordion/hooks/useAccordionItemContext.ts +1 -0
  28. package/src/Accordion/hooks/useAccordionRoot.ts +151 -0
  29. package/src/Accordion/hooks/useAccordionTrigger.ts +90 -0
  30. package/src/Accordion/index.ts +1 -0
  31. package/src/Accordion/types.ts +81 -0
  32. package/src/Alert/Alert.tsx +43 -0
  33. package/src/Alert/README.md +54 -0
  34. package/src/Alert/__tests__/Alert.test.tsx +28 -0
  35. package/src/Alert/index.ts +2 -0
  36. package/src/Alert/types.ts +5 -0
  37. package/src/Avatar/Avatar.tsx +149 -0
  38. package/src/Avatar/AvatarContext.ts +20 -0
  39. package/src/Avatar/README.md +116 -0
  40. package/src/Avatar/__tests__/Avatar.asChild.test.tsx +53 -0
  41. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +14 -0
  42. package/src/Avatar/__tests__/Avatar.error-handling.test.tsx +30 -0
  43. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +75 -0
  44. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +81 -0
  45. package/src/Avatar/hooks/index.ts +2 -0
  46. package/src/Avatar/hooks/useAvatarContext.ts +1 -0
  47. package/src/Avatar/hooks/useAvatarImage.ts +40 -0
  48. package/src/Avatar/index.ts +3 -0
  49. package/src/Avatar/types.ts +44 -0
  50. package/src/Breadcrumb/Breadcrumb.tsx +234 -0
  51. package/src/Breadcrumb/README.md +111 -0
  52. package/src/Breadcrumb/__tests__/Breadcrumb.asChild.test.tsx +33 -0
  53. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +132 -0
  54. package/src/Breadcrumb/index.ts +2 -0
  55. package/src/Breadcrumb/types.ts +22 -0
  56. package/src/Button/Button.tsx +95 -0
  57. package/src/Button/README.md +112 -0
  58. package/src/Button/__tests__/Button.asChild.test.tsx +91 -0
  59. package/src/Button/__tests__/Button.basic-rendering.test.tsx +126 -0
  60. package/src/Button/__tests__/Button.contract.test.tsx +72 -0
  61. package/src/Button/__tests__/Button.disabled.test.tsx +52 -0
  62. package/src/Button/__tests__/Button.icon-usage.test.tsx +57 -0
  63. package/src/Button/__tests__/Button.keyboard-interaction.test.tsx +70 -0
  64. package/src/Button/index.ts +2 -0
  65. package/src/Button/types.ts +8 -0
  66. package/src/Carousel/Carousel.tsx +708 -0
  67. package/src/Carousel/CarouselContext.ts +11 -0
  68. package/src/Carousel/README.md +848 -0
  69. package/src/Carousel/__tests__/Carousel.asChild.test.tsx +178 -0
  70. package/src/Carousel/__tests__/Carousel.auto-play.test.tsx +617 -0
  71. package/src/Carousel/__tests__/Carousel.basic-rendering.test.tsx +569 -0
  72. package/src/Carousel/__tests__/Carousel.controlled-state.test.tsx +137 -0
  73. package/src/Carousel/__tests__/Carousel.error-handling.test.tsx +81 -0
  74. package/src/Carousel/__tests__/Carousel.ids.test.tsx +111 -0
  75. package/src/Carousel/__tests__/Carousel.imperative-api.test.tsx +213 -0
  76. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +560 -0
  77. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +276 -0
  78. package/src/Carousel/__tests__/Carousel.keyboard-navigation.test.tsx +158 -0
  79. package/src/Carousel/__tests__/Carousel.play-pause.test.tsx +232 -0
  80. package/src/Carousel/__tests__/Carousel.prev-next.test.tsx +68 -0
  81. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +49 -0
  82. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +87 -0
  83. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +179 -0
  84. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +109 -0
  85. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +151 -0
  86. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +183 -0
  87. package/src/Carousel/__tests__/Carousel.touch-interaction.test.tsx +96 -0
  88. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +70 -0
  89. package/src/Carousel/__tests__/Carousel.translations.test.tsx +157 -0
  90. package/src/Carousel/__tests__/Carousel.uncontrolled-state.test.tsx +146 -0
  91. package/src/Carousel/hooks/index.ts +4 -0
  92. package/src/Carousel/hooks/useCarouselContext.ts +13 -0
  93. package/src/Carousel/hooks/useCarouselRoot.ts +450 -0
  94. package/src/Carousel/hooks/useCarouselSlide.ts +45 -0
  95. package/src/Carousel/hooks/useCarouselViewport.ts +290 -0
  96. package/src/Carousel/index.ts +3 -0
  97. package/src/Carousel/types.ts +400 -0
  98. package/src/Checkbox/Checkbox.tsx +228 -0
  99. package/src/Checkbox/CheckboxContext.ts +12 -0
  100. package/src/Checkbox/README.md +156 -0
  101. package/src/Checkbox/__tests__/Checkbox.asChild.test.tsx +69 -0
  102. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +41 -0
  103. package/src/Checkbox/__tests__/Checkbox.controlled-state.test.tsx +82 -0
  104. package/src/Checkbox/__tests__/Checkbox.disabled.test.tsx +15 -0
  105. package/src/Checkbox/__tests__/Checkbox.indeterminate.test.tsx +82 -0
  106. package/src/Checkbox/__tests__/Checkbox.indicator.test.tsx +117 -0
  107. package/src/Checkbox/__tests__/Checkbox.uncontrolled-state.test.tsx +89 -0
  108. package/src/Checkbox/hooks/index.ts +2 -0
  109. package/src/Checkbox/hooks/useCheckboxContext.ts +1 -0
  110. package/src/Checkbox/hooks/useCheckboxRoot.ts +32 -0
  111. package/src/Checkbox/index.ts +1 -0
  112. package/src/Checkbox/types.ts +33 -0
  113. package/src/CheckboxCard/CheckboxCard.tsx +208 -0
  114. package/src/CheckboxCard/CheckboxCardContext.ts +12 -0
  115. package/src/CheckboxCard/README.md +114 -0
  116. package/src/CheckboxCard/__tests__/CheckboxCard.asChild.test.tsx +54 -0
  117. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +58 -0
  118. package/src/CheckboxCard/__tests__/CheckboxCard.controlled-state.test.tsx +77 -0
  119. package/src/CheckboxCard/__tests__/CheckboxCard.disabled.test.tsx +55 -0
  120. package/src/CheckboxCard/__tests__/CheckboxCard.error-handling.test.tsx +20 -0
  121. package/src/CheckboxCard/__tests__/CheckboxCard.indeterminate.test.tsx +60 -0
  122. package/src/CheckboxCard/__tests__/CheckboxCard.indicator.test.tsx +136 -0
  123. package/src/CheckboxCard/__tests__/CheckboxCard.uncontrolled-state.test.tsx +73 -0
  124. package/src/CheckboxCard/hooks/index.ts +2 -0
  125. package/src/CheckboxCard/hooks/useCheckboxCardContext.ts +1 -0
  126. package/src/CheckboxCard/hooks/useCheckboxCardRoot.ts +30 -0
  127. package/src/CheckboxCard/index.ts +3 -0
  128. package/src/CheckboxCard/types.ts +33 -0
  129. package/src/Collapsible/Collapsible.tsx +316 -0
  130. package/src/Collapsible/CollapsibleContext.ts +7 -0
  131. package/src/Collapsible/README.md +174 -0
  132. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +240 -0
  133. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +118 -0
  134. package/src/Collapsible/__tests__/Collapsible.controlled-state.test.tsx +134 -0
  135. package/src/Collapsible/__tests__/Collapsible.disabled.test.tsx +132 -0
  136. package/src/Collapsible/__tests__/Collapsible.error-handling.test.tsx +40 -0
  137. package/src/Collapsible/__tests__/Collapsible.forceMount.test.tsx +111 -0
  138. package/src/Collapsible/__tests__/Collapsible.triggerIcon.test.tsx +93 -0
  139. package/src/Collapsible/__tests__/Collapsible.uncontrolled-state.test.tsx +125 -0
  140. package/src/Collapsible/hooks/index.ts +2 -0
  141. package/src/Collapsible/hooks/useCollapsibleRoot.ts +34 -0
  142. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +49 -0
  143. package/src/Collapsible/index.ts +1 -0
  144. package/src/Collapsible/types.ts +48 -0
  145. package/src/ContextMenu/ContextMenu.tsx +1004 -0
  146. package/src/ContextMenu/ContextMenuContentContext.ts +15 -0
  147. package/src/ContextMenu/ContextMenuContext.ts +21 -0
  148. package/src/ContextMenu/ContextMenuGroupContext.ts +8 -0
  149. package/src/ContextMenu/ContextMenuItemIndicatorContext.ts +8 -0
  150. package/src/ContextMenu/ContextMenuRadioGroupContext.ts +9 -0
  151. package/src/ContextMenu/ContextMenuSubContext.ts +15 -0
  152. package/src/ContextMenu/README.md +275 -0
  153. package/src/ContextMenu/__tests__/ContextMenu.asChild.test.tsx +186 -0
  154. package/src/ContextMenu/__tests__/ContextMenu.basic-rendering.test.tsx +39 -0
  155. package/src/ContextMenu/__tests__/ContextMenu.checkbox-item.test.tsx +145 -0
  156. package/src/ContextMenu/__tests__/ContextMenu.error-handling.test.tsx +113 -0
  157. package/src/ContextMenu/__tests__/ContextMenu.group-label.test.tsx +48 -0
  158. package/src/ContextMenu/__tests__/ContextMenu.item-indicator.test.tsx +88 -0
  159. package/src/ContextMenu/__tests__/ContextMenu.item.test.tsx +106 -0
  160. package/src/ContextMenu/__tests__/ContextMenu.keyboard-interaction.test.tsx +172 -0
  161. package/src/ContextMenu/__tests__/ContextMenu.mouse-interaction.test.tsx +227 -0
  162. package/src/ContextMenu/__tests__/ContextMenu.radio-item.test.tsx +127 -0
  163. package/src/ContextMenu/__tests__/ContextMenu.reading-direction.test.tsx +152 -0
  164. package/src/ContextMenu/__tests__/ContextMenu.separator.test.tsx +47 -0
  165. package/src/ContextMenu/__tests__/ContextMenu.state-modes.test.tsx +119 -0
  166. package/src/ContextMenu/__tests__/ContextMenu.sub.test.tsx +262 -0
  167. package/src/ContextMenu/__tests__/ContextMenu.typeahead.test.tsx +89 -0
  168. package/src/ContextMenu/constants.ts +4 -0
  169. package/src/ContextMenu/index.ts +1 -0
  170. package/src/ContextMenu/types.ts +199 -0
  171. package/src/DirectionProvider/DirectionContext.ts +21 -0
  172. package/src/DirectionProvider/DirectionProvider.tsx +31 -0
  173. package/src/DirectionProvider/README.md +62 -0
  174. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +29 -0
  175. package/src/DirectionProvider/index.ts +3 -0
  176. package/src/DirectionProvider/types.ts +10 -0
  177. package/src/Divider/Divider.tsx +57 -0
  178. package/src/Divider/README.md +57 -0
  179. package/src/Divider/__tests__/Divider.test.tsx +41 -0
  180. package/src/Divider/index.ts +1 -0
  181. package/src/Divider/types.ts +5 -0
  182. package/src/Dropdown/Dropdown.tsx +842 -0
  183. package/src/Dropdown/DropdownContentContext.ts +15 -0
  184. package/src/Dropdown/DropdownContext.ts +17 -0
  185. package/src/Dropdown/DropdownGroupContext.ts +8 -0
  186. package/src/Dropdown/DropdownItemIndicatorContext.ts +13 -0
  187. package/src/Dropdown/DropdownRadioGroupContext.ts +9 -0
  188. package/src/Dropdown/DropdownSubContext.ts +15 -0
  189. package/src/Dropdown/README.md +284 -0
  190. package/src/Dropdown/__tests__/Dropdown.asChild.test.tsx +286 -0
  191. package/src/Dropdown/__tests__/Dropdown.basic-rendering.test.tsx +43 -0
  192. package/src/Dropdown/__tests__/Dropdown.checkbox-item.test.tsx +121 -0
  193. package/src/Dropdown/__tests__/Dropdown.disabled.test.tsx +143 -0
  194. package/src/Dropdown/__tests__/Dropdown.error-handling.test.tsx +85 -0
  195. package/src/Dropdown/__tests__/Dropdown.group-label.test.tsx +68 -0
  196. package/src/Dropdown/__tests__/Dropdown.item-indicator.test.tsx +260 -0
  197. package/src/Dropdown/__tests__/Dropdown.item.test.tsx +72 -0
  198. package/src/Dropdown/__tests__/Dropdown.keyboard-edge-cases.test.tsx +77 -0
  199. package/src/Dropdown/__tests__/Dropdown.keyboard-interaction.test.tsx +310 -0
  200. package/src/Dropdown/__tests__/Dropdown.mouse-interaction.test.tsx +347 -0
  201. package/src/Dropdown/__tests__/Dropdown.radio-item.test.tsx +134 -0
  202. package/src/Dropdown/__tests__/Dropdown.reading-direction.test.tsx +153 -0
  203. package/src/Dropdown/__tests__/Dropdown.separator.test.tsx +46 -0
  204. package/src/Dropdown/__tests__/Dropdown.state-modes.test.tsx +100 -0
  205. package/src/Dropdown/__tests__/Dropdown.sub.test.tsx +185 -0
  206. package/src/Dropdown/__tests__/Dropdown.trigger.test.tsx +110 -0
  207. package/src/Dropdown/__tests__/Dropdown.typeahead.test.tsx +133 -0
  208. package/src/Dropdown/constants.ts +4 -0
  209. package/src/Dropdown/hooks/index.ts +9 -0
  210. package/src/Dropdown/hooks/useCloseSiblingSub.ts +13 -0
  211. package/src/Dropdown/hooks/useDropdownContent.ts +162 -0
  212. package/src/Dropdown/hooks/useDropdownContext.ts +1 -0
  213. package/src/Dropdown/hooks/useDropdownGroup.ts +18 -0
  214. package/src/Dropdown/hooks/useDropdownItem.ts +49 -0
  215. package/src/Dropdown/hooks/useDropdownLabel.ts +15 -0
  216. package/src/Dropdown/hooks/useDropdownRoot.ts +57 -0
  217. package/src/Dropdown/hooks/useDropdownSubContext.ts +1 -0
  218. package/src/Dropdown/hooks/useDropdownTrigger.ts +31 -0
  219. package/src/Dropdown/index.ts +1 -0
  220. package/src/Dropdown/types.ts +200 -0
  221. package/src/EmptyState/EmptyState.tsx +245 -0
  222. package/src/EmptyState/README.md +129 -0
  223. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +32 -0
  224. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +30 -0
  225. package/src/EmptyState/__tests__/EmptyState.Media.test.tsx +34 -0
  226. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +28 -0
  227. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +26 -0
  228. package/src/EmptyState/index.ts +2 -0
  229. package/src/EmptyState/types.ts +21 -0
  230. package/src/Field/Field.tsx +239 -0
  231. package/src/Field/FieldContext.ts +22 -0
  232. package/src/Field/README.md +167 -0
  233. package/src/Field/__tests__/Field.asChild.test.tsx +83 -0
  234. package/src/Field/__tests__/Field.basic-rendering.test.tsx +104 -0
  235. package/src/Field/__tests__/Field.state-cascade.test.tsx +75 -0
  236. package/src/Field/hooks/index.ts +2 -0
  237. package/src/Field/hooks/useFieldContext.ts +1 -0
  238. package/src/Field/hooks/useFieldProps.ts +57 -0
  239. package/src/Field/index.ts +2 -0
  240. package/src/Field/types.ts +33 -0
  241. package/src/Fieldset/Fieldset.tsx +104 -0
  242. package/src/Fieldset/README.md +74 -0
  243. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +81 -0
  244. package/src/Fieldset/__tests__/Fieldset.disabled.test.tsx +41 -0
  245. package/src/Fieldset/index.ts +2 -0
  246. package/src/Fieldset/types.ts +5 -0
  247. package/src/Input/Input.tsx +120 -0
  248. package/src/Input/README.md +180 -0
  249. package/src/Input/__tests__/Input.asChild.test.tsx +85 -0
  250. package/src/Input/__tests__/Input.basic-rendering.test.tsx +118 -0
  251. package/src/Input/__tests__/Input.disabled.test.tsx +49 -0
  252. package/src/Input/__tests__/Input.field-integration.test.tsx +148 -0
  253. package/src/Input/index.ts +2 -0
  254. package/src/Input/types.ts +7 -0
  255. package/src/InputGroup/InputGroup.tsx +228 -0
  256. package/src/InputGroup/README.md +178 -0
  257. package/src/InputGroup/__tests__/InputGroup.asChild.test.tsx +109 -0
  258. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +106 -0
  259. package/src/InputGroup/index.ts +2 -0
  260. package/src/InputGroup/types.ts +13 -0
  261. package/src/MillerColumns/MillerColumns.tsx +329 -0
  262. package/src/MillerColumns/MillerColumnsContext.ts +25 -0
  263. package/src/MillerColumns/README.md +278 -0
  264. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +82 -0
  265. package/src/MillerColumns/__tests__/MillerColumns.asChild.test.tsx +106 -0
  266. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +68 -0
  267. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +52 -0
  268. package/src/MillerColumns/__tests__/MillerColumns.column-projection.test.tsx +161 -0
  269. package/src/MillerColumns/__tests__/MillerColumns.controlled-state.test.tsx +90 -0
  270. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +77 -0
  271. package/src/MillerColumns/__tests__/MillerColumns.disabled-items.test.tsx +65 -0
  272. package/src/MillerColumns/__tests__/MillerColumns.error-handling.test.tsx +57 -0
  273. package/src/MillerColumns/__tests__/MillerColumns.fixtures.ts +15 -0
  274. package/src/MillerColumns/__tests__/MillerColumns.item-indicator.test.tsx +57 -0
  275. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +181 -0
  276. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +47 -0
  277. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +137 -0
  278. package/src/MillerColumns/__tests__/MillerColumns.roving-tabindex.test.tsx +91 -0
  279. package/src/MillerColumns/__tests__/MillerColumns.selection.test.tsx +54 -0
  280. package/src/MillerColumns/__tests__/MillerColumns.uncontrolled-state.test.tsx +70 -0
  281. package/src/MillerColumns/hooks/index.ts +7 -0
  282. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +23 -0
  283. package/src/MillerColumns/hooks/useMillerColumnsColumnContext.ts +1 -0
  284. package/src/MillerColumns/hooks/useMillerColumnsContext.ts +1 -0
  285. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +157 -0
  286. package/src/MillerColumns/hooks/useMillerColumnsItemContext.ts +1 -0
  287. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +76 -0
  288. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  289. package/src/MillerColumns/index.ts +3 -0
  290. package/src/MillerColumns/types.ts +93 -0
  291. package/src/MillerColumns/useMillerColumnsSelection.ts +31 -0
  292. package/src/MillerColumns/utils.ts +75 -0
  293. package/src/Modal/Modal.tsx +474 -0
  294. package/src/Modal/ModalContext.ts +13 -0
  295. package/src/Modal/README.md +207 -0
  296. package/src/Modal/__tests__/Modal.accessibility.test.tsx +167 -0
  297. package/src/Modal/__tests__/Modal.asChild.test.tsx +162 -0
  298. package/src/Modal/__tests__/Modal.click-outside.test.tsx +115 -0
  299. package/src/Modal/__tests__/Modal.content.test.tsx +193 -0
  300. package/src/Modal/__tests__/Modal.controlled-state.test.tsx +120 -0
  301. package/src/Modal/__tests__/Modal.error-handling.test.tsx +30 -0
  302. package/src/Modal/__tests__/Modal.escape-hatches.test.tsx +99 -0
  303. package/src/Modal/__tests__/Modal.imperative-api.test.tsx +119 -0
  304. package/src/Modal/__tests__/Modal.nested.test.tsx +106 -0
  305. package/src/Modal/__tests__/Modal.overlay.test.tsx +99 -0
  306. package/src/Modal/__tests__/Modal.portal.test.tsx +90 -0
  307. package/src/Modal/__tests__/Modal.presence.test.tsx +111 -0
  308. package/src/Modal/__tests__/Modal.trigger.test.tsx +70 -0
  309. package/src/Modal/__tests__/Modal.uncontrolled-state.test.tsx +72 -0
  310. package/src/Modal/__tests__/dialog-polyfill.ts +40 -0
  311. package/src/Modal/hooks/index.ts +4 -0
  312. package/src/Modal/hooks/useModalContent.ts +62 -0
  313. package/src/Modal/hooks/useModalContext.ts +1 -0
  314. package/src/Modal/hooks/useModalRoot.ts +81 -0
  315. package/src/Modal/hooks/useModalTrigger.ts +25 -0
  316. package/src/Modal/index.ts +3 -0
  317. package/src/Modal/types.ts +76 -0
  318. package/src/Portal/Portal.tsx +28 -0
  319. package/src/Portal/README.md +70 -0
  320. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +17 -0
  321. package/src/Portal/index.ts +2 -0
  322. package/src/Portal/types.ts +6 -0
  323. package/src/Progress/Progress.tsx +178 -0
  324. package/src/Progress/ProgressContext.ts +15 -0
  325. package/src/Progress/README.md +112 -0
  326. package/src/Progress/__tests__/Progress.asChild.test.tsx +37 -0
  327. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +65 -0
  328. package/src/Progress/__tests__/Progress.error-handling.test.tsx +40 -0
  329. package/src/Progress/__tests__/Progress.fixtures.ts +7 -0
  330. package/src/Progress/__tests__/Progress.value.test.tsx +83 -0
  331. package/src/Progress/hooks/index.ts +2 -0
  332. package/src/Progress/hooks/useProgressContext.ts +1 -0
  333. package/src/Progress/hooks/useProgressRoot.ts +45 -0
  334. package/src/Progress/index.ts +3 -0
  335. package/src/Progress/types.ts +43 -0
  336. package/src/RadioCard/README.md +133 -0
  337. package/src/RadioCard/RadioCard.tsx +334 -0
  338. package/src/RadioCard/RadioCardContext.ts +23 -0
  339. package/src/RadioCard/RadioCardItemContext.ts +10 -0
  340. package/src/RadioCard/__tests__/RadioCard.asChild.test.tsx +76 -0
  341. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +87 -0
  342. package/src/RadioCard/__tests__/RadioCard.controlled-state.test.tsx +107 -0
  343. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +61 -0
  344. package/src/RadioCard/__tests__/RadioCard.error-handling.test.tsx +35 -0
  345. package/src/RadioCard/__tests__/RadioCard.indicator.test.tsx +119 -0
  346. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +158 -0
  347. package/src/RadioCard/__tests__/RadioCard.orientation.test.tsx +90 -0
  348. package/src/RadioCard/__tests__/RadioCard.reading-direction.test.tsx +65 -0
  349. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +108 -0
  350. package/src/RadioCard/hooks/index.ts +3 -0
  351. package/src/RadioCard/hooks/useRadioCardContext.ts +1 -0
  352. package/src/RadioCard/hooks/useRadioCardItemContext.ts +1 -0
  353. package/src/RadioCard/hooks/useRadioCardRoot.ts +77 -0
  354. package/src/RadioCard/index.ts +4 -0
  355. package/src/RadioCard/types.ts +51 -0
  356. package/src/RadioGroup/README.md +185 -0
  357. package/src/RadioGroup/RadioGroup.tsx +353 -0
  358. package/src/RadioGroup/RadioGroupContext.ts +23 -0
  359. package/src/RadioGroup/RadioGroupItemContext.ts +10 -0
  360. package/src/RadioGroup/__tests__/RadioGroup.asChild.test.tsx +105 -0
  361. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +72 -0
  362. package/src/RadioGroup/__tests__/RadioGroup.controlled-state.test.tsx +109 -0
  363. package/src/RadioGroup/__tests__/RadioGroup.disabled-keydown-guards.test.tsx +68 -0
  364. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +79 -0
  365. package/src/RadioGroup/__tests__/RadioGroup.error-handling.test.tsx +33 -0
  366. package/src/RadioGroup/__tests__/RadioGroup.indicator.test.tsx +85 -0
  367. package/src/RadioGroup/__tests__/RadioGroup.keyboard-interaction.test.tsx +135 -0
  368. package/src/RadioGroup/__tests__/RadioGroup.orientation.test.tsx +90 -0
  369. package/src/RadioGroup/__tests__/RadioGroup.reading-direction.test.tsx +65 -0
  370. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +45 -0
  371. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +105 -0
  372. package/src/RadioGroup/__tests__/RadioGroup.uncontrolled-state.test.tsx +96 -0
  373. package/src/RadioGroup/hooks/index.ts +3 -0
  374. package/src/RadioGroup/hooks/useRadioGroupContext.ts +1 -0
  375. package/src/RadioGroup/hooks/useRadioGroupItemContext.ts +1 -0
  376. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +87 -0
  377. package/src/RadioGroup/index.ts +1 -0
  378. package/src/RadioGroup/types.ts +51 -0
  379. package/src/Select/README.md +203 -0
  380. package/src/Select/Select.tsx +204 -0
  381. package/src/Select/__tests__/Select.asChild.test.tsx +36 -0
  382. package/src/Select/__tests__/Select.basic-rendering.test.tsx +17 -0
  383. package/src/Select/__tests__/Select.controlled-state.test.tsx +69 -0
  384. package/src/Select/__tests__/Select.data-attributes.test.tsx +29 -0
  385. package/src/Select/__tests__/Select.field-integration.test.tsx +150 -0
  386. package/src/Select/__tests__/Select.group.test.tsx +42 -0
  387. package/src/Select/__tests__/Select.placeholder.test.tsx +32 -0
  388. package/src/Select/index.ts +2 -0
  389. package/src/Select/types.ts +89 -0
  390. package/src/SkipNav/README.md +87 -0
  391. package/src/SkipNav/SkipNav.tsx +116 -0
  392. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +23 -0
  393. package/src/SkipNav/__tests__/SkipNav.ids.test.tsx +19 -0
  394. package/src/SkipNav/index.ts +1 -0
  395. package/src/SkipNav/types.ts +26 -0
  396. package/src/Slider/README.md +215 -0
  397. package/src/Slider/Slider.tsx +308 -0
  398. package/src/Slider/SliderContext.ts +24 -0
  399. package/src/Slider/__tests__/Slider.asChild.test.tsx +119 -0
  400. package/src/Slider/__tests__/Slider.basic-rendering.test.tsx +157 -0
  401. package/src/Slider/__tests__/Slider.controlled-state.test.tsx +78 -0
  402. package/src/Slider/__tests__/Slider.disabled.test.tsx +82 -0
  403. package/src/Slider/__tests__/Slider.error-handling.test.tsx +45 -0
  404. package/src/Slider/__tests__/Slider.fixtures.ts +53 -0
  405. package/src/Slider/__tests__/Slider.form.test.tsx +67 -0
  406. package/src/Slider/__tests__/Slider.inverted.test.tsx +112 -0
  407. package/src/Slider/__tests__/Slider.keyboard-interaction.test.tsx +118 -0
  408. package/src/Slider/__tests__/Slider.multiple-thumbs.test.tsx +84 -0
  409. package/src/Slider/__tests__/Slider.orientation.test.tsx +101 -0
  410. package/src/Slider/__tests__/Slider.pointer-interaction.test.tsx +205 -0
  411. package/src/Slider/__tests__/Slider.reading-direction.test.tsx +99 -0
  412. package/src/Slider/__tests__/Slider.uncontrolled-state.test.tsx +69 -0
  413. package/src/Slider/__tests__/Slider.value-commit.test.tsx +103 -0
  414. package/src/Slider/hooks/index.ts +3 -0
  415. package/src/Slider/hooks/useSliderContext.ts +1 -0
  416. package/src/Slider/hooks/useSliderRoot.ts +197 -0
  417. package/src/Slider/hooks/useSliderThumb.ts +77 -0
  418. package/src/Slider/index.ts +3 -0
  419. package/src/Slider/types.ts +48 -0
  420. package/src/Slider/utils.ts +155 -0
  421. package/src/Slot/Slot.tsx +158 -0
  422. package/src/Slot/__tests__/Slot.test.tsx +163 -0
  423. package/src/Slot/__tests__/composeEventHandlers.test.ts +74 -0
  424. package/src/Slot/composeEventHandlers.ts +38 -0
  425. package/src/Slot/index.ts +3 -0
  426. package/src/Slot/types.ts +5 -0
  427. package/src/Status/README.md +50 -0
  428. package/src/Status/Status.tsx +44 -0
  429. package/src/Status/__tests__/Status.test.tsx +28 -0
  430. package/src/Status/index.ts +2 -0
  431. package/src/Status/types.ts +5 -0
  432. package/src/Switch/README.md +121 -0
  433. package/src/Switch/Switch.tsx +167 -0
  434. package/src/Switch/SwitchContext.ts +10 -0
  435. package/src/Switch/__tests__/Switch.asChild.test.tsx +56 -0
  436. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +76 -0
  437. package/src/Switch/__tests__/Switch.contract.test.tsx +109 -0
  438. package/src/Switch/__tests__/Switch.controlled-state.test.tsx +79 -0
  439. package/src/Switch/__tests__/Switch.disabled.test.tsx +60 -0
  440. package/src/Switch/__tests__/Switch.error-handling.test.tsx +20 -0
  441. package/src/Switch/__tests__/Switch.keyboard-interaction.test.tsx +56 -0
  442. package/src/Switch/__tests__/Switch.thumb.test.tsx +84 -0
  443. package/src/Switch/__tests__/Switch.uncontrolled-state.test.tsx +83 -0
  444. package/src/Switch/hooks/index.ts +2 -0
  445. package/src/Switch/hooks/useSwitchContext.ts +1 -0
  446. package/src/Switch/hooks/useSwitchRoot.ts +28 -0
  447. package/src/Switch/index.ts +3 -0
  448. package/src/Switch/types.ts +37 -0
  449. package/src/Table/README.md +205 -0
  450. package/src/Table/Table.tsx +380 -0
  451. package/src/Table/__tests__/Table.Body.test.tsx +61 -0
  452. package/src/Table/__tests__/Table.Caption.test.tsx +70 -0
  453. package/src/Table/__tests__/Table.Cell.test.tsx +73 -0
  454. package/src/Table/__tests__/Table.Footer.test.tsx +61 -0
  455. package/src/Table/__tests__/Table.Head.test.tsx +61 -0
  456. package/src/Table/__tests__/Table.Header.test.tsx +73 -0
  457. package/src/Table/__tests__/Table.Root.test.tsx +49 -0
  458. package/src/Table/__tests__/Table.Row.test.tsx +67 -0
  459. package/src/Table/__tests__/Table.ScrollArea.test.tsx +83 -0
  460. package/src/Table/index.ts +1 -0
  461. package/src/Table/types.ts +63 -0
  462. package/src/Tabs/README.md +110 -0
  463. package/src/Tabs/Tabs.tsx +434 -0
  464. package/src/Tabs/TabsContext.ts +13 -0
  465. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +114 -0
  466. package/src/Tabs/__tests__/Tabs.asChild.test.tsx +91 -0
  467. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +483 -0
  468. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +133 -0
  469. package/src/Tabs/__tests__/Tabs.controlled-state.test.tsx +152 -0
  470. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +203 -0
  471. package/src/Tabs/__tests__/Tabs.error-handling.test.tsx +82 -0
  472. package/src/Tabs/__tests__/Tabs.fixtures.ts +171 -0
  473. package/src/Tabs/__tests__/Tabs.imperative-api.test.tsx +118 -0
  474. package/src/Tabs/__tests__/Tabs.keyboard-interaction.test.tsx +192 -0
  475. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +61 -0
  476. package/src/Tabs/__tests__/Tabs.mouse-interaction.test.tsx +216 -0
  477. package/src/Tabs/__tests__/Tabs.reading-direction.test.tsx +58 -0
  478. package/src/Tabs/__tests__/Tabs.uncontrolled-state.test.tsx +197 -0
  479. package/src/Tabs/hooks/index.ts +4 -0
  480. package/src/Tabs/hooks/useTabsContent.ts +27 -0
  481. package/src/Tabs/hooks/useTabsContext.ts +1 -0
  482. package/src/Tabs/hooks/useTabsRoot.ts +148 -0
  483. package/src/Tabs/hooks/useTabsTrigger.ts +111 -0
  484. package/src/Tabs/index.ts +3 -0
  485. package/src/Tabs/types.ts +99 -0
  486. package/src/Tabs/utils.ts +8 -0
  487. package/src/Textarea/README.md +98 -0
  488. package/src/Textarea/Textarea.tsx +93 -0
  489. package/src/Textarea/__tests__/Textarea.asChild.test.tsx +85 -0
  490. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +107 -0
  491. package/src/Textarea/__tests__/Textarea.disabled.test.tsx +49 -0
  492. package/src/Textarea/__tests__/Textarea.field-integration.test.tsx +134 -0
  493. package/src/Textarea/index.ts +2 -0
  494. package/src/Textarea/types.ts +7 -0
  495. package/src/Toggle/README.md +97 -0
  496. package/src/Toggle/Toggle.tsx +81 -0
  497. package/src/Toggle/__tests__/Toggle.asChild.test.tsx +42 -0
  498. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +28 -0
  499. package/src/Toggle/__tests__/Toggle.controlled-state.test.tsx +60 -0
  500. package/src/Toggle/__tests__/Toggle.disabled.test.tsx +34 -0
  501. package/src/Toggle/__tests__/Toggle.keyboard-interaction.test.tsx +42 -0
  502. package/src/Toggle/__tests__/Toggle.uncontrolled-state.test.tsx +40 -0
  503. package/src/Toggle/index.ts +2 -0
  504. package/src/Toggle/types.ts +23 -0
  505. package/src/ToggleGroup/README.md +137 -0
  506. package/src/ToggleGroup/ToggleGroup.tsx +298 -0
  507. package/src/ToggleGroup/ToggleGroupContext.ts +9 -0
  508. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +65 -0
  509. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +50 -0
  510. package/src/ToggleGroup/__tests__/ToggleGroup.disabled.test.tsx +54 -0
  511. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +151 -0
  512. package/src/ToggleGroup/__tests__/ToggleGroup.multiple-mode.test.tsx +144 -0
  513. package/src/ToggleGroup/__tests__/ToggleGroup.reading-direction.test.tsx +28 -0
  514. package/src/ToggleGroup/__tests__/ToggleGroup.single-mode.test.tsx +139 -0
  515. package/src/ToggleGroup/hooks/index.ts +2 -0
  516. package/src/ToggleGroup/hooks/useToggleGroupContext.ts +1 -0
  517. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +110 -0
  518. package/src/ToggleGroup/index.ts +2 -0
  519. package/src/ToggleGroup/types.ts +72 -0
  520. package/src/Tooltip/README.md +214 -0
  521. package/src/Tooltip/Tooltip.tsx +260 -0
  522. package/src/Tooltip/TooltipContext.ts +20 -0
  523. package/src/Tooltip/__tests__/Tooltip.asChild.test.tsx +77 -0
  524. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +180 -0
  525. package/src/Tooltip/__tests__/Tooltip.controlled-state.test.tsx +128 -0
  526. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  527. package/src/Tooltip/__tests__/Tooltip.focus-interaction.test.tsx +88 -0
  528. package/src/Tooltip/__tests__/Tooltip.hover-interaction.test.tsx +179 -0
  529. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +85 -0
  530. package/src/Tooltip/__tests__/Tooltip.uncontrolled-state.test.tsx +67 -0
  531. package/src/Tooltip/hooks/index.ts +4 -0
  532. package/src/Tooltip/hooks/useTooltipContent.ts +53 -0
  533. package/src/Tooltip/hooks/useTooltipProvider.ts +41 -0
  534. package/src/Tooltip/hooks/useTooltipRoot.ts +106 -0
  535. package/src/Tooltip/hooks/useTooltipTrigger.ts +44 -0
  536. package/src/Tooltip/index.ts +1 -0
  537. package/src/Tooltip/types.ts +64 -0
  538. package/src/Tree/README.md +339 -0
  539. package/src/Tree/Tree.tsx +571 -0
  540. package/src/Tree/TreeContext.ts +24 -0
  541. package/src/Tree/__tests__/Tree.aria.test.tsx +53 -0
  542. package/src/Tree/__tests__/Tree.asChild.test.tsx +134 -0
  543. package/src/Tree/__tests__/Tree.basic-rendering.test.tsx +111 -0
  544. package/src/Tree/__tests__/Tree.branch-behaviour.test.tsx +87 -0
  545. package/src/Tree/__tests__/Tree.controlled-expansion.test.tsx +92 -0
  546. package/src/Tree/__tests__/Tree.data-attributes.test.tsx +88 -0
  547. package/src/Tree/__tests__/Tree.disabled-items.test.tsx +196 -0
  548. package/src/Tree/__tests__/Tree.error-handling.test.tsx +71 -0
  549. package/src/Tree/__tests__/Tree.forceMount.test.tsx +72 -0
  550. package/src/Tree/__tests__/Tree.keyboard-interaction.test.tsx +150 -0
  551. package/src/Tree/__tests__/Tree.multiple-selection.test.tsx +151 -0
  552. package/src/Tree/__tests__/Tree.range-selection.test.tsx +200 -0
  553. package/src/Tree/__tests__/Tree.recursion-depth.test.tsx +73 -0
  554. package/src/Tree/__tests__/Tree.roving-tabindex.test.tsx +117 -0
  555. package/src/Tree/__tests__/Tree.selection-path.test.tsx +404 -0
  556. package/src/Tree/__tests__/Tree.single-selection.test.tsx +108 -0
  557. package/src/Tree/__tests__/Tree.uncontrolled-expansion.test.tsx +69 -0
  558. package/src/Tree/hooks/index.ts +3 -0
  559. package/src/Tree/hooks/useTreeItemKeyboard.ts +86 -0
  560. package/src/Tree/hooks/useTreePath.ts +68 -0
  561. package/src/Tree/hooks/useTreeRoot.ts +279 -0
  562. package/src/Tree/index.ts +3 -0
  563. package/src/Tree/types.ts +224 -0
  564. package/src/Tree/utils.ts +59 -0
  565. package/src/VisuallyHidden/README.md +58 -0
  566. package/src/VisuallyHidden/VisuallyHidden.tsx +67 -0
  567. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +59 -0
  568. package/src/VisuallyHidden/index.ts +2 -0
  569. package/src/VisuallyHidden/types.ts +5 -0
  570. package/src/hooks/index.ts +3 -0
  571. package/src/hooks/useCollection.ts +74 -0
  572. package/src/hooks/useControllableState.ts +81 -0
  573. package/src/hooks/useRovingTabindex.ts +178 -0
  574. package/src/index.ts +38 -0
  575. package/src/test/intersectionObserverPolyfill.ts +83 -0
  576. package/src/test/popoverPolyfill.ts +86 -0
  577. package/src/test/scrollPolyfill.ts +23 -0
  578. package/src/types.ts +13 -0
  579. package/src/utils/__tests__/createStrictContext.test.tsx +69 -0
  580. package/src/utils/__tests__/deriveId.test.ts +28 -0
  581. package/src/utils/__tests__/getKeyToActionMap.test.ts +106 -0
  582. package/src/utils/createStrictContext.ts +49 -0
  583. package/src/utils/deriveId.ts +31 -0
  584. package/src/utils/getKeyToActionMap.ts +95 -0
  585. package/src/utils/index.ts +3 -0
@@ -0,0 +1,204 @@
1
+ import { ChangeEvent, Children, isValidElement, ReactNode } from "react";
2
+
3
+ import { useFieldProps } from "../Field/hooks";
4
+ import { Slot } from "../Slot";
5
+
6
+ import {
7
+ SelectGroupProps,
8
+ SelectOptionProps,
9
+ SelectPlaceholderProps,
10
+ SelectRootProps,
11
+ } from "./types";
12
+
13
+ const PLACEHOLDER_DISPLAY_NAME = "SelectPlaceholder";
14
+
15
+ function hasPlaceholderChild(children: ReactNode): boolean {
16
+ return Children.toArray(children).some((child) => {
17
+ if (!isValidElement(child)) return false;
18
+ const type = child.type as { displayName?: string };
19
+ return type.displayName === PLACEHOLDER_DISPLAY_NAME;
20
+ });
21
+ }
22
+
23
+ /**
24
+ * The root of a Select — renders a native `<select>` element and passes
25
+ * all `SelectHTMLAttributes` through to the DOM.
26
+ *
27
+ * Browser-native behaviour is preserved: keyboard navigation (arrow keys,
28
+ * Home/End, typeahead), the platform popup, mobile UX, and form
29
+ * submission all work without additional JS.
30
+ *
31
+ * Supports two state modes, statically discriminated at the type level:
32
+ *
33
+ * - **Uncontrolled** — pass `defaultValue` (or omit it). The browser owns
34
+ * the selection. `onValueChange` is optional.
35
+ * - **Controlled** — pass `value` and `onValueChange` together. Every
36
+ * transition defers back through `onValueChange`.
37
+ *
38
+ * `onValueChange` receives the new selection as a plain string. The
39
+ * consumer's own `onChange` (the raw `ChangeEvent`) still fires alongside
40
+ * it.
41
+ *
42
+ * **Placeholder integration.** When a {@link Select.Placeholder} appears
43
+ * among the direct children and neither `value` nor `defaultValue` is
44
+ * set, Root infers `defaultValue=""` so the placeholder — not the first
45
+ * selectable option — is the initial selection.
46
+ *
47
+ * **Field integration.** When rendered inside a `<Field.Root>`, Select
48
+ * opts into `FieldContext` and inherits `id`, `aria-describedby`,
49
+ * `aria-invalid`, `disabled`, and `required` from the field. Any prop
50
+ * the consumer passes wins; `aria-describedby` is composed (consumer
51
+ * ids first, then field-supplied description / error ids). Outside a
52
+ * `<Field.Root>`, behaviour is unchanged.
53
+ */
54
+ function SelectRoot({
55
+ children,
56
+ asChild = false,
57
+ onChange,
58
+ onValueChange,
59
+ value,
60
+ defaultValue,
61
+ ...consumer
62
+ }: SelectRootProps) {
63
+ const merged = useFieldProps(consumer);
64
+
65
+ const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
66
+ onChange?.(event);
67
+ onValueChange?.(event.target.value);
68
+ };
69
+
70
+ const inferredDefaultValue =
71
+ !asChild &&
72
+ value === undefined &&
73
+ defaultValue === undefined &&
74
+ hasPlaceholderChild(children)
75
+ ? ""
76
+ : defaultValue;
77
+
78
+ const controlProps =
79
+ value !== undefined
80
+ ? { value }
81
+ : inferredDefaultValue !== undefined
82
+ ? { defaultValue: inferredDefaultValue }
83
+ : {};
84
+
85
+ const rootProps = {
86
+ ...merged,
87
+ ...controlProps,
88
+ "data-disabled": merged.disabled ? "" : undefined,
89
+ onChange: handleChange,
90
+ };
91
+
92
+ if (asChild) {
93
+ return <Slot {...rootProps}>{children}</Slot>;
94
+ }
95
+ return <select {...rootProps}>{children}</select>;
96
+ }
97
+
98
+ SelectRoot.displayName = "SelectRoot";
99
+
100
+ /**
101
+ * An individual choice inside a Select — renders a native `<option>`
102
+ * element and passes all `OptionHTMLAttributes` through to the DOM.
103
+ *
104
+ * Native `<option>` only renders text; rich content (icons, descriptions)
105
+ * is not supported.
106
+ */
107
+ function SelectOption({ children, ...rest }: SelectOptionProps) {
108
+ return <option {...rest}>{children}</option>;
109
+ }
110
+
111
+ SelectOption.displayName = "SelectOption";
112
+
113
+ /**
114
+ * Visually groups related options inside the Select popup — renders a
115
+ * native `<optgroup>` element. The `label` is shown by the browser as a
116
+ * non-selectable heading and is announced as the group's accessible name.
117
+ */
118
+ function SelectGroup({ children, ...rest }: SelectGroupProps) {
119
+ return <optgroup {...rest}>{children}</optgroup>;
120
+ }
121
+
122
+ SelectGroup.displayName = "SelectGroup";
123
+
124
+ /**
125
+ * A non-selectable hint shown as the initial selection of a Select.
126
+ * Renders a native `<option value="" disabled hidden>` so the browser
127
+ * displays it before the user picks anything but makes it unreachable
128
+ * from the dropdown afterwards. Render it as the first child of
129
+ * {@link Select.Root} (above any `Select.Option` or `Select.Group`).
130
+ *
131
+ * Pair with `required` on {@link Select.Root} to make the browser's
132
+ * native form validation catch an unchosen value at submission.
133
+ */
134
+ function SelectPlaceholder({ children, ...rest }: SelectPlaceholderProps) {
135
+ return (
136
+ <option {...rest} value="" disabled hidden>
137
+ {children}
138
+ </option>
139
+ );
140
+ }
141
+
142
+ SelectPlaceholder.displayName = "SelectPlaceholder";
143
+
144
+ type TSelectCompound = typeof SelectRoot & {
145
+ Root: typeof SelectRoot;
146
+ Option: typeof SelectOption;
147
+ Group: typeof SelectGroup;
148
+ Placeholder: typeof SelectPlaceholder;
149
+ };
150
+
151
+ /**
152
+ * Headless **Select** — a compound component wrapping the native
153
+ * `<select>` / `<option>` / `<optgroup>` elements. Zero styles ship.
154
+ *
155
+ * Because the underlying element is the real `<select>`, the browser
156
+ * owns the popup, keyboard interaction (arrow keys, Home/End,
157
+ * typeahead), mobile UX (wheel pickers), and form submission. No
158
+ * positioning JS or Portal is involved.
159
+ *
160
+ * `Select` is both callable (an alias of {@link SelectRoot | `Select.Root`})
161
+ * and carries its sub-components as static properties.
162
+ *
163
+ * - {@link SelectRoot | `Select.Root`} — state owner, renders `<select>`.
164
+ * - {@link SelectOption | `Select.Option`} — renders `<option>`.
165
+ * - {@link SelectGroup | `Select.Group`} — renders `<optgroup label>`.
166
+ * - {@link SelectPlaceholder | `Select.Placeholder`} — disabled+hidden
167
+ * first option used as the initial hint.
168
+ *
169
+ * @example Minimal usage
170
+ * ```tsx
171
+ * import { Select } from "@primitiv-ui/react";
172
+ *
173
+ * <Select.Root defaultValue="apple" aria-label="Pick a fruit">
174
+ * <Select.Option value="apple">Apple</Select.Option>
175
+ * <Select.Option value="banana">Banana</Select.Option>
176
+ * </Select.Root>
177
+ * ```
178
+ *
179
+ * @example With placeholder and groups
180
+ * ```tsx
181
+ * <Select.Root required aria-label="Pick a food">
182
+ * <Select.Placeholder>Choose…</Select.Placeholder>
183
+ * <Select.Group label="Fruits">
184
+ * <Select.Option value="apple">Apple</Select.Option>
185
+ * </Select.Group>
186
+ * <Select.Group label="Vegetables">
187
+ * <Select.Option value="carrot">Carrot</Select.Option>
188
+ * </Select.Group>
189
+ * </Select.Root>
190
+ * ```
191
+ *
192
+ * @see {@link SelectRoot} for state modes, placeholder integration, and `asChild`.
193
+ * @see {@link SelectPlaceholder} for the placeholder + `defaultValue` interaction.
194
+ */
195
+ const SelectCompound: TSelectCompound = Object.assign(SelectRoot, {
196
+ Root: SelectRoot,
197
+ Option: SelectOption,
198
+ Group: SelectGroup,
199
+ Placeholder: SelectPlaceholder,
200
+ });
201
+
202
+ SelectCompound.displayName = "Select";
203
+
204
+ export { SelectCompound as Select };
@@ -0,0 +1,36 @@
1
+ import { ComponentProps, ReactNode } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ import { Select } from "../Select";
5
+
6
+ function StyledSelect({
7
+ children,
8
+ ...rest
9
+ }: ComponentProps<"select"> & { children: ReactNode }) {
10
+ return (
11
+ <select {...rest} className="styled-select" data-testid="styled">
12
+ {children}
13
+ </select>
14
+ );
15
+ }
16
+
17
+ describe("Select asChild", () => {
18
+ it("delegates to a consumer-supplied <select> wrapper, merging Root's props onto it", () => {
19
+ // Arrange & Act
20
+ render(
21
+ <Select.Root asChild disabled>
22
+ <StyledSelect>
23
+ <Select.Option value="apple">Apple</Select.Option>
24
+ </StyledSelect>
25
+ </Select.Root>,
26
+ );
27
+
28
+ // Assert — the consumer's <select> is the rendered element, with
29
+ // Root's props merged in.
30
+ const select = screen.getByTestId("styled");
31
+ expect(select.tagName).toBe("SELECT");
32
+ expect(select).toHaveClass("styled-select");
33
+ expect(select).toHaveAttribute("data-disabled", "");
34
+ expect(select).toBeDisabled();
35
+ });
36
+ });
@@ -0,0 +1,17 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Select } from "../Select";
4
+
5
+ describe("Select basic rendering", () => {
6
+ it("renders an <option> for each Select.Option child so the value is in the DOM", () => {
7
+ // Arrange & Act
8
+ render(
9
+ <Select.Root>
10
+ <Select.Option value="apple">Apple</Select.Option>
11
+ </Select.Root>,
12
+ );
13
+
14
+ // Assert
15
+ expect(screen.getByRole("option", { name: "Apple" })).toBeInTheDocument();
16
+ });
17
+ });
@@ -0,0 +1,69 @@
1
+ import { useState } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+
5
+ import { Select } from "../Select";
6
+
7
+ describe("Select controlled state", () => {
8
+ it("calls onValueChange with the new value string when the user selects a different option", async () => {
9
+ // Arrange
10
+ const onValueChange = vi.fn();
11
+ const user = userEvent.setup();
12
+
13
+ function Wrapper() {
14
+ const [value, setValue] = useState("apple");
15
+ const handle = (next: string) => {
16
+ onValueChange(next);
17
+ setValue(next);
18
+ };
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>
23
+ </Select.Root>
24
+ );
25
+ }
26
+
27
+ render(<Wrapper />);
28
+
29
+ // Act
30
+ await user.selectOptions(screen.getByRole("combobox"), "banana");
31
+
32
+ // Assert
33
+ expect(onValueChange).toHaveBeenCalledTimes(1);
34
+ expect(onValueChange).toHaveBeenCalledWith("banana");
35
+ });
36
+
37
+ it("invokes the consumer's own onChange handler alongside onValueChange", async () => {
38
+ // Arrange
39
+ const onChange = vi.fn();
40
+ const onValueChange = vi.fn();
41
+ const user = userEvent.setup();
42
+
43
+ function Wrapper() {
44
+ const [value, setValue] = useState("apple");
45
+ return (
46
+ <Select.Root
47
+ value={value}
48
+ onChange={(event) => {
49
+ onChange(event.target.value);
50
+ setValue(event.target.value);
51
+ }}
52
+ onValueChange={onValueChange}
53
+ >
54
+ <Select.Option value="apple">Apple</Select.Option>
55
+ <Select.Option value="banana">Banana</Select.Option>
56
+ </Select.Root>
57
+ );
58
+ }
59
+
60
+ render(<Wrapper />);
61
+
62
+ // Act
63
+ await user.selectOptions(screen.getByRole("combobox"), "banana");
64
+
65
+ // Assert
66
+ expect(onChange).toHaveBeenCalledWith("banana");
67
+ expect(onValueChange).toHaveBeenCalledWith("banana");
68
+ });
69
+ });
@@ -0,0 +1,29 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Select } from "../Select";
4
+
5
+ describe("Select data attributes", () => {
6
+ it("renders data-disabled='' on the root <select> when disabled is true so CSS can style the disabled state", () => {
7
+ // Arrange & Act
8
+ render(
9
+ <Select.Root disabled>
10
+ <Select.Option value="apple">Apple</Select.Option>
11
+ </Select.Root>,
12
+ );
13
+
14
+ // Assert
15
+ expect(screen.getByRole("combobox")).toHaveAttribute("data-disabled", "");
16
+ });
17
+
18
+ it("omits data-disabled entirely when disabled is false or absent so the enabled state has nothing to override", () => {
19
+ // Arrange & Act
20
+ render(
21
+ <Select.Root>
22
+ <Select.Option value="apple">Apple</Select.Option>
23
+ </Select.Root>,
24
+ );
25
+
26
+ // Assert
27
+ expect(screen.getByRole("combobox")).not.toHaveAttribute("data-disabled");
28
+ });
29
+ });
@@ -0,0 +1,150 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Field } from "../../Field";
4
+ import { Select } from "../Select";
5
+
6
+ function renderSelect(children?: React.ReactNode) {
7
+ return (
8
+ <>
9
+ {children}
10
+ <Select.Option value="apple">Apple</Select.Option>
11
+ <Select.Option value="banana">Banana</Select.Option>
12
+ </>
13
+ );
14
+ }
15
+
16
+ describe("Select — Field integration", () => {
17
+ it("inherits the field id when no id prop is passed", () => {
18
+ // Arrange & Act
19
+ render(
20
+ <Field.Root id="fruit">
21
+ <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
22
+ </Field.Root>,
23
+ );
24
+
25
+ // Assert
26
+ expect(screen.getByRole("combobox", { name: "Fruit" })).toHaveAttribute(
27
+ "id",
28
+ "fruit",
29
+ );
30
+ });
31
+
32
+ it("consumer-supplied id wins over the field id", () => {
33
+ // Arrange & Act
34
+ render(
35
+ <Field.Root id="fruit">
36
+ <Select.Root id="my-fruit" aria-label="Fruit">
37
+ {renderSelect()}
38
+ </Select.Root>
39
+ </Field.Root>,
40
+ );
41
+
42
+ // Assert
43
+ expect(screen.getByRole("combobox", { name: "Fruit" })).toHaveAttribute(
44
+ "id",
45
+ "my-fruit",
46
+ );
47
+ });
48
+
49
+ it("inherits aria-describedby pointing at the field's descriptionId", () => {
50
+ // Arrange & Act
51
+ render(
52
+ <Field.Root id="fruit">
53
+ <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
54
+ </Field.Root>,
55
+ );
56
+
57
+ // Assert
58
+ expect(screen.getByRole("combobox", { name: "Fruit" })).toHaveAttribute(
59
+ "aria-describedby",
60
+ "fruit-description",
61
+ );
62
+ });
63
+
64
+ it("includes the errorId in aria-describedby when the field is invalid", () => {
65
+ // Arrange & Act
66
+ render(
67
+ <Field.Root id="fruit" invalid>
68
+ <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
69
+ </Field.Root>,
70
+ );
71
+
72
+ // Assert
73
+ expect(
74
+ screen
75
+ .getByRole("combobox", { name: "Fruit" })
76
+ .getAttribute("aria-describedby"),
77
+ ).toBe("fruit-description fruit-error");
78
+ });
79
+
80
+ it("appends consumer-supplied aria-describedby to the field-supplied ids", () => {
81
+ // Arrange & Act
82
+ render(
83
+ <Field.Root id="fruit">
84
+ <Select.Root aria-label="Fruit" aria-describedby="extra-hint">
85
+ {renderSelect()}
86
+ </Select.Root>
87
+ </Field.Root>,
88
+ );
89
+
90
+ // Assert
91
+ expect(
92
+ screen
93
+ .getByRole("combobox", { name: "Fruit" })
94
+ .getAttribute("aria-describedby"),
95
+ ).toBe("extra-hint fruit-description");
96
+ });
97
+
98
+ it("inherits aria-invalid='true' when the field is invalid", () => {
99
+ // Arrange & Act
100
+ render(
101
+ <Field.Root id="fruit" invalid>
102
+ <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
103
+ </Field.Root>,
104
+ );
105
+
106
+ // Assert
107
+ expect(screen.getByRole("combobox", { name: "Fruit" })).toHaveAttribute(
108
+ "aria-invalid",
109
+ "true",
110
+ );
111
+ });
112
+
113
+ it("inherits disabled from the field", () => {
114
+ // Arrange & Act
115
+ render(
116
+ <Field.Root id="fruit" disabled>
117
+ <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
118
+ </Field.Root>,
119
+ );
120
+
121
+ // Assert
122
+ expect(screen.getByRole("combobox", { name: "Fruit" })).toBeDisabled();
123
+ });
124
+
125
+ it("inherits required from the field", () => {
126
+ // Arrange & Act
127
+ render(
128
+ <Field.Root id="fruit" required>
129
+ <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>
130
+ </Field.Root>,
131
+ );
132
+
133
+ // Assert
134
+ expect(screen.getByRole("combobox", { name: "Fruit" })).toBeRequired();
135
+ });
136
+
137
+ it("Select outside Field.Root behaves identically to before — no field-derived attributes", () => {
138
+ // Arrange & Act
139
+ render(
140
+ <Select.Root aria-label="Fruit">{renderSelect()}</Select.Root>,
141
+ );
142
+ const select = screen.getByRole("combobox", { name: "Fruit" });
143
+
144
+ // Assert
145
+ expect(select).not.toHaveAttribute("aria-describedby");
146
+ expect(select).not.toHaveAttribute("aria-invalid");
147
+ expect(select).not.toHaveAttribute("disabled");
148
+ expect(select).not.toHaveAttribute("required");
149
+ });
150
+ });
@@ -0,0 +1,42 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Select } from "../Select";
4
+
5
+ describe("Select group", () => {
6
+ it("renders an <optgroup> with the given label so options can be visually grouped", () => {
7
+ // Arrange & Act
8
+ render(
9
+ <Select.Root>
10
+ <Select.Group label="Fruits">
11
+ <Select.Option value="apple">Apple</Select.Option>
12
+ <Select.Option value="banana">Banana</Select.Option>
13
+ </Select.Group>
14
+ <Select.Group label="Vegetables">
15
+ <Select.Option value="carrot">Carrot</Select.Option>
16
+ </Select.Group>
17
+ </Select.Root>,
18
+ );
19
+
20
+ // Assert — native <optgroup> has implicit role="group" with the
21
+ // label attribute as its accessible name.
22
+ expect(screen.getByRole("group", { name: "Fruits" })).toBeInTheDocument();
23
+ expect(
24
+ screen.getByRole("group", { name: "Vegetables" }),
25
+ ).toBeInTheDocument();
26
+ });
27
+
28
+ it("nests its Option children inside the rendered <optgroup>", () => {
29
+ // Arrange & Act
30
+ render(
31
+ <Select.Root>
32
+ <Select.Group label="Fruits">
33
+ <Select.Option value="apple">Apple</Select.Option>
34
+ </Select.Group>
35
+ </Select.Root>,
36
+ );
37
+
38
+ // Assert
39
+ const option = screen.getByRole("option", { name: "Apple" });
40
+ expect(option.parentElement?.tagName).toBe("OPTGROUP");
41
+ });
42
+ });
@@ -0,0 +1,32 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Select } from "../Select";
4
+
5
+ describe("Select placeholder", () => {
6
+ it("renders a non-selectable placeholder option that holds the initial selection until the user picks something", () => {
7
+ // Arrange & Act
8
+ const { container } = render(
9
+ <Select.Root>
10
+ <Select.Placeholder>Choose a fruit…</Select.Placeholder>
11
+ <Select.Option value="apple">Apple</Select.Option>
12
+ </Select.Root>,
13
+ );
14
+
15
+ // Assert — placeholder is in the DOM with the attributes that make it
16
+ // unreachable from the dropdown after the first selection. The
17
+ // `hidden` attribute also pulls it out of the ARIA tree, so query the
18
+ // DOM directly rather than via getByRole.
19
+ const placeholder = container.querySelector(
20
+ "option[hidden]",
21
+ ) as HTMLOptionElement | null;
22
+ expect(placeholder).not.toBeNull();
23
+ expect(placeholder).toBeDisabled();
24
+ expect(placeholder).toHaveAttribute("value", "");
25
+ expect(placeholder).toHaveTextContent("Choose a fruit…");
26
+
27
+ // …and is the initial selection because it's the first option with an
28
+ // empty value.
29
+ const select = screen.getByRole("combobox") as HTMLSelectElement;
30
+ expect(select.value).toBe("");
31
+ });
32
+ });
@@ -0,0 +1,2 @@
1
+ export { Select } from "./Select";
2
+ export * from "./types";
@@ -0,0 +1,89 @@
1
+ import { ChangeEventHandler, ComponentProps, ReactNode, Ref } from "react";
2
+
3
+ type SelectRootBaseProps = Omit<
4
+ ComponentProps<"select">,
5
+ "value" | "defaultValue" | "multiple" | "onChange"
6
+ > & {
7
+ children?: ReactNode;
8
+ ref?: Ref<HTMLSelectElement>;
9
+ /**
10
+ * Native `change` handler. Fires alongside `onValueChange` whenever the
11
+ * user picks a different option. Use this when you want the raw
12
+ * `ChangeEvent` (e.g. to inspect `event.target.validity`).
13
+ */
14
+ onChange?: ChangeEventHandler<HTMLSelectElement>;
15
+ /**
16
+ * When `true`, Root delegates to a single consumer-supplied element
17
+ * (expected to render a `<select>`) and merges its own props onto it
18
+ * via the {@link Slot} pattern. The placeholder-detection inside Root
19
+ * walks direct children only, so placeholder + `asChild` requires the
20
+ * consumer to set `defaultValue=""` explicitly.
21
+ */
22
+ asChild?: boolean;
23
+ };
24
+
25
+ type SelectRootUncontrolledProps = SelectRootBaseProps & {
26
+ defaultValue?: string;
27
+ value?: never;
28
+ onValueChange?: (value: string) => void;
29
+ };
30
+
31
+ type SelectRootControlledProps = SelectRootBaseProps & {
32
+ defaultValue?: never;
33
+ value: string;
34
+ onValueChange: (value: string) => void;
35
+ };
36
+
37
+ /**
38
+ * Props for {@link Select.Root}.
39
+ *
40
+ * Two state modes are statically discriminated at the type level so only
41
+ * one shape is accepted by TypeScript:
42
+ *
43
+ * - **Uncontrolled** — pass `defaultValue` (or omit it). The browser owns
44
+ * the selection state. `onValueChange` is optional.
45
+ * - **Controlled** — pass `value` and `onValueChange` together. The
46
+ * parent owns the selection; the component defers every transition
47
+ * back through the callback.
48
+ *
49
+ * Native `multiple`-selection mode is not supported in v1.
50
+ */
51
+ export type SelectRootProps =
52
+ | SelectRootUncontrolledProps
53
+ | SelectRootControlledProps;
54
+
55
+ /**
56
+ * Props for {@link Select.Option} — all `OptionHTMLAttributes` on the
57
+ * underlying `<option>` element, plus a typed `ref`.
58
+ */
59
+ export type SelectOptionProps = ComponentProps<"option"> & {
60
+ children?: ReactNode;
61
+ ref?: Ref<HTMLOptionElement>;
62
+ };
63
+
64
+ /**
65
+ * Props for {@link Select.Group} — all `OptgroupHTMLAttributes` on the
66
+ * underlying `<optgroup>` element, plus a typed `ref`. `label` is
67
+ * required by the native element and is what assistive technology
68
+ * announces for the group.
69
+ */
70
+ export type SelectGroupProps = ComponentProps<"optgroup"> & {
71
+ label: string;
72
+ children?: ReactNode;
73
+ ref?: Ref<HTMLOptGroupElement>;
74
+ };
75
+
76
+ /**
77
+ * Props for {@link Select.Placeholder}.
78
+ *
79
+ * `value`, `disabled`, and `hidden` are owned by the component — the
80
+ * placeholder always has `value=""`, is always disabled, and is always
81
+ * hidden from the dropdown — so they can't be set by the consumer.
82
+ */
83
+ export type SelectPlaceholderProps = Omit<
84
+ ComponentProps<"option">,
85
+ "value" | "disabled" | "hidden"
86
+ > & {
87
+ children?: ReactNode;
88
+ ref?: Ref<HTMLOptionElement>;
89
+ };