@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,15 @@
1
+ import { createContext, RefObject } from "react";
2
+
3
+ /**
4
+ * Content-scoped context provided by {@link DropdownContent} and
5
+ * {@link DropdownSubContent}. Holds a ref to a single "currently open
6
+ * direct-child sub" close callback so that sibling items can dismiss the
7
+ * open sub on hover, mirroring the keyboard contract (focus returning to
8
+ * the parent menu closes the sub).
9
+ */
10
+ export type DropdownContentContextValue = {
11
+ closeOpenSubRef: RefObject<(() => void) | null>;
12
+ };
13
+
14
+ export const DropdownContentContext =
15
+ createContext<DropdownContentContextValue | null>(null);
@@ -0,0 +1,17 @@
1
+ import { RefObject } from "react";
2
+
3
+ import { Direction } from "../DirectionProvider";
4
+ import { createStrictContext } from "../utils";
5
+
6
+ export type DropdownContextValue = {
7
+ open: boolean;
8
+ setOpen: (open: boolean) => void;
9
+ contentId: string;
10
+ triggerRef: RefObject<HTMLButtonElement | null>;
11
+ dir: Direction;
12
+ };
13
+
14
+ export const [DropdownContext, useDropdownContext] =
15
+ createStrictContext<DropdownContextValue>(
16
+ "Dropdown sub-components must be rendered inside a <Dropdown.Root>.",
17
+ );
@@ -0,0 +1,8 @@
1
+ import { createContext } from "react";
2
+
3
+ export type DropdownGroupContextValue = {
4
+ labelId: string;
5
+ };
6
+
7
+ export const DropdownGroupContext =
8
+ createContext<DropdownGroupContextValue | null>(null);
@@ -0,0 +1,13 @@
1
+ import { createContext } from "react";
2
+
3
+ /**
4
+ * Provided by {@link DropdownCheckboxItem} and {@link DropdownRadioItem}.
5
+ * {@link DropdownItemIndicator} reads it to decide whether to render and
6
+ * what `data-state` to expose.
7
+ */
8
+ export type DropdownItemIndicatorContextValue = {
9
+ checked: boolean | "indeterminate";
10
+ };
11
+
12
+ export const DropdownItemIndicatorContext =
13
+ createContext<DropdownItemIndicatorContextValue | null>(null);
@@ -0,0 +1,9 @@
1
+ import { createContext } from "react";
2
+
3
+ export type DropdownRadioGroupContextValue = {
4
+ value: string | undefined;
5
+ select: (value: string) => void;
6
+ };
7
+
8
+ export const DropdownRadioGroupContext =
9
+ createContext<DropdownRadioGroupContextValue | null>(null);
@@ -0,0 +1,15 @@
1
+ import { RefObject } from "react";
2
+
3
+ import { createStrictContext } from "../utils";
4
+
5
+ export type DropdownSubContextValue = {
6
+ open: boolean;
7
+ setOpen: (open: boolean) => void;
8
+ contentId: string;
9
+ triggerRef: RefObject<HTMLLIElement | null>;
10
+ };
11
+
12
+ export const [DropdownSubContext, useDropdownSubContext] =
13
+ createStrictContext<DropdownSubContextValue>(
14
+ "Dropdown.SubTrigger and Dropdown.SubContent must be rendered inside a <Dropdown.Sub>.",
15
+ );
@@ -0,0 +1,284 @@
1
+ # Dropdown
2
+
3
+ A compound component implementing the
4
+ [WAI-ARIA Menu Button / Menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/)
5
+ on top of the native HTML
6
+ [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API).
7
+ No portal, no floating-ui — the browser handles layering and light-dismiss.
8
+
9
+ ```tsx
10
+ import { Dropdown } from "@primitiv-ui/react";
11
+
12
+ <Dropdown.Root>
13
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
14
+ <Dropdown.Content>
15
+ <Dropdown.Item onSelect={() => rename()}>Rename</Dropdown.Item>
16
+ <Dropdown.Item onSelect={() => duplicate()}>Duplicate</Dropdown.Item>
17
+ <Dropdown.Separator />
18
+ <Dropdown.Item disabled>Archive</Dropdown.Item>
19
+ </Dropdown.Content>
20
+ </Dropdown.Root>;
21
+ ```
22
+
23
+ ## Sub-components
24
+
25
+ | Export | Role | Notes |
26
+ | ------------------------ | -------------------- | ------------------------------------------------------------------------------------ |
27
+ | `Dropdown.Root` | State owner | Uncontrolled (`defaultOpen`) or controlled (`open` + `onOpenChange`) |
28
+ | `Dropdown.Trigger` | `aria-haspopup=menu` | Toggles the menu; supports `asChild` |
29
+ | `Dropdown.Content` | `menu` | Native `popover="auto"`; handles arrow keys, typeahead, Escape |
30
+ | `Dropdown.Item` | `menuitem` | Activatable row with `onSelect` escape hatch |
31
+ | `Dropdown.CheckboxItem` | `menuitemcheckbox` | Tri-state toggle (`true` / `false` / `"indeterminate"`) |
32
+ | `Dropdown.RadioGroup` | `group` | Single-selection container for `RadioItem`s |
33
+ | `Dropdown.RadioItem` | `menuitemradio` | Must live inside a `RadioGroup` |
34
+ | `Dropdown.ItemIndicator` | — | Icon slot inside a `CheckboxItem` / `RadioItem`; exposes `data-state` + `forceMount` |
35
+ | `Dropdown.Label` | — | Non-interactive label; auto-wired to the enclosing `Group` via `aria-labelledby` |
36
+ | `Dropdown.Group` | `group` | Semantic grouping for related items |
37
+ | `Dropdown.Separator` | `separator` | Visual divider; skipped by focus and typeahead |
38
+ | `Dropdown.Sub` | State owner | Submenu boundary; same state modes as `Root` |
39
+ | `Dropdown.SubTrigger` | `menuitem` | Opens the submenu on click or the inline-forward arrow (`ArrowRight` LTR / `ArrowLeft` RTL) |
40
+ | `Dropdown.SubContent` | `menu` | Submenu panel; the inline-backward arrow (`ArrowLeft` LTR / `ArrowRight` RTL) closes it and returns focus to the trigger |
41
+
42
+ All sub-components that render an element accept `asChild` to compose
43
+ their ARIA and behaviour onto a caller-supplied child.
44
+
45
+ ## Keyboard interaction
46
+
47
+ | Key | Behaviour |
48
+ | ----------------------- | ------------------------------------------------- |
49
+ | `ArrowDown` / `ArrowUp` | Move focus to next / previous item (wraps) |
50
+ | `Home` / `End` | Jump to first / last enabled item |
51
+ | `Enter` / `Space` | Activate the focused item |
52
+ | `Escape` | Close the menu and return focus to the trigger |
53
+ | any printable character | Typeahead — focuses the next item matching prefix |
54
+ | `ArrowRight` | Open a focused submenu (`SubTrigger`, LTR) / close the submenu (inside `SubContent`, RTL) |
55
+ | `ArrowLeft` | Close the current submenu (inside `SubContent`, LTR) / open a focused submenu (`SubTrigger`, RTL) |
56
+
57
+ Typeahead accumulates keystrokes within a 500 ms window; pressing the
58
+ same character repeatedly cycles through items sharing that first letter.
59
+ Disabled items are skipped by arrow navigation, typeahead, and activation.
60
+
61
+ ## Reading direction
62
+
63
+ Pass `dir="ltr"` or `dir="rtl"` on `Dropdown.Root` to invert the submenu
64
+ open / close arrow keys. When omitted, the component reads the inherited
65
+ `DirectionProvider` value, falling back to `"ltr"`:
66
+
67
+ ```tsx
68
+ <DirectionProvider dir="rtl">
69
+ <Dropdown.Root>{/* submenus now open on ArrowLeft */}</Dropdown.Root>
70
+ </DirectionProvider>
71
+ ```
72
+
73
+ An explicit `dir` prop on `Dropdown.Root` always wins over the inherited
74
+ value. The reading direction only affects keyboard handling — submenu
75
+ visual placement is the consumer's CSS concern (use logical properties
76
+ or `position-try-fallbacks` with `flip-inline`).
77
+
78
+ ## State modes
79
+
80
+ - **Uncontrolled** — pass `defaultOpen` (or omit to start closed). Optional
81
+ `onOpenChange` observes user-driven transitions.
82
+ - **Controlled** — pass `open` and `onOpenChange` together.
83
+
84
+ `Dropdown.Sub` follows the same contract for its own open state.
85
+
86
+ `Dropdown.CheckboxItem` and `Dropdown.RadioGroup` each expose the same
87
+ uncontrolled / controlled split for their `checked` / `value` state.
88
+
89
+ ## The `onSelect` escape hatch
90
+
91
+ Every activatable item (`Item`, `CheckboxItem`, `RadioItem`) fires
92
+ `onSelect` with a cancellable `Event` on activation. By default the menu
93
+ auto-closes; call `event.preventDefault()` to keep it open — useful for
94
+ rapidly toggling multiple checkboxes, or running an action whose outcome
95
+ is shown inline in the menu.
96
+
97
+ ```tsx
98
+ <Dropdown.CheckboxItem
99
+ onSelect={(event) => event.preventDefault()}
100
+ onCheckedChange={setGridVisible}
101
+ >
102
+ Show grid
103
+ </Dropdown.CheckboxItem>
104
+ ```
105
+
106
+ ## Disabled items
107
+
108
+ Disabled items receive `aria-disabled="true"` rather than the native
109
+ `disabled` attribute, so they remain visible to assistive technology but
110
+ no-op on activation. Arrow navigation and typeahead skip them.
111
+
112
+ ```tsx
113
+ <Dropdown.Item disabled>Archive (coming soon)</Dropdown.Item>
114
+ ```
115
+
116
+ A disabled `SubTrigger` refuses to open on both click and the
117
+ inline-forward arrow key.
118
+
119
+ ## Checkbox and radio items
120
+
121
+ ```tsx
122
+ <Dropdown.Content>
123
+ <Dropdown.Label>View</Dropdown.Label>
124
+ <Dropdown.CheckboxItem defaultChecked>Show grid</Dropdown.CheckboxItem>
125
+ <Dropdown.CheckboxItem>Show ruler</Dropdown.CheckboxItem>
126
+ <Dropdown.Separator />
127
+ <Dropdown.RadioGroup defaultValue="system">
128
+ <Dropdown.RadioItem value="light">Light</Dropdown.RadioItem>
129
+ <Dropdown.RadioItem value="dark">Dark</Dropdown.RadioItem>
130
+ <Dropdown.RadioItem value="system">Match system</Dropdown.RadioItem>
131
+ </Dropdown.RadioGroup>
132
+ </Dropdown.Content>
133
+ ```
134
+
135
+ `CheckboxItem` supports a tri-state: `true`, `false`, or `"indeterminate"`
136
+ (which renders as `aria-checked="mixed"`). An indeterminate item resolves
137
+ to `true` on the next activation, matching the native
138
+ `<input type="checkbox">` contract.
139
+
140
+ ### `ItemIndicator`
141
+
142
+ Render the visible mark (usually a checkmark or a bullet) inside the
143
+ item via `Dropdown.ItemIndicator`. It defaults to a `<span>`, supports
144
+ `asChild` so consumers can compose onto an SVG icon, and exposes
145
+ `data-state` for styling:
146
+
147
+ ```tsx
148
+ <Dropdown.CheckboxItem
149
+ checked={showBookmarks}
150
+ onCheckedChange={setShowBookmarks}
151
+ >
152
+ <Dropdown.ItemIndicator>
153
+ <CheckIcon />
154
+ </Dropdown.ItemIndicator>
155
+ Show bookmarks
156
+ </Dropdown.CheckboxItem>
157
+ ```
158
+
159
+ | `data-state` | When |
160
+ | ----------------- | ----------------------------------------------------------------------------------- |
161
+ | `"checked"` | Parent `CheckboxItem` is `true`, or parent `RadioItem` is the group's current value |
162
+ | `"unchecked"` | Parent is `false` (only reachable when `forceMount` is set — see below) |
163
+ | `"indeterminate"` | Parent `CheckboxItem` is `"indeterminate"` |
164
+
165
+ By default the indicator **unmounts** when its parent is unchecked. Pass
166
+ `forceMount` to keep the DOM node in both states so CSS transitions or a
167
+ React animation library can drive the visual state off `data-state`:
168
+
169
+ ```tsx
170
+ <Dropdown.ItemIndicator forceMount>
171
+ <CheckIcon className="indicator" /> {/* fade in/out via data-state */}
172
+ </Dropdown.ItemIndicator>
173
+ ```
174
+
175
+ Rendering `Dropdown.ItemIndicator` outside a `CheckboxItem` or `RadioItem`
176
+ throws a descriptive error.
177
+
178
+ ## Submenus
179
+
180
+ ```tsx
181
+ <Dropdown.Content>
182
+ <Dropdown.Item>New</Dropdown.Item>
183
+ <Dropdown.Sub>
184
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
185
+ <Dropdown.SubContent>
186
+ <Dropdown.Item>Project A</Dropdown.Item>
187
+ <Dropdown.Item>Project B</Dropdown.Item>
188
+ </Dropdown.SubContent>
189
+ </Dropdown.Sub>
190
+ </Dropdown.Content>
191
+ ```
192
+
193
+ Open a submenu with the inline-forward arrow key (`ArrowRight` in LTR,
194
+ `ArrowLeft` in RTL) or a click on the trigger; close it with the
195
+ inline-backward arrow or by selecting an item. Focus returns to the
196
+ `SubTrigger` when the submenu closes.
197
+
198
+ Hovering the `SubTrigger` opens the submenu automatically; hovering onto
199
+ a sibling item in the parent menu closes it, mirroring the keyboard
200
+ contract in which focus returning to the parent menu dismisses the sub.
201
+
202
+ Arrow-key navigation is scoped to the popover that currently holds
203
+ focus. Items inside a `SubContent` aren't pulled into the parent's
204
+ navigation cycle, so `ArrowDown` from a `SubTrigger` lands on the next
205
+ sibling item in the parent menu — not on the first item inside a closed
206
+ submenu, which is unfocusable. While focus lives inside an open
207
+ `SubContent`, `ArrowDown` and `ArrowUp` wrap within that sub's items.
208
+
209
+ ## Groups and labels
210
+
211
+ ```tsx
212
+ <Dropdown.Content>
213
+ <Dropdown.Group>
214
+ <Dropdown.Label>File</Dropdown.Label>
215
+ <Dropdown.Item>New</Dropdown.Item>
216
+ <Dropdown.Item>Open…</Dropdown.Item>
217
+ </Dropdown.Group>
218
+ <Dropdown.Separator />
219
+ <Dropdown.Group>
220
+ <Dropdown.Label>Edit</Dropdown.Label>
221
+ <Dropdown.Item>Undo</Dropdown.Item>
222
+ </Dropdown.Group>
223
+ </Dropdown.Content>
224
+ ```
225
+
226
+ Nesting a `Dropdown.Label` inside a `Dropdown.Group` wires the group's
227
+ `aria-labelledby` to the label automatically — no manual `id` threading
228
+ is needed.
229
+
230
+ ## `asChild` composition
231
+
232
+ Every rendering sub-component accepts `asChild` to compose its
233
+ ARIA attributes, event handlers, and ref onto a caller-supplied child.
234
+ All ARIA attributes and handlers merge onto the child following the
235
+ [Slot](../Slot.tsx) composition rules (child handler runs first, then
236
+ the component's):
237
+
238
+ ```tsx
239
+ <Dropdown.Trigger asChild>
240
+ <MyStyledButton>Options</MyStyledButton>
241
+ </Dropdown.Trigger>
242
+
243
+ <Dropdown.Item asChild>
244
+ <a href="/rename">Rename</a>
245
+ </Dropdown.Item>
246
+ ```
247
+
248
+ ## Styling hooks
249
+
250
+ ```css
251
+ /* Open state on the menu panel */
252
+ [role="menu"][data-popover-open] {
253
+ animation: fade-in 120ms ease-out;
254
+ }
255
+
256
+ /* Highlighted item — pointer focus */
257
+ [role="menuitem"][data-highlighted],
258
+ [role="menuitemcheckbox"][data-highlighted],
259
+ [role="menuitemradio"][data-highlighted] {
260
+ background: rgba(0 0 0 / 0.06);
261
+ outline: none;
262
+ }
263
+
264
+ /* Disabled items */
265
+ [aria-disabled="true"] {
266
+ opacity: 0.5;
267
+ pointer-events: none;
268
+ }
269
+
270
+ /* Checked state for checkbox / radio items */
271
+ [role="menuitemcheckbox"][aria-checked="true"]::before,
272
+ [role="menuitemradio"][aria-checked="true"]::before {
273
+ content: "✓";
274
+ }
275
+ ```
276
+
277
+ The native popover API adds `data-popover-open` on the element while the
278
+ popover is showing; combine it with the standard ARIA attributes for
279
+ state-driven styling.
280
+
281
+ `data-highlighted` is present on `Item`, `CheckboxItem`, `RadioItem`, and
282
+ `SubTrigger` while the item has pointer focus (mouseenter). On `SubTrigger`
283
+ it also remains present for the duration its sub-menu is open, so the
284
+ active path stays highlighted as the user navigates nested levels.
@@ -0,0 +1,286 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Dropdown } from "../Dropdown";
5
+
6
+ describe("Dropdown asChild", () => {
7
+ it("delegates Trigger to the child element via asChild, preserving ARIA wiring", async () => {
8
+ // Arrange
9
+ const user = userEvent.setup();
10
+ render(
11
+ <Dropdown.Root>
12
+ <Dropdown.Trigger asChild>
13
+ <a href="#menu" data-testid="custom-trigger">
14
+ Options
15
+ </a>
16
+ </Dropdown.Trigger>
17
+ <Dropdown.Content>
18
+ <Dropdown.Item>Rename</Dropdown.Item>
19
+ </Dropdown.Content>
20
+ </Dropdown.Root>,
21
+ );
22
+
23
+ // Assert — Trigger renders as the <a>, not a <button>
24
+ const trigger = screen.getByTestId("custom-trigger");
25
+ expect(trigger.tagName).toBe("A");
26
+ expect(trigger).toHaveAttribute("aria-haspopup", "menu");
27
+ expect(trigger).toHaveAttribute("aria-expanded", "false");
28
+
29
+ // Act
30
+ await user.click(trigger);
31
+
32
+ // Assert
33
+ expect(trigger).toHaveAttribute("aria-expanded", "true");
34
+ });
35
+
36
+ it("delegates Content to the child element via asChild", () => {
37
+ // Arrange & Act
38
+ render(
39
+ <Dropdown.Root defaultOpen>
40
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
41
+ <Dropdown.Content asChild>
42
+ <div data-testid="custom-content">
43
+ <Dropdown.Item>Rename</Dropdown.Item>
44
+ </div>
45
+ </Dropdown.Content>
46
+ </Dropdown.Root>,
47
+ );
48
+
49
+ // Assert
50
+ const content = screen.getByTestId("custom-content");
51
+ expect(content.tagName).toBe("DIV");
52
+ expect(content).toHaveAttribute("role", "menu");
53
+ expect(content).toHaveAttribute("popover", "auto");
54
+ });
55
+
56
+ it("delegates Item to the child element via asChild and still auto-closes on click", async () => {
57
+ // Arrange
58
+ const user = userEvent.setup();
59
+ const onSelect = vi.fn();
60
+ render(
61
+ <Dropdown.Root defaultOpen>
62
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
63
+ <Dropdown.Content>
64
+ <Dropdown.Item asChild onSelect={onSelect}>
65
+ <a href="#rename" data-testid="custom-item">
66
+ Rename
67
+ </a>
68
+ </Dropdown.Item>
69
+ </Dropdown.Content>
70
+ </Dropdown.Root>,
71
+ );
72
+ const item = screen.getByTestId("custom-item");
73
+ expect(item.tagName).toBe("A");
74
+ expect(item).toHaveAttribute("role", "menuitem");
75
+ const menu = screen.getByRole("menu", { hidden: true });
76
+ expect(menu).toHaveAttribute("data-popover-open");
77
+
78
+ // Act
79
+ await user.click(item);
80
+
81
+ // Assert
82
+ expect(onSelect).toHaveBeenCalledTimes(1);
83
+ expect(menu).not.toHaveAttribute("data-popover-open");
84
+ });
85
+
86
+ it("delegates Separator to the child element via asChild", () => {
87
+ // Arrange & Act
88
+ render(
89
+ <Dropdown.Root defaultOpen>
90
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
91
+ <Dropdown.Content>
92
+ <Dropdown.Item>Rename</Dropdown.Item>
93
+ <Dropdown.Separator asChild>
94
+ <hr data-testid="custom-sep" />
95
+ </Dropdown.Separator>
96
+ <Dropdown.Item>Delete</Dropdown.Item>
97
+ </Dropdown.Content>
98
+ </Dropdown.Root>,
99
+ );
100
+
101
+ // Assert
102
+ const sep = screen.getByTestId("custom-sep");
103
+ expect(sep.tagName).toBe("HR");
104
+ expect(sep).toHaveAttribute("role", "separator");
105
+ });
106
+
107
+ it("delegates Group to the child element via asChild while still labelling it", () => {
108
+ // Arrange & Act
109
+ render(
110
+ <Dropdown.Root defaultOpen>
111
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
112
+ <Dropdown.Content>
113
+ <Dropdown.Group asChild>
114
+ <section data-testid="custom-group">
115
+ <Dropdown.Label>Actions</Dropdown.Label>
116
+ <Dropdown.Item>Rename</Dropdown.Item>
117
+ </section>
118
+ </Dropdown.Group>
119
+ </Dropdown.Content>
120
+ </Dropdown.Root>,
121
+ );
122
+
123
+ // Assert
124
+ const group = screen.getByTestId("custom-group");
125
+ expect(group.tagName).toBe("SECTION");
126
+ expect(group).toHaveAttribute("role", "group");
127
+ const labelId = group.getAttribute("aria-labelledby");
128
+ expect(labelId).toBeTruthy();
129
+ const label = screen.getByText("Actions");
130
+ expect(label).toHaveAttribute("id", labelId);
131
+ });
132
+
133
+ it("delegates Label to the child element via asChild", () => {
134
+ // Arrange & Act
135
+ render(
136
+ <Dropdown.Root defaultOpen>
137
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
138
+ <Dropdown.Content>
139
+ <Dropdown.Group>
140
+ <Dropdown.Label asChild>
141
+ <h2 data-testid="custom-label">Actions</h2>
142
+ </Dropdown.Label>
143
+ <Dropdown.Item>Rename</Dropdown.Item>
144
+ </Dropdown.Group>
145
+ </Dropdown.Content>
146
+ </Dropdown.Root>,
147
+ );
148
+
149
+ // Assert
150
+ const label = screen.getByTestId("custom-label");
151
+ expect(label.tagName).toBe("H2");
152
+ expect(label).toHaveAttribute("id");
153
+ });
154
+
155
+ it("delegates CheckboxItem to the child element via asChild", async () => {
156
+ // Arrange
157
+ const user = userEvent.setup();
158
+ const onCheckedChange = vi.fn();
159
+ render(
160
+ <Dropdown.Root defaultOpen>
161
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
162
+ <Dropdown.Content>
163
+ <Dropdown.CheckboxItem asChild onCheckedChange={onCheckedChange}>
164
+ <a href="#show-hidden" data-testid="custom-check">
165
+ Show hidden
166
+ </a>
167
+ </Dropdown.CheckboxItem>
168
+ </Dropdown.Content>
169
+ </Dropdown.Root>,
170
+ );
171
+ const item = screen.getByTestId("custom-check");
172
+ expect(item.tagName).toBe("A");
173
+ expect(item).toHaveAttribute("role", "menuitemcheckbox");
174
+ expect(item).toHaveAttribute("aria-checked", "false");
175
+
176
+ // Act
177
+ await user.click(item);
178
+
179
+ // Assert
180
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
181
+ });
182
+
183
+ it("delegates RadioGroup to the child element via asChild", () => {
184
+ // Arrange & Act
185
+ render(
186
+ <Dropdown.Root defaultOpen>
187
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
188
+ <Dropdown.Content>
189
+ <Dropdown.RadioGroup asChild defaultValue="a">
190
+ <section data-testid="custom-radiogroup">
191
+ <Dropdown.RadioItem value="a">A</Dropdown.RadioItem>
192
+ <Dropdown.RadioItem value="b">B</Dropdown.RadioItem>
193
+ </section>
194
+ </Dropdown.RadioGroup>
195
+ </Dropdown.Content>
196
+ </Dropdown.Root>,
197
+ );
198
+
199
+ // Assert
200
+ const group = screen.getByTestId("custom-radiogroup");
201
+ expect(group.tagName).toBe("SECTION");
202
+ expect(group).toHaveAttribute("role", "group");
203
+ });
204
+
205
+ it("delegates RadioItem to the child element via asChild", async () => {
206
+ // Arrange
207
+ const user = userEvent.setup();
208
+ const onValueChange = vi.fn();
209
+ render(
210
+ <Dropdown.Root defaultOpen>
211
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
212
+ <Dropdown.Content>
213
+ <Dropdown.RadioGroup onValueChange={onValueChange}>
214
+ <Dropdown.RadioItem asChild value="a">
215
+ <a href="#a" data-testid="custom-radio">
216
+ A
217
+ </a>
218
+ </Dropdown.RadioItem>
219
+ </Dropdown.RadioGroup>
220
+ </Dropdown.Content>
221
+ </Dropdown.Root>,
222
+ );
223
+ const item = screen.getByTestId("custom-radio");
224
+ expect(item.tagName).toBe("A");
225
+ expect(item).toHaveAttribute("role", "menuitemradio");
226
+
227
+ // Act
228
+ await user.click(item);
229
+
230
+ // Assert
231
+ expect(onValueChange).toHaveBeenCalledWith("a");
232
+ });
233
+
234
+ it("delegates SubTrigger to the child element via asChild", () => {
235
+ // Arrange & Act
236
+ render(
237
+ <Dropdown.Root defaultOpen>
238
+ <Dropdown.Trigger>File</Dropdown.Trigger>
239
+ <Dropdown.Content>
240
+ <Dropdown.Sub>
241
+ <Dropdown.SubTrigger asChild>
242
+ <a href="#open-recent" data-testid="custom-subtrigger">
243
+ Open Recent
244
+ </a>
245
+ </Dropdown.SubTrigger>
246
+ <Dropdown.SubContent>
247
+ <Dropdown.Item>Project A</Dropdown.Item>
248
+ </Dropdown.SubContent>
249
+ </Dropdown.Sub>
250
+ </Dropdown.Content>
251
+ </Dropdown.Root>,
252
+ );
253
+
254
+ // Assert
255
+ const subTrigger = screen.getByTestId("custom-subtrigger");
256
+ expect(subTrigger.tagName).toBe("A");
257
+ expect(subTrigger).toHaveAttribute("role", "menuitem");
258
+ expect(subTrigger).toHaveAttribute("aria-haspopup", "menu");
259
+ expect(subTrigger).toHaveAttribute("aria-expanded", "false");
260
+ });
261
+
262
+ it("delegates SubContent to the child element via asChild", () => {
263
+ // Arrange & Act
264
+ render(
265
+ <Dropdown.Root defaultOpen>
266
+ <Dropdown.Trigger>File</Dropdown.Trigger>
267
+ <Dropdown.Content>
268
+ <Dropdown.Sub defaultOpen>
269
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
270
+ <Dropdown.SubContent asChild>
271
+ <div data-testid="custom-subcontent">
272
+ <Dropdown.Item>Project A</Dropdown.Item>
273
+ </div>
274
+ </Dropdown.SubContent>
275
+ </Dropdown.Sub>
276
+ </Dropdown.Content>
277
+ </Dropdown.Root>,
278
+ );
279
+
280
+ // Assert
281
+ const subContent = screen.getByTestId("custom-subcontent");
282
+ expect(subContent.tagName).toBe("DIV");
283
+ expect(subContent).toHaveAttribute("role", "menu");
284
+ expect(subContent).toHaveAttribute("popover", "auto");
285
+ });
286
+ });