@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,26 @@
1
+ import { EmptyState } from "..";
2
+ import { render, screen } from "@testing-library/react";
3
+
4
+ describe("EmptyState.Title component", () => {
5
+ it("should render a paragraph containing its children", () => {
6
+ // Arrange
7
+ render(<EmptyState.Title>No results found</EmptyState.Title>);
8
+
9
+ // Assert
10
+ const title = screen.getByText("No results found");
11
+ expect(title.tagName).toBe("P");
12
+ });
13
+
14
+ it("should render the consumer element with asChild", () => {
15
+ // Arrange
16
+ render(
17
+ <EmptyState.Title asChild>
18
+ <h2>No results found</h2>
19
+ </EmptyState.Title>,
20
+ );
21
+
22
+ // Assert
23
+ const title = screen.getByRole("heading", { name: "No results found" });
24
+ expect(title.tagName).toBe("H2");
25
+ });
26
+ });
@@ -0,0 +1,2 @@
1
+ export * from "./EmptyState";
2
+ export * from "./types";
@@ -0,0 +1,21 @@
1
+ import { ComponentProps } from "react";
2
+
3
+ type WithAsChild = {
4
+ /** Render the consumer's own element instead of the default, via `Slot`. */
5
+ asChild?: boolean;
6
+ };
7
+
8
+ /** Props for {@link EmptyState.Root} — all `<div>` props plus `asChild`. */
9
+ export type EmptyStateRootProps = ComponentProps<"div"> & WithAsChild;
10
+
11
+ /** Props for {@link EmptyState.Media} — all `<div>` props plus `asChild`. */
12
+ export type EmptyStateMediaProps = ComponentProps<"div"> & WithAsChild;
13
+
14
+ /** Props for {@link EmptyState.Title} — all `<p>` props plus `asChild`. */
15
+ export type EmptyStateTitleProps = ComponentProps<"p"> & WithAsChild;
16
+
17
+ /** Props for {@link EmptyState.Description} — all `<p>` props plus `asChild`. */
18
+ export type EmptyStateDescriptionProps = ComponentProps<"p"> & WithAsChild;
19
+
20
+ /** Props for {@link EmptyState.Actions} — all `<div>` props plus `asChild`. */
21
+ export type EmptyStateActionsProps = ComponentProps<"div"> & WithAsChild;
@@ -0,0 +1,239 @@
1
+ import { useId, useMemo } from "react";
2
+
3
+ import { Slot } from "../Slot";
4
+ import { FieldContext } from "./FieldContext";
5
+ import { useFieldContext } from "./hooks";
6
+ import {
7
+ FieldDescriptionProps,
8
+ FieldErrorTextProps,
9
+ FieldLabelProps,
10
+ FieldRootProps,
11
+ } from "./types";
12
+
13
+ /**
14
+ * The root of a Field — provides {@link FieldContext} (stable id plus
15
+ * derived `descriptionId` / `errorId`, and the cascaded `invalid` /
16
+ * `disabled` / `required` flags) and renders a `<div data-field>`
17
+ * wrapper.
18
+ *
19
+ * **ID propagation.** If `id` is passed, it's used directly; otherwise
20
+ * a stable id is auto-generated via React's `useId`. The
21
+ * `descriptionId` and `errorId` are derived from the field id
22
+ * (`<id>-description` / `<id>-error`) and are exposed via context to
23
+ * any sub-component or context-aware control (e.g. `Input`).
24
+ *
25
+ * **State cascade.** `invalid`, `disabled`, and `required` cascade via
26
+ * context to any control reading {@link FieldContext} — `Input`
27
+ * inherits them automatically when nested inside a `<Field.Root>` and
28
+ * the consumer hasn't passed an explicit override. The wrapper also
29
+ * carries `data-field-invalid` / `data-field-disabled` /
30
+ * `data-field-required` attributes when the corresponding flag is
31
+ * truthy, so CSS can style the whole field group on a single selector.
32
+ *
33
+ * **`asChild` composition.** Pass `asChild` to render the consumer's
34
+ * element instead of `<div>` — e.g. a semantic `<fieldset>` or
35
+ * `<section>`. The `data-field-*` hooks and context provider stay
36
+ * intact.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * <Field.Root id="email" invalid={!!errors.email}>
41
+ * <Field.Label>Email</Field.Label>
42
+ * <Input type="email" {...register("email")} />
43
+ * <Field.Description>We won't share it.</Field.Description>
44
+ * <Field.ErrorText>{errors.email?.message}</Field.ErrorText>
45
+ * </Field.Root>
46
+ * ```
47
+ */
48
+ function FieldRoot({
49
+ id: idProp,
50
+ invalid = false,
51
+ disabled = false,
52
+ required = false,
53
+ asChild = false,
54
+ children,
55
+ ...rest
56
+ }: FieldRootProps) {
57
+ const autoId = useId();
58
+ const id = idProp ?? autoId;
59
+ const descriptionId = `${id}-description`;
60
+ const errorId = `${id}-error`;
61
+
62
+ const value = useMemo(
63
+ () => ({ id, descriptionId, errorId, invalid, disabled, required }),
64
+ [id, descriptionId, errorId, invalid, disabled, required],
65
+ );
66
+
67
+ const rootProps = {
68
+ ...rest,
69
+ "data-field": "",
70
+ "data-field-invalid": invalid ? "" : undefined,
71
+ "data-field-disabled": disabled ? "" : undefined,
72
+ "data-field-required": required ? "" : undefined,
73
+ };
74
+
75
+ return (
76
+ <FieldContext.Provider value={value}>
77
+ {asChild ? (
78
+ <Slot {...rootProps}>{children}</Slot>
79
+ ) : (
80
+ <div {...rootProps}>{children}</div>
81
+ )}
82
+ </FieldContext.Provider>
83
+ );
84
+ }
85
+
86
+ FieldRoot.displayName = "FieldRoot";
87
+
88
+ /**
89
+ * Renders a `<label>` wired to the field's id via `htmlFor` — clicking
90
+ * the label focuses the associated control.
91
+ *
92
+ * **`asChild` composition.** Pass `asChild` to render any consumer
93
+ * element with the `htmlFor` attribute merged on.
94
+ *
95
+ * @throws If rendered outside a `<Field.Root>`.
96
+ */
97
+ function FieldLabel({ asChild = false, children, ...rest }: FieldLabelProps) {
98
+ const { id } = useFieldContext();
99
+ const labelProps = { ...rest, htmlFor: id };
100
+
101
+ if (asChild) {
102
+ return <Slot {...labelProps}>{children}</Slot>;
103
+ }
104
+ return <label {...labelProps}>{children}</label>;
105
+ }
106
+
107
+ FieldLabel.displayName = "FieldLabel";
108
+
109
+ /**
110
+ * Renders a `<div>` carrying the field's `descriptionId`. Reference it
111
+ * from your control via `aria-describedby` for assistive technology to
112
+ * announce it alongside the label — `Input` does this automatically
113
+ * when nested inside a `<Field.Root>`.
114
+ *
115
+ * **`asChild` composition.** Pass `asChild` to render a `<p>`,
116
+ * `<span>`, or any other element with the `id` merged on.
117
+ *
118
+ * @throws If rendered outside a `<Field.Root>`.
119
+ */
120
+ function FieldDescription({
121
+ asChild = false,
122
+ children,
123
+ ...rest
124
+ }: FieldDescriptionProps) {
125
+ const { descriptionId } = useFieldContext();
126
+ const descriptionProps = { ...rest, id: descriptionId };
127
+
128
+ if (asChild) {
129
+ return <Slot {...descriptionProps}>{children}</Slot>;
130
+ }
131
+ return <div {...descriptionProps}>{children}</div>;
132
+ }
133
+
134
+ FieldDescription.displayName = "FieldDescription";
135
+
136
+ /**
137
+ * Renders a `<div role="alert">` carrying the field's `errorId` — only
138
+ * when the field is in an invalid state. Returns `null` otherwise, so
139
+ * consumers can render it unconditionally and rely on the field to gate
140
+ * visibility.
141
+ *
142
+ * **`asChild` composition.** Pass `asChild` to render the consumer's
143
+ * element with the `id` and `role="alert"` merged on.
144
+ *
145
+ * @example
146
+ * ```tsx
147
+ * <Field.Root invalid={!!errors.email}>
148
+ * <Field.ErrorText>{errors.email?.message ?? "Required"}</Field.ErrorText>
149
+ * </Field.Root>
150
+ * ```
151
+ *
152
+ * @throws If rendered outside a `<Field.Root>`.
153
+ */
154
+ function FieldErrorText({
155
+ asChild = false,
156
+ children,
157
+ ...rest
158
+ }: FieldErrorTextProps) {
159
+ const { errorId, invalid } = useFieldContext();
160
+ if (!invalid) return null;
161
+ const errorProps = { ...rest, id: errorId, role: "alert" as const };
162
+
163
+ if (asChild) {
164
+ return <Slot {...errorProps}>{children}</Slot>;
165
+ }
166
+ return <div {...errorProps}>{children}</div>;
167
+ }
168
+
169
+ FieldErrorText.displayName = "FieldErrorText";
170
+
171
+ type TFieldCompound = typeof FieldRoot & {
172
+ Root: typeof FieldRoot;
173
+ Label: typeof FieldLabel;
174
+ Description: typeof FieldDescription;
175
+ ErrorText: typeof FieldErrorText;
176
+ };
177
+
178
+ /**
179
+ * Headless, accessible **Field** — a coordinator compound that owns the
180
+ * `id`, `aria-describedby`, and `invalid` / `disabled` / `required`
181
+ * wiring for a single form control plus its label, description, and
182
+ * error message. Zero styles ship.
183
+ *
184
+ * `Field` does not render the control itself — it sits *around* an
185
+ * existing control (`Input`, `InputGroup` wrapping an `Input`, a future
186
+ * `Textarea`, etc.). The control opts into {@link FieldContext} to
187
+ * inherit the wiring; the sub-components below render the
188
+ * label / description / error UI.
189
+ *
190
+ * `Field` is callable (alias of `Field.Root`) and carries its
191
+ * sub-components as static properties:
192
+ *
193
+ * - {@link FieldRoot | `Field.Root`} — provides context, renders the
194
+ * `<div data-field>` wrapper (or any element via `asChild`).
195
+ * - {@link FieldLabel | `Field.Label`} — `<label htmlFor>` wired to the
196
+ * field id.
197
+ * - {@link FieldDescription | `Field.Description`} — `<div id>` for
198
+ * help text, referenced via `aria-describedby`.
199
+ * - {@link FieldErrorText | `Field.ErrorText`} — `<div role="alert">`
200
+ * rendered only when invalid.
201
+ *
202
+ * Every part supports `asChild` for the consumer to swap in their own
203
+ * element while keeping the wiring.
204
+ *
205
+ * @example Basic
206
+ * ```tsx
207
+ * import { Field, Input } from "@primitiv-ui/react";
208
+ *
209
+ * <Field.Root>
210
+ * <Field.Label>Email</Field.Label>
211
+ * <Input type="email" required />
212
+ * <Field.Description>We won't share it.</Field.Description>
213
+ * </Field.Root>
214
+ * ```
215
+ *
216
+ * @example With error and InputGroup adornments
217
+ * ```tsx
218
+ * <Field.Root id="email" invalid={!!errors.email}>
219
+ * <Field.Label>Email</Field.Label>
220
+ * <InputGroup>
221
+ * <InputGroup.LeadingAdornment><MailIcon aria-hidden="true" /></InputGroup.LeadingAdornment>
222
+ * <Input type="email" {...register("email")} />
223
+ * </InputGroup>
224
+ * <Field.ErrorText>{errors.email?.message}</Field.ErrorText>
225
+ * </Field.Root>
226
+ * ```
227
+ *
228
+ * @see {@link FieldContext} — the context shape that controls can opt into.
229
+ */
230
+ const FieldCompound: TFieldCompound = Object.assign(FieldRoot, {
231
+ Root: FieldRoot,
232
+ Label: FieldLabel,
233
+ Description: FieldDescription,
234
+ ErrorText: FieldErrorText,
235
+ });
236
+
237
+ FieldCompound.displayName = "Field";
238
+
239
+ export { FieldCompound as Field };
@@ -0,0 +1,22 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ export type FieldContextValue = {
4
+ /** Stable id for the control wired to this field. */
5
+ id: string;
6
+ /** Id of the {@link Field.Description} element, when rendered. */
7
+ descriptionId: string;
8
+ /** Id of the {@link Field.ErrorText} element, when rendered. */
9
+ errorId: string;
10
+ /** Whether the field is in an invalid state. */
11
+ invalid: boolean;
12
+ /** Whether the field is disabled. */
13
+ disabled: boolean;
14
+ /** Whether the field is required. */
15
+ required: boolean;
16
+ };
17
+
18
+ export const [FieldContext, useFieldContext] =
19
+ createStrictContext<FieldContextValue>(
20
+ "Field sub-components must be rendered inside a <Field.Root>.",
21
+ "FieldContext",
22
+ );
@@ -0,0 +1,167 @@
1
+ # Field
2
+
3
+ A headless, accessible coordinator that owns the `id`,
4
+ `aria-describedby`, and `invalid` / `disabled` / `required` wiring for
5
+ a single form control plus its label, description, and error message.
6
+ Zero styles ship.
7
+
8
+ ```tsx
9
+ import { Field, Input } from "@primitiv-ui/react";
10
+
11
+ <Field.Root>
12
+ <Field.Label>Email</Field.Label>
13
+ <Input type="email" required />
14
+ <Field.Description>We won't share it.</Field.Description>
15
+ </Field.Root>
16
+ ```
17
+
18
+ ## Anatomy
19
+
20
+ | Part | Element | Styling hook |
21
+ | --------------------------- | ------------------------ | ------------------------------------- |
22
+ | `Field` / `Field.Root` | `<div>` | `data-field=""` |
23
+ | `Field.Label` | `<label>` (`htmlFor=id`) | n/a |
24
+ | `Field.Description` | `<div id=…-description>` | n/a |
25
+ | `Field.ErrorText` | `<div role="alert">` | n/a (only renders when invalid) |
26
+
27
+ Plus three state-driven hooks on the root: `data-field-invalid`,
28
+ `data-field-disabled`, `data-field-required`.
29
+
30
+ `Field` does **not** render the control itself — it sits *around* an
31
+ existing control. `Input` reads `FieldContext` automatically and
32
+ inherits the wiring; outside a `<Field.Root>`, `Input` behaves exactly
33
+ as before.
34
+
35
+ ## State cascade
36
+
37
+ Three props on `Field.Root` propagate via context to the control:
38
+
39
+ ```tsx
40
+ <Field.Root invalid disabled required>
41
+ <Field.Label>Email</Field.Label>
42
+ <Input type="email" />
43
+ {/* ^ inherits aria-invalid="true", disabled, required, plus
44
+ aria-describedby pointing at the error / description ids */}
45
+ </Field.Root>
46
+ ```
47
+
48
+ Consumer-supplied props on the control always win — pass
49
+ `disabled={false}` on `Input` and it stays enabled regardless of
50
+ `Field.Root`'s `disabled`.
51
+
52
+ ## ID and aria-describedby wiring
53
+
54
+ `Field.Root` either accepts an explicit `id` or auto-generates one via
55
+ `useId`. The derived ids:
56
+
57
+ | Reference | Form |
58
+ | -------------------- | ------------------- |
59
+ | `field.id` | `<id>` |
60
+ | `field.descriptionId`| `<id>-description` |
61
+ | `field.errorId` | `<id>-error` |
62
+
63
+ When `Input` is rendered inside `Field.Root`:
64
+
65
+ - `id` defaults to `field.id`
66
+ - `aria-describedby` composes consumer-supplied ids first, then the
67
+ field's `descriptionId`, then the `errorId` (only when invalid)
68
+ - `aria-invalid` is set to `"true"` when `field.invalid` is true
69
+
70
+ If you don't render `Field.Description` or `Field.ErrorText`, the
71
+ referenced ids point at nothing — screen readers handle missing ids
72
+ gracefully, but it's worth knowing.
73
+
74
+ ## Validation flow
75
+
76
+ `Field.ErrorText` returns `null` unless `Field.Root` is `invalid`, so
77
+ you can render it unconditionally:
78
+
79
+ ```tsx
80
+ <Field.Root invalid={!!errors.email}>
81
+ <Field.Label>Email</Field.Label>
82
+ <Input type="email" required {...register("email")} />
83
+ <Field.ErrorText>{errors.email?.message ?? "Required"}</Field.ErrorText>
84
+ </Field.Root>
85
+ ```
86
+
87
+ ## With react-hook-form
88
+
89
+ `Field` doesn't know about RHF — it just provides the wiring; you tell
90
+ it whether the field is invalid by passing `invalid` to `Field.Root`,
91
+ and spread `register` onto the `Input`:
92
+
93
+ ```tsx
94
+ import { useForm } from "react-hook-form";
95
+
96
+ const { register, formState: { errors } } = useForm();
97
+
98
+ <Field.Root invalid={!!errors.email}>
99
+ <Field.Label>Email</Field.Label>
100
+ <Input type="email" required {...register("email")} />
101
+ <Field.Description>We won't spam you.</Field.Description>
102
+ <Field.ErrorText>{errors.email?.message}</Field.ErrorText>
103
+ </Field.Root>
104
+ ```
105
+
106
+ ## With InputGroup
107
+
108
+ `Field` and `InputGroup` compose freely — `Field` wraps the entire
109
+ field group; `InputGroup` wraps just the control plus its adornments.
110
+
111
+ ```tsx
112
+ <Field.Root invalid={!!errors.email}>
113
+ <Field.Label>Email</Field.Label>
114
+ <InputGroup>
115
+ <InputGroup.LeadingAdornment>
116
+ <MailIcon aria-hidden="true" />
117
+ </InputGroup.LeadingAdornment>
118
+ <Input type="email" {...register("email")} />
119
+ </InputGroup>
120
+ <Field.ErrorText>{errors.email?.message}</Field.ErrorText>
121
+ </Field.Root>
122
+ ```
123
+
124
+ `Input` still reads `FieldContext` even when wrapped in `InputGroup` —
125
+ the context flows through any DOM nesting.
126
+
127
+ ## Sub-components throw when used outside a Root
128
+
129
+ `Field.Label`, `Field.Description`, and `Field.ErrorText` throw a
130
+ clear error if rendered outside a `<Field.Root>`. The strict-context
131
+ hook ensures the typo / misuse fails loudly rather than rendering
132
+ silently broken markup.
133
+
134
+ ## `asChild` composition
135
+
136
+ Every part supports `asChild` for swapping the default element while
137
+ preserving the wiring:
138
+
139
+ ```tsx
140
+ // Render the field as a semantic <fieldset>
141
+ <Field.Root asChild>
142
+ <fieldset>
143
+ <Field.Label>Email</Field.Label>
144
+ <Input type="email" />
145
+ </fieldset>
146
+ </Field.Root>
147
+
148
+ // Render the description as a <p> instead of a <div>
149
+ <Field.Description asChild>
150
+ <p className="hint">We won't share it.</p>
151
+ </Field.Description>
152
+
153
+ // Render the error as a <p>
154
+ <Field.ErrorText asChild>
155
+ <p className="error">{errors.email?.message}</p>
156
+ </Field.ErrorText>
157
+
158
+ // Render the label on a custom heading slot
159
+ <Field.Label asChild>
160
+ <span className="label-text">Email</span>
161
+ </Field.Label>
162
+ ```
163
+
164
+ Merge rules follow the standard Slot contract: event handlers compose
165
+ (child runs first), `style` is shallow-merged, `className` is
166
+ concatenated, and the wired attributes (`htmlFor`, `id`, `role`,
167
+ `data-field*`) are always merged onto the consumer element.
@@ -0,0 +1,83 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Field } from "../Field";
4
+
5
+ describe("Field asChild composition", () => {
6
+ it("Root asChild renders the consumer element with data-field merged on", () => {
7
+ // Arrange & Act
8
+ render(
9
+ <Field.Root asChild data-testid="field">
10
+ <fieldset />
11
+ </Field.Root>,
12
+ );
13
+ const root = screen.getByTestId("field");
14
+
15
+ // Assert
16
+ expect(root.tagName).toBe("FIELDSET");
17
+ expect(root).toHaveAttribute("data-field", "");
18
+ });
19
+
20
+ it("Root asChild keeps data-field-invalid when invalid", () => {
21
+ // Arrange & Act
22
+ render(
23
+ <Field.Root asChild data-testid="field" invalid>
24
+ <section />
25
+ </Field.Root>,
26
+ );
27
+
28
+ // Assert
29
+ expect(screen.getByTestId("field")).toHaveAttribute(
30
+ "data-field-invalid",
31
+ "",
32
+ );
33
+ });
34
+
35
+ it("Label asChild renders the consumer element with htmlFor merged on", () => {
36
+ // Arrange & Act
37
+ render(
38
+ <Field.Root id="email">
39
+ <Field.Label asChild>
40
+ <span data-testid="label">Email</span>
41
+ </Field.Label>
42
+ </Field.Root>,
43
+ );
44
+ const label = screen.getByTestId("label");
45
+
46
+ // Assert
47
+ expect(label.tagName).toBe("SPAN");
48
+ expect(label).toHaveAttribute("for", "email");
49
+ });
50
+
51
+ it("Description asChild renders the consumer element with id merged on", () => {
52
+ // Arrange & Act
53
+ render(
54
+ <Field.Root id="email">
55
+ <Field.Description asChild>
56
+ <p data-testid="hint">We won't share it.</p>
57
+ </Field.Description>
58
+ </Field.Root>,
59
+ );
60
+ const description = screen.getByTestId("hint");
61
+
62
+ // Assert
63
+ expect(description.tagName).toBe("P");
64
+ expect(description).toHaveAttribute("id", "email-description");
65
+ });
66
+
67
+ it("ErrorText asChild renders the consumer element with id and role merged on", () => {
68
+ // Arrange & Act
69
+ render(
70
+ <Field.Root id="email" invalid>
71
+ <Field.ErrorText asChild>
72
+ <p data-testid="error">Required.</p>
73
+ </Field.ErrorText>
74
+ </Field.Root>,
75
+ );
76
+ const error = screen.getByTestId("error");
77
+
78
+ // Assert
79
+ expect(error.tagName).toBe("P");
80
+ expect(error).toHaveAttribute("id", "email-error");
81
+ expect(error).toHaveAttribute("role", "alert");
82
+ });
83
+ });
@@ -0,0 +1,104 @@
1
+ import { render, screen } from "@testing-library/react";
2
+
3
+ import { Field } from "../Field";
4
+
5
+ describe("Field basic rendering", () => {
6
+ it("renders a wrapper element with data-field on the root", () => {
7
+ // Arrange & Act
8
+ render(<Field.Root data-testid="field" />);
9
+
10
+ // Assert
11
+ const root = screen.getByTestId("field");
12
+ expect(root.tagName).toBe("DIV");
13
+ expect(root).toHaveAttribute("data-field", "");
14
+ });
15
+
16
+ it("Field is callable as an alias of Field.Root", () => {
17
+ // Arrange & Act
18
+ render(<Field data-testid="field" />);
19
+
20
+ // Assert
21
+ expect(screen.getByTestId("field").tagName).toBe("DIV");
22
+ });
23
+
24
+ it("Field.Label renders a <label> wired to an auto-generated id", () => {
25
+ // Arrange & Act
26
+ render(
27
+ <Field.Root>
28
+ <Field.Label>Email</Field.Label>
29
+ </Field.Root>,
30
+ );
31
+ const label = screen.getByText("Email");
32
+
33
+ // Assert
34
+ expect(label.tagName).toBe("LABEL");
35
+ expect(label).toHaveAttribute("for");
36
+ expect(label.getAttribute("for")).toBeTruthy();
37
+ });
38
+
39
+ it("Field.Label uses a consumer-supplied id on Root", () => {
40
+ // Arrange & Act
41
+ render(
42
+ <Field.Root id="email">
43
+ <Field.Label>Email</Field.Label>
44
+ </Field.Root>,
45
+ );
46
+
47
+ // Assert
48
+ expect(screen.getByText("Email")).toHaveAttribute("for", "email");
49
+ });
50
+
51
+ it("Field.Description renders a <div> with id derived from the field id", () => {
52
+ // Arrange & Act
53
+ render(
54
+ <Field.Root id="email">
55
+ <Field.Description>We won't share it.</Field.Description>
56
+ </Field.Root>,
57
+ );
58
+
59
+ // Assert
60
+ expect(screen.getByText("We won't share it.")).toHaveAttribute(
61
+ "id",
62
+ "email-description",
63
+ );
64
+ });
65
+
66
+ it("Field.ErrorText renders a <div role='alert'> with derived id when invalid", () => {
67
+ // Arrange & Act
68
+ render(
69
+ <Field.Root id="email" invalid>
70
+ <Field.ErrorText>Required.</Field.ErrorText>
71
+ </Field.Root>,
72
+ );
73
+ const error = screen.getByText("Required.");
74
+
75
+ // Assert
76
+ expect(error.tagName).toBe("DIV");
77
+ expect(error).toHaveAttribute("role", "alert");
78
+ expect(error).toHaveAttribute("id", "email-error");
79
+ });
80
+
81
+ it("renders children inside the wrapper in source order", () => {
82
+ // Arrange & Act
83
+ render(
84
+ <Field.Root id="email">
85
+ <Field.Label>Email</Field.Label>
86
+ <input data-testid="input" />
87
+ <Field.Description>Hint.</Field.Description>
88
+ </Field.Root>,
89
+ );
90
+ const wrapper = screen.getByText("Email").parentElement;
91
+
92
+ // Assert
93
+ expect(wrapper).toContainElement(screen.getByTestId("input"));
94
+ expect(wrapper).toContainElement(screen.getByText("Hint."));
95
+ });
96
+
97
+ it("passes className and data-* through on the Root", () => {
98
+ // Arrange & Act
99
+ render(<Field.Root className="form-field" data-testid="field" />);
100
+
101
+ // Assert
102
+ expect(screen.getByTestId("field")).toHaveClass("form-field");
103
+ });
104
+ });