@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,262 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { ContextMenu } from "../ContextMenu";
5
+
6
+ describe("ContextMenu.Sub", () => {
7
+ it("renders SubTrigger as role=menuitem with aria-haspopup=menu and aria-expanded=false when the sub is closed", () => {
8
+ // Arrange & Act
9
+ render(
10
+ <ContextMenu.Root defaultOpen>
11
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
12
+ <ContextMenu.Content>
13
+ <ContextMenu.Sub>
14
+ <ContextMenu.SubTrigger>More</ContextMenu.SubTrigger>
15
+ <ContextMenu.SubContent>
16
+ <ContextMenu.Item>Nested</ContextMenu.Item>
17
+ </ContextMenu.SubContent>
18
+ </ContextMenu.Sub>
19
+ </ContextMenu.Content>
20
+ </ContextMenu.Root>,
21
+ );
22
+
23
+ // Assert
24
+ const subTrigger = screen.getByRole("menuitem", {
25
+ name: "More",
26
+ hidden: true,
27
+ });
28
+ expect(subTrigger).toHaveAttribute("aria-haspopup", "menu");
29
+ expect(subTrigger).toHaveAttribute("aria-expanded", "false");
30
+ expect(subTrigger).toHaveAttribute("aria-controls");
31
+ });
32
+
33
+ it("wires aria-controls on the SubTrigger to the SubContent's id", () => {
34
+ // Arrange & Act
35
+ render(
36
+ <ContextMenu.Root defaultOpen>
37
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
38
+ <ContextMenu.Content>
39
+ <ContextMenu.Sub defaultOpen>
40
+ <ContextMenu.SubTrigger>More</ContextMenu.SubTrigger>
41
+ <ContextMenu.SubContent>
42
+ <ContextMenu.Item>Nested</ContextMenu.Item>
43
+ </ContextMenu.SubContent>
44
+ </ContextMenu.Sub>
45
+ </ContextMenu.Content>
46
+ </ContextMenu.Root>,
47
+ );
48
+
49
+ // Assert
50
+ const subTrigger = screen.getByRole("menuitem", {
51
+ name: "More",
52
+ hidden: true,
53
+ });
54
+ const [, subMenu] = screen.getAllByRole("menu", { hidden: true });
55
+ expect(subMenu.id).toBeTruthy();
56
+ expect(subTrigger).toHaveAttribute("aria-controls", subMenu.id);
57
+ expect(subTrigger).toHaveAttribute("aria-expanded", "true");
58
+ });
59
+
60
+ it("opens the sub-menu on ArrowRight from the SubTrigger", async () => {
61
+ // Arrange
62
+ const user = userEvent.setup();
63
+ render(
64
+ <ContextMenu.Root defaultOpen>
65
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
66
+ <ContextMenu.Content>
67
+ <ContextMenu.Sub>
68
+ <ContextMenu.SubTrigger>More</ContextMenu.SubTrigger>
69
+ <ContextMenu.SubContent>
70
+ <ContextMenu.Item>Nested</ContextMenu.Item>
71
+ </ContextMenu.SubContent>
72
+ </ContextMenu.Sub>
73
+ </ContextMenu.Content>
74
+ </ContextMenu.Root>,
75
+ );
76
+ const subTrigger = screen.getByRole("menuitem", {
77
+ name: "More",
78
+ hidden: true,
79
+ });
80
+ expect(subTrigger).toHaveAttribute("aria-expanded", "false");
81
+
82
+ // Act
83
+ await user.keyboard("{ArrowRight}");
84
+
85
+ // Assert
86
+ expect(subTrigger).toHaveAttribute("aria-expanded", "true");
87
+ });
88
+
89
+ it("closes the sub-menu on ArrowLeft and returns focus to the SubTrigger", async () => {
90
+ // Arrange
91
+ const user = userEvent.setup();
92
+ render(
93
+ <ContextMenu.Root defaultOpen>
94
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
95
+ <ContextMenu.Content>
96
+ <ContextMenu.Sub defaultOpen>
97
+ <ContextMenu.SubTrigger>More</ContextMenu.SubTrigger>
98
+ <ContextMenu.SubContent>
99
+ <ContextMenu.Item>Nested</ContextMenu.Item>
100
+ </ContextMenu.SubContent>
101
+ </ContextMenu.Sub>
102
+ </ContextMenu.Content>
103
+ </ContextMenu.Root>,
104
+ );
105
+ const subTrigger = screen.getByRole("menuitem", {
106
+ name: "More",
107
+ hidden: true,
108
+ });
109
+
110
+ // Act
111
+ await user.keyboard("{ArrowLeft}");
112
+
113
+ // Assert
114
+ expect(subTrigger).toHaveAttribute("aria-expanded", "false");
115
+ expect(subTrigger).toHaveFocus();
116
+ });
117
+
118
+ it("opens the sub-menu when the pointer enters the SubTrigger", async () => {
119
+ // Arrange
120
+ const user = userEvent.setup();
121
+ render(
122
+ <ContextMenu.Root defaultOpen>
123
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
124
+ <ContextMenu.Content>
125
+ <ContextMenu.Sub>
126
+ <ContextMenu.SubTrigger>More</ContextMenu.SubTrigger>
127
+ <ContextMenu.SubContent>
128
+ <ContextMenu.Item>Nested</ContextMenu.Item>
129
+ </ContextMenu.SubContent>
130
+ </ContextMenu.Sub>
131
+ </ContextMenu.Content>
132
+ </ContextMenu.Root>,
133
+ );
134
+ const subTrigger = screen.getByRole("menuitem", {
135
+ name: "More",
136
+ hidden: true,
137
+ });
138
+
139
+ // Act
140
+ await user.hover(subTrigger);
141
+
142
+ // Assert
143
+ expect(subTrigger).toHaveAttribute("aria-expanded", "true");
144
+ });
145
+
146
+ it("throws when SubTrigger is rendered outside a Sub", () => {
147
+ // Arrange
148
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
149
+
150
+ // Act + Assert
151
+ expect(() =>
152
+ render(
153
+ <ContextMenu.Root defaultOpen>
154
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
155
+ <ContextMenu.Content>
156
+ <ContextMenu.SubTrigger>More</ContextMenu.SubTrigger>
157
+ </ContextMenu.Content>
158
+ </ContextMenu.Root>,
159
+ ),
160
+ ).toThrow(/SubTrigger.*<ContextMenu\.Sub>/);
161
+
162
+ spy.mockRestore();
163
+ });
164
+
165
+ it("disables ArrowRight, click, and hover when SubTrigger is disabled", async () => {
166
+ // Arrange
167
+ const user = userEvent.setup();
168
+ render(
169
+ <ContextMenu.Root defaultOpen>
170
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
171
+ <ContextMenu.Content>
172
+ <ContextMenu.Sub>
173
+ <ContextMenu.SubTrigger disabled>More</ContextMenu.SubTrigger>
174
+ <ContextMenu.SubContent>
175
+ <ContextMenu.Item>Nested</ContextMenu.Item>
176
+ </ContextMenu.SubContent>
177
+ </ContextMenu.Sub>
178
+ </ContextMenu.Content>
179
+ </ContextMenu.Root>,
180
+ );
181
+ const subTrigger = screen.getByRole("menuitem", {
182
+ name: "More",
183
+ hidden: true,
184
+ });
185
+
186
+ // Act
187
+ await user.hover(subTrigger);
188
+
189
+ // Assert
190
+ expect(subTrigger).toHaveAttribute("aria-disabled", "true");
191
+ expect(subTrigger).toHaveAttribute("aria-expanded", "false");
192
+ });
193
+
194
+ it("closes the nested Sub when an item inside the SubContent is activated, so the sub state does not leak to the next open", async () => {
195
+ // Repro: open menu, hover into the sub, click an item inside. The whole
196
+ // menu closes. If the sub's own state were left stale, re-opening the
197
+ // menu would briefly render the sub popover before anything else closed
198
+ // it — a real-browser flash in the top-left corner.
199
+ const user = userEvent.setup();
200
+ render(
201
+ <ContextMenu.Root defaultOpen>
202
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
203
+ <ContextMenu.Content>
204
+ <ContextMenu.Sub>
205
+ <ContextMenu.SubTrigger>Share</ContextMenu.SubTrigger>
206
+ <ContextMenu.SubContent>
207
+ <ContextMenu.Item>Email</ContextMenu.Item>
208
+ </ContextMenu.SubContent>
209
+ </ContextMenu.Sub>
210
+ </ContextMenu.Content>
211
+ </ContextMenu.Root>,
212
+ );
213
+ const subTrigger = screen.getByRole("menuitem", {
214
+ name: "Share",
215
+ hidden: true,
216
+ });
217
+
218
+ // Act — hover opens the sub, then activate a nested item
219
+ await user.hover(subTrigger);
220
+ expect(subTrigger).toHaveAttribute("aria-expanded", "true");
221
+ fireEvent.click(
222
+ screen.getByRole("menuitem", { name: "Email", hidden: true }),
223
+ );
224
+
225
+ // Assert — the sub's open must have collapsed alongside the root close
226
+ expect(subTrigger).toHaveAttribute("aria-expanded", "false");
227
+ });
228
+
229
+ it("closes a stale nested Sub when the parent menu closes via outside click", async () => {
230
+ // Same leak shape via a different close path: outside-click dismisses
231
+ // the menu while a sub is open. The sub must collapse with it.
232
+ const user = userEvent.setup();
233
+ render(
234
+ <div>
235
+ <ContextMenu.Root defaultOpen>
236
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
237
+ <ContextMenu.Content>
238
+ <ContextMenu.Sub>
239
+ <ContextMenu.SubTrigger>Share</ContextMenu.SubTrigger>
240
+ <ContextMenu.SubContent>
241
+ <ContextMenu.Item>Email</ContextMenu.Item>
242
+ </ContextMenu.SubContent>
243
+ </ContextMenu.Sub>
244
+ </ContextMenu.Content>
245
+ </ContextMenu.Root>
246
+ <button type="button">Outside</button>
247
+ </div>,
248
+ );
249
+ const subTrigger = screen.getByRole("menuitem", {
250
+ name: "Share",
251
+ hidden: true,
252
+ });
253
+ await user.hover(subTrigger);
254
+ expect(subTrigger).toHaveAttribute("aria-expanded", "true");
255
+
256
+ // Act
257
+ await user.click(screen.getByText("Outside"));
258
+
259
+ // Assert
260
+ expect(subTrigger).toHaveAttribute("aria-expanded", "false");
261
+ });
262
+ });
@@ -0,0 +1,89 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { ContextMenu } from "../ContextMenu";
5
+
6
+ function renderMenu() {
7
+ return render(
8
+ <ContextMenu.Root defaultOpen>
9
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
10
+ <ContextMenu.Content>
11
+ <ContextMenu.Item>Apple</ContextMenu.Item>
12
+ <ContextMenu.Item>Banana</ContextMenu.Item>
13
+ <ContextMenu.Item>Blueberry</ContextMenu.Item>
14
+ <ContextMenu.Item>Cherry</ContextMenu.Item>
15
+ </ContextMenu.Content>
16
+ </ContextMenu.Root>,
17
+ );
18
+ }
19
+
20
+ describe("ContextMenu typeahead", () => {
21
+ it("focuses the next item that starts with the typed character", async () => {
22
+ // Arrange
23
+ const user = userEvent.setup();
24
+ renderMenu();
25
+
26
+ // Act
27
+ await user.keyboard("b");
28
+
29
+ // Assert
30
+ expect(
31
+ screen.getByRole("menuitem", { name: "Banana", hidden: true }),
32
+ ).toHaveFocus();
33
+ });
34
+
35
+ it("cycles through items sharing a first letter when the same character is pressed repeatedly", async () => {
36
+ // Arrange
37
+ const user = userEvent.setup();
38
+ renderMenu();
39
+
40
+ // Act
41
+ await user.keyboard("b");
42
+ expect(
43
+ screen.getByRole("menuitem", { name: "Banana", hidden: true }),
44
+ ).toHaveFocus();
45
+ await user.keyboard("b");
46
+
47
+ // Assert
48
+ expect(
49
+ screen.getByRole("menuitem", { name: "Blueberry", hidden: true }),
50
+ ).toHaveFocus();
51
+ });
52
+
53
+ it("matches a multi-character prefix typed within the typeahead window", async () => {
54
+ // Arrange
55
+ const user = userEvent.setup();
56
+ renderMenu();
57
+
58
+ // Act — "bl" should jump straight to Blueberry, skipping Banana
59
+ await user.keyboard("bl");
60
+
61
+ // Assert
62
+ expect(
63
+ screen.getByRole("menuitem", { name: "Blueberry", hidden: true }),
64
+ ).toHaveFocus();
65
+ });
66
+
67
+ it("ignores disabled items in the typeahead match", async () => {
68
+ // Arrange
69
+ const user = userEvent.setup();
70
+ render(
71
+ <ContextMenu.Root defaultOpen>
72
+ <ContextMenu.Trigger>Area</ContextMenu.Trigger>
73
+ <ContextMenu.Content>
74
+ <ContextMenu.Item>Apple</ContextMenu.Item>
75
+ <ContextMenu.Item disabled>Banana</ContextMenu.Item>
76
+ <ContextMenu.Item>Blueberry</ContextMenu.Item>
77
+ </ContextMenu.Content>
78
+ </ContextMenu.Root>,
79
+ );
80
+
81
+ // Act
82
+ await user.keyboard("b");
83
+
84
+ // Assert — Banana is disabled, so first match is Blueberry
85
+ expect(
86
+ screen.getByRole("menuitem", { name: "Blueberry", hidden: true }),
87
+ ).toHaveFocus();
88
+ });
89
+ });
@@ -0,0 +1,4 @@
1
+ export const MENUITEM_SELECTOR =
2
+ '[role="menuitem"]:not([aria-disabled="true"]), [role="menuitemcheckbox"]:not([aria-disabled="true"]), [role="menuitemradio"]:not([aria-disabled="true"])';
3
+
4
+ export const TYPEAHEAD_RESET_MS = 500;
@@ -0,0 +1 @@
1
+ export { ContextMenu } from "./ContextMenu";
@@ -0,0 +1,199 @@
1
+ import { ComponentProps, ReactNode, Ref } from "react";
2
+
3
+ import { CheckedState } from "../Checkbox/types";
4
+ import { Direction } from "../DirectionProvider";
5
+
6
+ type ContextMenuRootBaseProps = {
7
+ children?: ReactNode;
8
+ /**
9
+ * Reading direction for the menu. Affects which arrow key opens / closes
10
+ * a submenu — `ArrowRight` opens in `"ltr"`, `ArrowLeft` opens in
11
+ * `"rtl"`. Falls back to the inherited {@link DirectionProvider} value,
12
+ * or to `"ltr"` if no provider is present.
13
+ */
14
+ dir?: Direction;
15
+ };
16
+
17
+ type ContextMenuRootUncontrolledProps = ContextMenuRootBaseProps & {
18
+ defaultOpen?: boolean;
19
+ open?: never;
20
+ onOpenChange?: (open: boolean) => void;
21
+ };
22
+
23
+ type ContextMenuRootControlledProps = ContextMenuRootBaseProps & {
24
+ defaultOpen?: never;
25
+ open: boolean;
26
+ onOpenChange: (open: boolean) => void;
27
+ };
28
+
29
+ export type ContextMenuRootProps =
30
+ | ContextMenuRootUncontrolledProps
31
+ | ContextMenuRootControlledProps;
32
+
33
+ export type ContextMenuTriggerProps = ComponentProps<"span"> & {
34
+ children?: ReactNode;
35
+ ref?: Ref<HTMLSpanElement>;
36
+ asChild?: boolean;
37
+ disabled?: boolean;
38
+ };
39
+
40
+ export type ContextMenuContentProps = Omit<
41
+ ComponentProps<"menu">,
42
+ "role" | "popover" | "id"
43
+ > & {
44
+ children?: ReactNode;
45
+ ref?: Ref<HTMLMenuElement>;
46
+ asChild?: boolean;
47
+ };
48
+
49
+ export type ContextMenuItemProps = Omit<
50
+ ComponentProps<"li">,
51
+ "role" | "tabIndex" | "onSelect"
52
+ > & {
53
+ children?: ReactNode;
54
+ ref?: Ref<HTMLLIElement>;
55
+ asChild?: boolean;
56
+ disabled?: boolean;
57
+ /**
58
+ * Fires when the item is activated (click, Enter, or Space). Called
59
+ * with an event whose `preventDefault()` skips the auto-close that
60
+ * ContextMenu performs after selection.
61
+ */
62
+ onSelect?: (event: Event) => void;
63
+ };
64
+
65
+ export type ContextMenuSeparatorProps = Omit<ComponentProps<"li">, "role"> & {
66
+ children?: ReactNode;
67
+ ref?: Ref<HTMLLIElement>;
68
+ asChild?: boolean;
69
+ };
70
+
71
+ export type ContextMenuGroupProps = Omit<ComponentProps<"li">, "role"> & {
72
+ children?: ReactNode;
73
+ ref?: Ref<HTMLLIElement>;
74
+ asChild?: boolean;
75
+ };
76
+
77
+ export type ContextMenuLabelProps = ComponentProps<"li"> & {
78
+ children?: ReactNode;
79
+ ref?: Ref<HTMLLIElement>;
80
+ asChild?: boolean;
81
+ };
82
+
83
+ type ContextMenuCheckboxItemBaseProps = Omit<
84
+ ComponentProps<"li">,
85
+ "role" | "tabIndex" | "aria-checked" | "defaultChecked" | "onSelect"
86
+ > & {
87
+ children?: ReactNode;
88
+ ref?: Ref<HTMLLIElement>;
89
+ asChild?: boolean;
90
+ disabled?: boolean;
91
+ /**
92
+ * Fires when activation completes and the auto-close fires. Call
93
+ * `event.preventDefault()` to keep the menu open after toggling.
94
+ */
95
+ onSelect?: (event: Event) => void;
96
+ };
97
+
98
+ type ContextMenuCheckboxItemUncontrolledProps =
99
+ ContextMenuCheckboxItemBaseProps & {
100
+ defaultChecked?: CheckedState;
101
+ checked?: never;
102
+ onCheckedChange?: (checked: boolean) => void;
103
+ };
104
+
105
+ type ContextMenuCheckboxItemControlledProps =
106
+ ContextMenuCheckboxItemBaseProps & {
107
+ defaultChecked?: never;
108
+ checked: CheckedState;
109
+ onCheckedChange: (checked: boolean) => void;
110
+ };
111
+
112
+ export type ContextMenuCheckboxItemProps =
113
+ | ContextMenuCheckboxItemUncontrolledProps
114
+ | ContextMenuCheckboxItemControlledProps;
115
+
116
+ export type ContextMenuItemIndicatorProps = ComponentProps<"span"> & {
117
+ children?: ReactNode;
118
+ ref?: Ref<HTMLSpanElement>;
119
+ asChild?: boolean;
120
+ /**
121
+ * Render the indicator even when its parent item is unchecked. The
122
+ * `data-state` attribute still reflects the live state.
123
+ */
124
+ forceMount?: boolean;
125
+ };
126
+
127
+ type ContextMenuRadioGroupBaseProps = Omit<ComponentProps<"li">, "role"> & {
128
+ children?: ReactNode;
129
+ ref?: Ref<HTMLLIElement>;
130
+ asChild?: boolean;
131
+ };
132
+
133
+ type ContextMenuRadioGroupUncontrolledProps =
134
+ ContextMenuRadioGroupBaseProps & {
135
+ defaultValue?: string;
136
+ value?: never;
137
+ onValueChange?: (value: string) => void;
138
+ };
139
+
140
+ type ContextMenuRadioGroupControlledProps = ContextMenuRadioGroupBaseProps & {
141
+ defaultValue?: never;
142
+ value: string;
143
+ onValueChange: (value: string) => void;
144
+ };
145
+
146
+ export type ContextMenuRadioGroupProps =
147
+ | ContextMenuRadioGroupUncontrolledProps
148
+ | ContextMenuRadioGroupControlledProps;
149
+
150
+ export type ContextMenuRadioItemProps = Omit<
151
+ ComponentProps<"li">,
152
+ "role" | "tabIndex" | "aria-checked" | "onSelect"
153
+ > & {
154
+ children?: ReactNode;
155
+ ref?: Ref<HTMLLIElement>;
156
+ asChild?: boolean;
157
+ disabled?: boolean;
158
+ value: string;
159
+ onSelect?: (event: Event) => void;
160
+ };
161
+
162
+ type ContextMenuSubBaseProps = {
163
+ children?: ReactNode;
164
+ };
165
+
166
+ type ContextMenuSubUncontrolledProps = ContextMenuSubBaseProps & {
167
+ defaultOpen?: boolean;
168
+ open?: never;
169
+ onOpenChange?: (open: boolean) => void;
170
+ };
171
+
172
+ type ContextMenuSubControlledProps = ContextMenuSubBaseProps & {
173
+ defaultOpen?: never;
174
+ open: boolean;
175
+ onOpenChange: (open: boolean) => void;
176
+ };
177
+
178
+ export type ContextMenuSubProps =
179
+ | ContextMenuSubUncontrolledProps
180
+ | ContextMenuSubControlledProps;
181
+
182
+ export type ContextMenuSubTriggerProps = Omit<
183
+ ComponentProps<"li">,
184
+ "role" | "tabIndex" | "aria-haspopup" | "aria-expanded" | "aria-controls"
185
+ > & {
186
+ children?: ReactNode;
187
+ ref?: Ref<HTMLLIElement>;
188
+ asChild?: boolean;
189
+ disabled?: boolean;
190
+ };
191
+
192
+ export type ContextMenuSubContentProps = Omit<
193
+ ComponentProps<"menu">,
194
+ "role" | "popover" | "id"
195
+ > & {
196
+ children?: ReactNode;
197
+ ref?: Ref<HTMLMenuElement>;
198
+ asChild?: boolean;
199
+ };
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ import { Direction } from "./types";
4
+
5
+ export const DirectionContext = createContext<Direction>("ltr");
6
+
7
+ /**
8
+ * Reads the current reading direction from the nearest
9
+ * {@link DirectionProvider} ancestor.
10
+ *
11
+ * Falls back to `"ltr"` when no provider is present, so it is always safe
12
+ * to call — components use it as the default for an omitted `dir` prop.
13
+ *
14
+ * @example Fall back to the inherited direction
15
+ * ```tsx
16
+ * const resolvedDir = dir ?? useDirection();
17
+ * ```
18
+ */
19
+ export function useDirection(): Direction {
20
+ return useContext(DirectionContext);
21
+ }
@@ -0,0 +1,31 @@
1
+ import { DirectionContext } from "./DirectionContext";
2
+ import { DirectionProviderProps } from "./types";
3
+
4
+ /**
5
+ * Broadcasts a reading direction to every descendant so direction-aware
6
+ * components can inherit it instead of each being passed an explicit
7
+ * `dir` prop.
8
+ *
9
+ * Renders **no DOM** — only a context provider. Direction is never
10
+ * inferred from the DOM; the consumer still owns setting `dir` on their
11
+ * `<html>` (or another element) for CSS. A descendant component's own
12
+ * `dir` prop always wins over the inherited value; components fall back
13
+ * to {@link useDirection} only when their `dir` prop is omitted.
14
+ *
15
+ * @example Declare direction once near the app root
16
+ * ```tsx
17
+ * <DirectionProvider dir="rtl">
18
+ * <Tabs.Root>…</Tabs.Root>
19
+ * <Slider.Root>…</Slider.Root>
20
+ * </DirectionProvider>
21
+ * ```
22
+ */
23
+ export function DirectionProvider({ dir, children }: DirectionProviderProps) {
24
+ return (
25
+ <DirectionContext.Provider value={dir}>
26
+ {children}
27
+ </DirectionContext.Provider>
28
+ );
29
+ }
30
+
31
+ DirectionProvider.displayName = "DirectionProvider";
@@ -0,0 +1,62 @@
1
+ # DirectionProvider
2
+
3
+ Broadcasts a reading direction (`ltr` / `rtl`) to its descendants so
4
+ direction-aware components inherit it through context instead of each
5
+ being passed an explicit `dir` prop.
6
+
7
+ Renders **no DOM** — only a React context provider.
8
+
9
+ ## Usage
10
+
11
+ ```tsx
12
+ import { DirectionProvider } from "@primitiv-ui/react";
13
+
14
+ <DirectionProvider dir="rtl">
15
+ <Tabs.Root defaultValue="overview">…</Tabs.Root>
16
+ <Slider.Root defaultValue={[50]} aria-label="Volume" />
17
+ </DirectionProvider>;
18
+ ```
19
+
20
+ A component's own `dir` prop always wins over the inherited direction —
21
+ components fall back to the provider only when `dir` is omitted:
22
+
23
+ ```tsx
24
+ <DirectionProvider dir="rtl">
25
+ {/* inherits rtl */}
26
+ <Tabs.Root defaultValue="a">…</Tabs.Root>
27
+ {/* explicit ltr overrides the provider */}
28
+ <Tabs.Root dir="ltr" defaultValue="a">…</Tabs.Root>
29
+ </DirectionProvider>;
30
+ ```
31
+
32
+ ## Props
33
+
34
+ | Prop | Type | Default | Description |
35
+ | ---------- | ---------------- | ------- | ---------------------------------------------- |
36
+ | `dir` | `"ltr" \| "rtl"` | — | Reading direction broadcast to all descendants |
37
+ | `children` | `ReactNode` | — | The subtree that inherits the direction |
38
+
39
+ ## useDirection
40
+
41
+ `useDirection()` reads the current direction from the nearest
42
+ `DirectionProvider`. It falls back to `"ltr"` when no provider is present,
43
+ so it is always safe to call — direction-aware components use it as the
44
+ default for an omitted `dir` prop:
45
+
46
+ ```tsx
47
+ import { useDirection } from "@primitiv-ui/react";
48
+
49
+ function MyControl({ dir }: { dir?: "ltr" | "rtl" }) {
50
+ const resolvedDir = dir ?? useDirection();
51
+ // …
52
+ }
53
+ ```
54
+
55
+ ## Notes
56
+
57
+ - `DirectionProvider` never inspects the DOM. Setting `dir` on `<html>`
58
+ (or another element) for CSS purposes remains the consumer's
59
+ responsibility — this component only propagates the value to
60
+ `@primitiv-ui/react` components.
61
+ - Providers nest: an inner `DirectionProvider` overrides an outer one for
62
+ its subtree.