@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,842 @@
1
+ import { useContext, useEffect, useId, useMemo, useRef, useState } from "react";
2
+
3
+ import { useCheckboxRoot } from "../Checkbox/hooks";
4
+ import { useDirection } from "../DirectionProvider";
5
+ import { useRadioGroupRoot } from "../RadioGroup/hooks";
6
+ import { composeEventHandlers, Slot } from "../Slot";
7
+
8
+ import { DropdownContext } from "./DropdownContext";
9
+ import { DropdownContentContext } from "./DropdownContentContext";
10
+ import { DropdownGroupContext } from "./DropdownGroupContext";
11
+ import { DropdownItemIndicatorContext } from "./DropdownItemIndicatorContext";
12
+ import { DropdownRadioGroupContext } from "./DropdownRadioGroupContext";
13
+ import { DropdownSubContext } from "./DropdownSubContext";
14
+ import {
15
+ useCloseSiblingSub,
16
+ useDropdownContent,
17
+ useDropdownContext,
18
+ useDropdownGroup,
19
+ useDropdownItem,
20
+ useDropdownLabel,
21
+ useDropdownRoot,
22
+ useDropdownSubContext,
23
+ useDropdownTrigger,
24
+ } from "./hooks";
25
+ import {
26
+ DropdownCheckboxItemProps,
27
+ DropdownContentProps,
28
+ DropdownGroupProps,
29
+ DropdownItemIndicatorProps,
30
+ DropdownItemProps,
31
+ DropdownLabelProps,
32
+ DropdownRadioGroupProps,
33
+ DropdownRadioItemProps,
34
+ DropdownRootProps,
35
+ DropdownSeparatorProps,
36
+ DropdownSubContentProps,
37
+ DropdownSubProps,
38
+ DropdownSubTriggerProps,
39
+ DropdownTriggerProps,
40
+ } from "./types";
41
+ import { MENUITEM_SELECTOR } from "./constants";
42
+
43
+ /**
44
+ * The root of a Dropdown menu — owns the open state and provides context to
45
+ * descendants. Renders no DOM of its own; it is a context boundary.
46
+ *
47
+ * Supports two state modes, statically discriminated at the type level so
48
+ * only one shape is accepted by TypeScript:
49
+ *
50
+ * - **Uncontrolled** — pass {@link DropdownRootProps.defaultOpen | `defaultOpen`}
51
+ * (or omit it to start closed). The component owns and updates the open
52
+ * state internally. Optional {@link DropdownRootProps.onOpenChange | `onOpenChange`}
53
+ * observes transitions.
54
+ * - **Controlled** — pass {@link DropdownRootProps.open | `open`} *and*
55
+ * {@link DropdownRootProps.onOpenChange | `onOpenChange`} together. The
56
+ * parent owns the state; the component defers every transition back through
57
+ * the callback.
58
+ *
59
+ * Only user-driven transitions (trigger clicks, Escape, selection) invoke
60
+ * `onOpenChange`. External state flips made by the parent do not.
61
+ *
62
+ * **Reading direction.** Pass {@link DropdownRootProps.dir | `dir`} to set
63
+ * `"ltr"` or `"rtl"`, which inverts the submenu open / close arrow keys
64
+ * (`ArrowRight` ↔ `ArrowLeft`). When omitted, the component reads the
65
+ * inherited {@link DirectionProvider} value, falling back to `"ltr"`.
66
+ *
67
+ * @example Uncontrolled
68
+ * ```tsx
69
+ * <Dropdown.Root>
70
+ * <Dropdown.Trigger>Options</Dropdown.Trigger>
71
+ * <Dropdown.Content>
72
+ * <Dropdown.Item>Rename</Dropdown.Item>
73
+ * <Dropdown.Item>Delete</Dropdown.Item>
74
+ * </Dropdown.Content>
75
+ * </Dropdown.Root>
76
+ * ```
77
+ *
78
+ * @example Controlled
79
+ * ```tsx
80
+ * const [open, setOpen] = useState(false);
81
+ *
82
+ * <Dropdown.Root open={open} onOpenChange={setOpen}>
83
+ * …
84
+ * </Dropdown.Root>
85
+ * ```
86
+ */
87
+ function DropdownRoot({
88
+ defaultOpen,
89
+ open,
90
+ onOpenChange,
91
+ dir,
92
+ children,
93
+ }: DropdownRootProps) {
94
+ const inheritedDir = useDirection();
95
+ const { contextValue } = useDropdownRoot({
96
+ defaultOpen,
97
+ open,
98
+ onOpenChange,
99
+ dir: dir ?? inheritedDir,
100
+ });
101
+
102
+ return (
103
+ <DropdownContext.Provider value={contextValue}>
104
+ {children}
105
+ </DropdownContext.Provider>
106
+ );
107
+ }
108
+
109
+ DropdownRoot.displayName = "DropdownRoot";
110
+
111
+ /**
112
+ * The menu button. Toggles the Dropdown open/closed on click and exposes
113
+ * the ARIA contract for WAI-ARIA Menu Button: `aria-haspopup="menu"`,
114
+ * `aria-expanded` reflecting the open state, and `aria-controls` pointing
115
+ * at the content's id.
116
+ *
117
+ * Renders a `<button type="button">` by default. Pass `asChild` to render
118
+ * the composed child element instead (e.g. a link, or a custom button
119
+ * component). All ARIA attributes and event handlers are merged onto the
120
+ * child following the {@link Slot} composition rules.
121
+ *
122
+ * @example With an anchor via `asChild`
123
+ * ```tsx
124
+ * <Dropdown.Trigger asChild>
125
+ * <a href="#options">Options</a>
126
+ * </Dropdown.Trigger>
127
+ * ```
128
+ */
129
+ function DropdownTrigger({
130
+ children,
131
+ onClick,
132
+ asChild = false,
133
+ ...rest
134
+ }: DropdownTriggerProps) {
135
+ const { triggerProps } = useDropdownTrigger({ onClick, restProps: rest });
136
+
137
+ if (asChild) {
138
+ return <Slot {...triggerProps}>{children}</Slot>;
139
+ }
140
+
141
+ return (
142
+ <button type="button" {...triggerProps}>
143
+ {children}
144
+ </button>
145
+ );
146
+ }
147
+
148
+ DropdownTrigger.displayName = "DropdownTrigger";
149
+
150
+ /**
151
+ * The menu panel rendered with the native HTML
152
+ * [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
153
+ * (`popover="auto"`) — no portal, no floating-ui. The browser manages
154
+ * layering via the top layer and dispatches a light-dismiss on outside
155
+ * click and Escape in the light-dismiss flow.
156
+ *
157
+ * Renders a `<menu role="menu">` by default; pass `asChild` to render
158
+ * any element with menu semantics.
159
+ *
160
+ * **Keyboard interaction.** While the menu is open:
161
+ *
162
+ * | Key | Behaviour |
163
+ * | ----------------------- | -------------------------------------------------- |
164
+ * | `ArrowDown` / `ArrowUp` | Move focus to next / previous item (wraps) |
165
+ * | `Home` / `End` | Jump to first / last item |
166
+ * | `Enter` / `Space` | Activate the focused item |
167
+ * | `Escape` | Close the menu and return focus to the trigger |
168
+ * | any printable character | Typeahead — focuses the next item matching prefix |
169
+ *
170
+ * Typeahead accumulates keystrokes within a 500 ms window; pressing the
171
+ * same character repeatedly cycles through items that share that first
172
+ * letter. Disabled items are skipped during arrow navigation and typeahead.
173
+ *
174
+ * Arrow navigation is scoped to the popover that currently holds focus —
175
+ * a {@link DropdownSubContent | `Dropdown.SubContent`} rendered inside
176
+ * this menu does not pull its items into the parent's navigation cycle,
177
+ * so `ArrowDown` past a {@link DropdownSubTrigger | `Dropdown.SubTrigger`}
178
+ * lands on the next sibling item in the parent rather than getting
179
+ * stuck on a non-focusable item inside a closed sub-popover.
180
+ */
181
+ function DropdownContent({
182
+ children,
183
+ onKeyDown,
184
+ asChild = false,
185
+ ...rest
186
+ }: DropdownContentProps) {
187
+ const { contentContextValue, contentProps } = useDropdownContent({
188
+ onKeyDown,
189
+ restProps: rest,
190
+ });
191
+
192
+ return (
193
+ <DropdownContentContext.Provider value={contentContextValue}>
194
+ {asChild ? (
195
+ <Slot {...contentProps}>{children}</Slot>
196
+ ) : (
197
+ <menu {...contentProps}>{children}</menu>
198
+ )}
199
+ </DropdownContentContext.Provider>
200
+ );
201
+ }
202
+
203
+ DropdownContent.displayName = "DropdownContent";
204
+
205
+ /**
206
+ * A standard menu item. Renders a `<li role="menuitem">` by default; pass
207
+ * `asChild` to render any element with menuitem semantics.
208
+ *
209
+ * Clicking the item (or pressing Enter / Space while focused) fires
210
+ * {@link DropdownItemProps.onSelect | `onSelect`} with a cancellable
211
+ * `Event`. The menu auto-closes after selection; call
212
+ * `event.preventDefault()` inside `onSelect` to keep it open — useful
213
+ * for actions that perform in-place mutations (e.g. copying to clipboard).
214
+ *
215
+ * Disabled items receive `aria-disabled="true"` and are skipped by arrow
216
+ * navigation, typeahead, and activation handlers.
217
+ */
218
+ function DropdownItem({
219
+ children,
220
+ onClick,
221
+ onSelect,
222
+ disabled,
223
+ asChild = false,
224
+ ...rest
225
+ }: DropdownItemProps) {
226
+ const { itemProps } = useDropdownItem({
227
+ disabled,
228
+ onClick,
229
+ onSelect,
230
+ restProps: rest,
231
+ });
232
+
233
+ if (asChild) {
234
+ return <Slot {...itemProps}>{children}</Slot>;
235
+ }
236
+
237
+ return <li {...itemProps}>{children}</li>;
238
+ }
239
+
240
+ DropdownItem.displayName = "DropdownItem";
241
+
242
+ /**
243
+ * A visual separator between groups of items. Renders a `<li role="separator">`
244
+ * by default; pass `asChild` to render any element with separator semantics.
245
+ * Non-interactive — skipped by focus, arrow navigation, and typeahead.
246
+ */
247
+ function DropdownSeparator({
248
+ asChild = false,
249
+ children,
250
+ ...rest
251
+ }: DropdownSeparatorProps) {
252
+ const separatorProps = { ...rest, role: "separator" as const };
253
+
254
+ if (asChild) {
255
+ return <Slot {...separatorProps}>{children}</Slot>;
256
+ }
257
+
258
+ return <li {...separatorProps} />;
259
+ }
260
+
261
+ DropdownSeparator.displayName = "DropdownSeparator";
262
+
263
+ /**
264
+ * A semantic grouping of related items. Renders as a `<li role="group">`
265
+ * wrapping an inner `<ul role="none">`, or — with `asChild` — a single
266
+ * grouping element composed onto the provided child.
267
+ *
268
+ * Generates a stable id for its accompanying {@link DropdownLabel | `Dropdown.Label`},
269
+ * wired automatically via `aria-labelledby`. Nest a `Dropdown.Label` as the
270
+ * first child to provide the accessible name; screen readers will announce
271
+ * the group when arrowing into it.
272
+ */
273
+ function DropdownGroup({
274
+ children,
275
+ asChild = false,
276
+ ...rest
277
+ }: DropdownGroupProps) {
278
+ const { contextValue, groupProps } = useDropdownGroup({ restProps: rest });
279
+
280
+ return (
281
+ <DropdownGroupContext.Provider value={contextValue}>
282
+ {asChild ? (
283
+ <Slot {...groupProps}>{children}</Slot>
284
+ ) : (
285
+ <li {...groupProps}>
286
+ <ul role="none">{children}</ul>
287
+ </li>
288
+ )}
289
+ </DropdownGroupContext.Provider>
290
+ );
291
+ }
292
+
293
+ DropdownGroup.displayName = "DropdownGroup";
294
+
295
+ /**
296
+ * A non-interactive label, typically used inside a {@link DropdownGroup |
297
+ * `Dropdown.Group`} to give that group an accessible name. When nested in
298
+ * a group, the label's `id` is auto-wired to the group's `aria-labelledby`
299
+ * — consumers don't need to thread ids manually.
300
+ *
301
+ * Renders a `<li>` by default; pass `asChild` to render any element. A
302
+ * caller-supplied `id` takes precedence over the auto-generated one.
303
+ */
304
+ function DropdownLabel({
305
+ id,
306
+ children,
307
+ asChild = false,
308
+ ...rest
309
+ }: DropdownLabelProps) {
310
+ const { labelProps } = useDropdownLabel({ id, restProps: rest });
311
+
312
+ if (asChild) {
313
+ return <Slot {...labelProps}>{children}</Slot>;
314
+ }
315
+ return <li {...labelProps}>{children}</li>;
316
+ }
317
+
318
+ DropdownLabel.displayName = "DropdownLabel";
319
+
320
+ /**
321
+ * A toggleable menu item. Renders a `<li role="menuitemcheckbox">` with
322
+ * `aria-checked` reflecting the current state. `asChild` is supported.
323
+ *
324
+ * Supports a WAI-ARIA tri-state: `true`, `false`, or `"indeterminate"`
325
+ * (encoded as `aria-checked="mixed"`). An indeterminate item resolves to
326
+ * `true` on the next activation, matching the native `<input type="checkbox">`
327
+ * behaviour.
328
+ *
329
+ * State modes are discriminated at the type level:
330
+ *
331
+ * - **Uncontrolled** — pass `defaultChecked` (or omit to start unchecked).
332
+ * Optional `onCheckedChange` observes toggles.
333
+ * - **Controlled** — pass `checked` *and* `onCheckedChange` together.
334
+ *
335
+ * Activation (click / Enter / Space) toggles the checked state, then
336
+ * fires {@link DropdownCheckboxItemProps.onSelect | `onSelect`} with a
337
+ * cancellable `Event`. Call `event.preventDefault()` to keep the menu
338
+ * open — useful for rapidly toggling multiple checkboxes.
339
+ *
340
+ * Nest a {@link DropdownItemIndicator | `Dropdown.ItemIndicator`} to
341
+ * render the visible check mark; it reads its parent's state from
342
+ * context and exposes `data-state` for styling.
343
+ *
344
+ * Disabled items receive `aria-disabled="true"` and no-op on activation.
345
+ */
346
+ function DropdownCheckboxItem({
347
+ children,
348
+ onClick,
349
+ onSelect,
350
+ disabled,
351
+ defaultChecked,
352
+ checked: controlledChecked,
353
+ onCheckedChange,
354
+ asChild = false,
355
+ ...rest
356
+ }: DropdownCheckboxItemProps) {
357
+ const { setOpen, triggerRef } = useDropdownContext();
358
+ const closeSiblingSub = useCloseSiblingSub();
359
+ const [highlighted, setHighlighted] = useState(false);
360
+ const { checked, toggle } = useCheckboxRoot({
361
+ defaultChecked,
362
+ checked: controlledChecked,
363
+ onCheckedChange,
364
+ });
365
+ const ariaChecked: "mixed" | "true" | "false" =
366
+ checked === "indeterminate" ? "mixed" : checked ? "true" : "false";
367
+ const handleClick = () => {
368
+ if (disabled) return;
369
+ toggle();
370
+ const event = new Event("dropdown.select", { cancelable: true });
371
+ onSelect?.(event);
372
+ if (!event.defaultPrevented) {
373
+ setOpen(false);
374
+ triggerRef.current?.focus();
375
+ }
376
+ };
377
+ const itemProps = {
378
+ ...rest,
379
+ role: "menuitemcheckbox" as const,
380
+ tabIndex: -1,
381
+ "aria-checked": ariaChecked,
382
+ "aria-disabled": disabled || undefined,
383
+ "data-highlighted": highlighted ? "" : undefined,
384
+ onClick: composeEventHandlers(onClick, handleClick),
385
+ onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
386
+ setHighlighted(true);
387
+ closeSiblingSub();
388
+ }),
389
+ onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
390
+ setHighlighted(false),
391
+ ),
392
+ };
393
+ const indicatorContextValue = useMemo(() => ({ checked }), [checked]);
394
+ const content = asChild ? (
395
+ <Slot {...itemProps}>{children}</Slot>
396
+ ) : (
397
+ <li {...itemProps}>{children}</li>
398
+ );
399
+ return (
400
+ <DropdownItemIndicatorContext.Provider value={indicatorContextValue}>
401
+ {content}
402
+ </DropdownItemIndicatorContext.Provider>
403
+ );
404
+ }
405
+
406
+ DropdownCheckboxItem.displayName = "DropdownCheckboxItem";
407
+
408
+ /**
409
+ * A single-selection group of menu items. Children must be
410
+ * {@link DropdownRadioItem | `Dropdown.RadioItem`} elements. Renders a
411
+ * `<li role="group">` wrapping `<ul role="none">`, or — with `asChild` —
412
+ * composes onto the provided child.
413
+ *
414
+ * State modes are discriminated at the type level:
415
+ *
416
+ * - **Uncontrolled** — pass `defaultValue` (or omit for no initial selection).
417
+ * Optional `onValueChange` observes selections.
418
+ * - **Controlled** — pass `value` *and* `onValueChange` together.
419
+ */
420
+ function DropdownRadioGroup({
421
+ defaultValue,
422
+ value: controlledValue,
423
+ onValueChange,
424
+ children,
425
+ asChild = false,
426
+ ...rest
427
+ }: DropdownRadioGroupProps) {
428
+ const { value, select } = useRadioGroupRoot({
429
+ defaultValue,
430
+ value: controlledValue,
431
+ onValueChange,
432
+ });
433
+ const contextValue = useMemo(() => ({ value, select }), [value, select]);
434
+ const groupProps = { ...rest, role: "group" as const };
435
+ return (
436
+ <DropdownRadioGroupContext.Provider value={contextValue}>
437
+ {asChild ? (
438
+ <Slot {...groupProps}>{children}</Slot>
439
+ ) : (
440
+ <li {...groupProps}>
441
+ <ul role="none">{children}</ul>
442
+ </li>
443
+ )}
444
+ </DropdownRadioGroupContext.Provider>
445
+ );
446
+ }
447
+
448
+ DropdownRadioGroup.displayName = "DropdownRadioGroup";
449
+
450
+ /**
451
+ * A single radio choice. Must be rendered inside a
452
+ * {@link DropdownRadioGroup | `Dropdown.RadioGroup`}; rendering it outside
453
+ * one throws a descriptive error.
454
+ *
455
+ * Renders a `<li role="menuitemradio">` with `aria-checked` reflecting
456
+ * whether this item's `value` matches the group's active value. `asChild`
457
+ * is supported.
458
+ *
459
+ * Activation (click / Enter / Space) selects this item, updating the
460
+ * group's value, then fires {@link DropdownRadioItemProps.onSelect |
461
+ * `onSelect`} with a cancellable `Event`. Call `event.preventDefault()`
462
+ * to keep the menu open.
463
+ *
464
+ * Nest a {@link DropdownItemIndicator | `Dropdown.ItemIndicator`} to
465
+ * render the visible bullet; it reads its parent's state from context
466
+ * and exposes `data-state` for styling.
467
+ *
468
+ * Disabled items receive `aria-disabled="true"` and no-op on activation.
469
+ */
470
+ function DropdownRadioItem({
471
+ children,
472
+ onClick,
473
+ onSelect,
474
+ disabled,
475
+ value: itemValue,
476
+ asChild = false,
477
+ ...rest
478
+ }: DropdownRadioItemProps) {
479
+ const { setOpen, triggerRef } = useDropdownContext();
480
+ const closeSiblingSub = useCloseSiblingSub();
481
+ const [highlighted, setHighlighted] = useState(false);
482
+ const group = useContext(DropdownRadioGroupContext);
483
+ if (!group) {
484
+ throw new Error(
485
+ "Dropdown.RadioItem must be rendered inside a <Dropdown.RadioGroup>.",
486
+ );
487
+ }
488
+ const checked = group.value === itemValue;
489
+ const handleClick = () => {
490
+ if (disabled) return;
491
+ group.select(itemValue);
492
+ const event = new Event("dropdown.select", { cancelable: true });
493
+ onSelect?.(event);
494
+ if (!event.defaultPrevented) {
495
+ setOpen(false);
496
+ triggerRef.current?.focus();
497
+ }
498
+ };
499
+ const itemProps = {
500
+ ...rest,
501
+ role: "menuitemradio" as const,
502
+ tabIndex: -1,
503
+ "aria-checked": checked,
504
+ "aria-disabled": disabled || undefined,
505
+ "data-highlighted": highlighted ? "" : undefined,
506
+ onClick: composeEventHandlers(onClick, handleClick),
507
+ onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
508
+ setHighlighted(true);
509
+ closeSiblingSub();
510
+ }),
511
+ onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
512
+ setHighlighted(false),
513
+ ),
514
+ };
515
+ const indicatorContextValue = useMemo(() => ({ checked }), [checked]);
516
+ const content = asChild ? (
517
+ <Slot {...itemProps}>{children}</Slot>
518
+ ) : (
519
+ <li {...itemProps}>{children}</li>
520
+ );
521
+ return (
522
+ <DropdownItemIndicatorContext.Provider value={indicatorContextValue}>
523
+ {content}
524
+ </DropdownItemIndicatorContext.Provider>
525
+ );
526
+ }
527
+
528
+ DropdownRadioItem.displayName = "DropdownRadioItem";
529
+
530
+ /**
531
+ * The visible mark (usually a checkmark or a bullet) rendered inside a
532
+ * {@link DropdownCheckboxItem | `Dropdown.CheckboxItem`} or
533
+ * {@link DropdownRadioItem | `Dropdown.RadioItem`}. Must be a descendant
534
+ * of one of those; rendering it anywhere else throws a descriptive error.
535
+ *
536
+ * Renders a `<span>` by default; pass `asChild` to compose onto any
537
+ * element (commonly an SVG icon). Exposes `data-state` reflecting the
538
+ * parent item's live state: `"checked"`, `"unchecked"`, or
539
+ * `"indeterminate"` — the last being reachable only through a tri-state
540
+ * `Dropdown.CheckboxItem`.
541
+ *
542
+ * By default the indicator unmounts when its parent is unchecked. Pass
543
+ * {@link DropdownItemIndicatorProps.forceMount | `forceMount`} to keep
544
+ * the DOM node mounted in both states, which is handy when animating the
545
+ * indicator in and out via CSS transitions or a React animation library.
546
+ *
547
+ * @example
548
+ * ```tsx
549
+ * <Dropdown.CheckboxItem checked={showBookmarks} onCheckedChange={setShowBookmarks}>
550
+ * <Dropdown.ItemIndicator>
551
+ * <CheckIcon />
552
+ * </Dropdown.ItemIndicator>
553
+ * Show bookmarks
554
+ * </Dropdown.CheckboxItem>
555
+ * ```
556
+ */
557
+ function DropdownItemIndicator({
558
+ children,
559
+ asChild = false,
560
+ forceMount = false,
561
+ ...rest
562
+ }: DropdownItemIndicatorProps) {
563
+ const context = useContext(DropdownItemIndicatorContext);
564
+ if (!context) {
565
+ throw new Error(
566
+ "Dropdown.ItemIndicator must be rendered inside a <Dropdown.CheckboxItem> or <Dropdown.RadioItem>.",
567
+ );
568
+ }
569
+ const { checked } = context;
570
+ const dataState =
571
+ checked === "indeterminate"
572
+ ? "indeterminate"
573
+ : checked
574
+ ? "checked"
575
+ : "unchecked";
576
+ if (!forceMount && checked === false) return null;
577
+ const indicatorProps = { ...rest, "data-state": dataState };
578
+ if (asChild) {
579
+ return <Slot {...indicatorProps}>{children}</Slot>;
580
+ }
581
+ return <span {...indicatorProps}>{children}</span>;
582
+ }
583
+
584
+ DropdownItemIndicator.displayName = "DropdownItemIndicator";
585
+
586
+ /**
587
+ * A submenu boundary. Wrap a {@link DropdownSubTrigger | `Dropdown.SubTrigger`}
588
+ * and its {@link DropdownSubContent | `Dropdown.SubContent`} in a
589
+ * `Dropdown.Sub` to establish an independent open state for the nested menu.
590
+ *
591
+ * Supports uncontrolled (`defaultOpen`) and controlled (`open` +
592
+ * `onOpenChange`) modes, discriminated at the type level — same contract
593
+ * as {@link DropdownRoot | `Dropdown.Root`}.
594
+ *
595
+ * @example
596
+ * ```tsx
597
+ * <Dropdown.Root>
598
+ * <Dropdown.Trigger>File</Dropdown.Trigger>
599
+ * <Dropdown.Content>
600
+ * <Dropdown.Sub>
601
+ * <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
602
+ * <Dropdown.SubContent>
603
+ * <Dropdown.Item>Project A</Dropdown.Item>
604
+ * <Dropdown.Item>Project B</Dropdown.Item>
605
+ * </Dropdown.SubContent>
606
+ * </Dropdown.Sub>
607
+ * </Dropdown.Content>
608
+ * </Dropdown.Root>
609
+ * ```
610
+ */
611
+ function DropdownSub({
612
+ defaultOpen,
613
+ open: controlledOpen,
614
+ onOpenChange,
615
+ children,
616
+ }: DropdownSubProps) {
617
+ const { open, setOpen } = useDropdownRoot({
618
+ defaultOpen,
619
+ open: controlledOpen,
620
+ onOpenChange,
621
+ });
622
+ const contentId = useId();
623
+ const triggerRef = useRef<HTMLLIElement | null>(null);
624
+ const contextValue = useMemo(
625
+ () => ({ open, setOpen, contentId, triggerRef }),
626
+ [open, setOpen, contentId],
627
+ );
628
+ // Register with the enclosing Content so sibling items can close this sub
629
+ // on hover (mirroring the keyboard behaviour where returning focus to the
630
+ // parent menu dismisses the sub). If another sibling sub is already
631
+ // registered as open, close it first — a hover-to-open transition onto
632
+ // our SubTrigger should supplant the prior sub, not stack it.
633
+ const parentContent = useContext(DropdownContentContext);
634
+ useEffect(() => {
635
+ if (!open || !parentContent) return;
636
+ const close = () => setOpen(false);
637
+ const prev = parentContent.closeOpenSubRef.current;
638
+ if (prev && prev !== close) prev();
639
+ parentContent.closeOpenSubRef.current = close;
640
+ return () => {
641
+ if (parentContent.closeOpenSubRef.current === close) {
642
+ parentContent.closeOpenSubRef.current = null;
643
+ }
644
+ };
645
+ }, [open, parentContent, setOpen]);
646
+ return (
647
+ <DropdownSubContext.Provider value={contextValue}>
648
+ {children}
649
+ </DropdownSubContext.Provider>
650
+ );
651
+ }
652
+
653
+ DropdownSub.displayName = "DropdownSub";
654
+
655
+ /**
656
+ * The submenu trigger. Must be rendered inside a {@link DropdownSub |
657
+ * `Dropdown.Sub`}; rendering it outside one throws a descriptive error.
658
+ *
659
+ * Renders a `<li role="menuitem">` with `aria-haspopup="menu"`,
660
+ * `aria-expanded`, and `aria-controls` wiring it to the sibling
661
+ * {@link DropdownSubContent | `Dropdown.SubContent`}. `asChild` is supported.
662
+ *
663
+ * Opens the submenu on click, the inline-forward arrow key, or pointer
664
+ * hover; all other keys bubble to the parent {@link DropdownContent |
665
+ * `Dropdown.Content`} so its roving focus and typeahead continue to work
666
+ * while a submenu is in play. The open key follows the resolved reading
667
+ * direction — `ArrowRight` in `"ltr"`, `ArrowLeft` in `"rtl"`. Hovering
668
+ * onto a sibling item in the parent menu closes the submenu — mirroring
669
+ * the keyboard contract where focus returning to the parent dismisses it.
670
+ *
671
+ * Disabled triggers receive `aria-disabled="true"` and ignore both click
672
+ * and the open arrow key.
673
+ */
674
+ function DropdownSubTrigger({
675
+ children,
676
+ onClick,
677
+ onKeyDown,
678
+ disabled,
679
+ asChild = false,
680
+ ...rest
681
+ }: DropdownSubTriggerProps) {
682
+ const sub = useDropdownSubContext();
683
+ const { dir } = useDropdownContext();
684
+ const openKey = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
685
+ const [hovered, setHovered] = useState(false);
686
+ const toggle = () => {
687
+ if (disabled) return;
688
+ sub.setOpen(true);
689
+ };
690
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLLIElement>) => {
691
+ if (disabled) return;
692
+ if (event.key !== openKey) return;
693
+ event.preventDefault();
694
+ event.stopPropagation();
695
+ sub.setOpen(true);
696
+ };
697
+ const subTriggerProps = {
698
+ ...rest,
699
+ ref: sub.triggerRef,
700
+ role: "menuitem" as const,
701
+ tabIndex: -1,
702
+ "aria-haspopup": "menu" as const,
703
+ "aria-expanded": sub.open,
704
+ "aria-controls": sub.contentId,
705
+ "aria-disabled": disabled || undefined,
706
+ "data-highlighted": hovered || sub.open ? "" : undefined,
707
+ onClick: composeEventHandlers(onClick, toggle),
708
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
709
+ onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
710
+ setHovered(true);
711
+ if (!disabled) sub.setOpen(true);
712
+ }),
713
+ onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
714
+ setHovered(false),
715
+ ),
716
+ };
717
+ if (asChild) {
718
+ return <Slot {...subTriggerProps}>{children}</Slot>;
719
+ }
720
+ return <li {...subTriggerProps}>{children}</li>;
721
+ }
722
+
723
+ DropdownSubTrigger.displayName = "DropdownSubTrigger";
724
+
725
+ /**
726
+ * The submenu panel. Must be rendered inside a {@link DropdownSub |
727
+ * `Dropdown.Sub`}; rendering it outside one throws a descriptive error.
728
+ *
729
+ * Renders a `<menu role="menu" popover="auto">` by default; pass `asChild`
730
+ * to render any element with menu semantics. When the submenu opens, focus
731
+ * moves to its first enabled item.
732
+ *
733
+ * Presses of the inline-backward arrow key close the submenu and return
734
+ * focus to the {@link DropdownSubTrigger | `Dropdown.SubTrigger`} —
735
+ * `ArrowLeft` in `"ltr"`, `ArrowRight` in `"rtl"`. All other keys bubble
736
+ * to the parent {@link DropdownContent | `Dropdown.Content`} so arrow
737
+ * navigation and typeahead apply to the submenu's items.
738
+ */
739
+ function DropdownSubContent({
740
+ children,
741
+ onKeyDown,
742
+ asChild = false,
743
+ ...rest
744
+ }: DropdownSubContentProps) {
745
+ const sub = useDropdownSubContext();
746
+ const { dir } = useDropdownContext();
747
+ const closeKey = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
748
+ const menuRef = useRef<HTMLMenuElement | null>(null);
749
+
750
+ useEffect(() => {
751
+ const menu = menuRef.current!;
752
+ if (sub.open) {
753
+ menu.showPopover();
754
+ const firstItem = menu.querySelector<HTMLElement>(MENUITEM_SELECTOR);
755
+ firstItem?.focus();
756
+ } else {
757
+ try {
758
+ menu.hidePopover();
759
+ } catch {
760
+ // already hidden — no-op
761
+ }
762
+ }
763
+ }, [sub.open]);
764
+
765
+ useEffect(() => {
766
+ const menu = menuRef.current!;
767
+ const handleToggle = (event: ToggleEvent) => {
768
+ if (event.newState === "closed") sub.setOpen(false);
769
+ };
770
+ menu.addEventListener("toggle", handleToggle);
771
+ return () => menu.removeEventListener("toggle", handleToggle);
772
+ }, [sub.setOpen]);
773
+
774
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLMenuElement>) => {
775
+ if (event.key !== closeKey) return;
776
+ event.preventDefault();
777
+ event.stopPropagation();
778
+ sub.setOpen(false);
779
+ sub.triggerRef.current?.focus();
780
+ };
781
+
782
+ const closeOpenSubRef = useRef<(() => void) | null>(null);
783
+ const contentContextValue = useMemo(() => ({ closeOpenSubRef }), []);
784
+
785
+ const subContentProps = {
786
+ ...rest,
787
+ ref: menuRef,
788
+ id: sub.contentId,
789
+ role: "menu" as const,
790
+ popover: "auto" as const,
791
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
792
+ };
793
+ return (
794
+ <DropdownContentContext.Provider value={contentContextValue}>
795
+ {asChild ? (
796
+ <Slot {...subContentProps}>{children}</Slot>
797
+ ) : (
798
+ <menu {...subContentProps}>{children}</menu>
799
+ )}
800
+ </DropdownContentContext.Provider>
801
+ );
802
+ }
803
+
804
+ DropdownSubContent.displayName = "DropdownSubContent";
805
+
806
+ type TDropdownCompound = typeof DropdownRoot & {
807
+ Root: typeof DropdownRoot;
808
+ Trigger: typeof DropdownTrigger;
809
+ Content: typeof DropdownContent;
810
+ Item: typeof DropdownItem;
811
+ Separator: typeof DropdownSeparator;
812
+ Group: typeof DropdownGroup;
813
+ Label: typeof DropdownLabel;
814
+ CheckboxItem: typeof DropdownCheckboxItem;
815
+ RadioGroup: typeof DropdownRadioGroup;
816
+ RadioItem: typeof DropdownRadioItem;
817
+ ItemIndicator: typeof DropdownItemIndicator;
818
+ Sub: typeof DropdownSub;
819
+ SubTrigger: typeof DropdownSubTrigger;
820
+ SubContent: typeof DropdownSubContent;
821
+ };
822
+
823
+ const DropdownCompound: TDropdownCompound = Object.assign(DropdownRoot, {
824
+ Root: DropdownRoot,
825
+ Trigger: DropdownTrigger,
826
+ Content: DropdownContent,
827
+ Item: DropdownItem,
828
+ Separator: DropdownSeparator,
829
+ Group: DropdownGroup,
830
+ Label: DropdownLabel,
831
+ CheckboxItem: DropdownCheckboxItem,
832
+ RadioGroup: DropdownRadioGroup,
833
+ RadioItem: DropdownRadioItem,
834
+ ItemIndicator: DropdownItemIndicator,
835
+ Sub: DropdownSub,
836
+ SubTrigger: DropdownSubTrigger,
837
+ SubContent: DropdownSubContent,
838
+ });
839
+
840
+ DropdownCompound.displayName = "Dropdown";
841
+
842
+ export { DropdownCompound as Dropdown };