@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,310 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Dropdown } from "../Dropdown";
5
+
6
+ describe("Dropdown keyboard interaction", () => {
7
+ it("focuses the first menu item when the dropdown opens", async () => {
8
+ // Arrange
9
+ const user = userEvent.setup();
10
+ render(
11
+ <Dropdown.Root>
12
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
13
+ <Dropdown.Content>
14
+ <Dropdown.Item>Rename</Dropdown.Item>
15
+ <Dropdown.Item>Duplicate</Dropdown.Item>
16
+ <Dropdown.Item>Delete</Dropdown.Item>
17
+ </Dropdown.Content>
18
+ </Dropdown.Root>,
19
+ );
20
+
21
+ // Act
22
+ await user.click(screen.getByRole("button", { name: "Options" }));
23
+
24
+ // Assert
25
+ expect(
26
+ screen.getByRole("menuitem", { name: "Rename", hidden: true }),
27
+ ).toHaveFocus();
28
+ });
29
+
30
+ it("moves focus between items with ArrowDown and ArrowUp, wrapping at both ends", async () => {
31
+ // Arrange
32
+ const user = userEvent.setup();
33
+ render(
34
+ <Dropdown.Root defaultOpen>
35
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
36
+ <Dropdown.Content>
37
+ <Dropdown.Item>Rename</Dropdown.Item>
38
+ <Dropdown.Item>Duplicate</Dropdown.Item>
39
+ <Dropdown.Item>Delete</Dropdown.Item>
40
+ </Dropdown.Content>
41
+ </Dropdown.Root>,
42
+ );
43
+ const rename = screen.getByRole("menuitem", { name: "Rename", hidden: true });
44
+ const duplicate = screen.getByRole("menuitem", {
45
+ name: "Duplicate",
46
+ hidden: true,
47
+ });
48
+ const del = screen.getByRole("menuitem", { name: "Delete", hidden: true });
49
+ expect(rename).toHaveFocus();
50
+
51
+ // Act + Assert: forward
52
+ await user.keyboard("{ArrowDown}");
53
+ expect(duplicate).toHaveFocus();
54
+ await user.keyboard("{ArrowDown}");
55
+ expect(del).toHaveFocus();
56
+ // wrap around forward
57
+ await user.keyboard("{ArrowDown}");
58
+ expect(rename).toHaveFocus();
59
+
60
+ // Act + Assert: backward + wrap
61
+ await user.keyboard("{ArrowUp}");
62
+ expect(del).toHaveFocus();
63
+ await user.keyboard("{ArrowUp}");
64
+ expect(duplicate).toHaveFocus();
65
+ });
66
+
67
+ it("activates the focused item on Enter and closes the dropdown", async () => {
68
+ // Arrange
69
+ const user = userEvent.setup();
70
+ const onSelect = vi.fn();
71
+ render(
72
+ <Dropdown.Root defaultOpen>
73
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
74
+ <Dropdown.Content>
75
+ <Dropdown.Item onSelect={onSelect}>Rename</Dropdown.Item>
76
+ <Dropdown.Item>Duplicate</Dropdown.Item>
77
+ </Dropdown.Content>
78
+ </Dropdown.Root>,
79
+ );
80
+ const menu = screen.getByRole("menu", { hidden: true });
81
+
82
+ // Act
83
+ await user.keyboard("{Enter}");
84
+
85
+ // Assert
86
+ expect(onSelect).toHaveBeenCalledTimes(1);
87
+ expect(menu).not.toHaveAttribute("data-popover-open");
88
+ });
89
+
90
+ it("activates the focused item on Space and closes the dropdown", async () => {
91
+ // Arrange
92
+ const user = userEvent.setup();
93
+ const onSelect = vi.fn();
94
+ render(
95
+ <Dropdown.Root defaultOpen>
96
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
97
+ <Dropdown.Content>
98
+ <Dropdown.Item onSelect={onSelect}>Rename</Dropdown.Item>
99
+ </Dropdown.Content>
100
+ </Dropdown.Root>,
101
+ );
102
+ const menu = screen.getByRole("menu", { hidden: true });
103
+
104
+ // Act — literal space, not "{Space}" (which emits key="Space")
105
+ await user.keyboard(" ");
106
+
107
+ // Assert
108
+ expect(onSelect).toHaveBeenCalledTimes(1);
109
+ expect(menu).not.toHaveAttribute("data-popover-open");
110
+ });
111
+
112
+ it("closes the dropdown on Escape and returns focus to the trigger", async () => {
113
+ // Arrange
114
+ const user = userEvent.setup();
115
+ render(
116
+ <Dropdown.Root defaultOpen>
117
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
118
+ <Dropdown.Content>
119
+ <Dropdown.Item>Rename</Dropdown.Item>
120
+ <Dropdown.Item>Duplicate</Dropdown.Item>
121
+ </Dropdown.Content>
122
+ </Dropdown.Root>,
123
+ );
124
+ const trigger = screen.getByRole("button", { name: "Options" });
125
+ const menu = screen.getByRole("menu", { hidden: true });
126
+ expect(menu).toHaveAttribute("data-popover-open");
127
+
128
+ // Act
129
+ await user.keyboard("{Escape}");
130
+
131
+ // Assert
132
+ expect(menu).not.toHaveAttribute("data-popover-open");
133
+ expect(trigger).toHaveFocus();
134
+ });
135
+
136
+ it("returns focus to the trigger after activating an item with Enter", async () => {
137
+ // Arrange
138
+ const user = userEvent.setup();
139
+ render(
140
+ <Dropdown.Root defaultOpen>
141
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
142
+ <Dropdown.Content>
143
+ <Dropdown.Item>Rename</Dropdown.Item>
144
+ <Dropdown.Item>Duplicate</Dropdown.Item>
145
+ </Dropdown.Content>
146
+ </Dropdown.Root>,
147
+ );
148
+ const trigger = screen.getByRole("button", { name: "Options" });
149
+
150
+ // Act
151
+ await user.keyboard("{Enter}");
152
+
153
+ // Assert
154
+ expect(trigger).toHaveFocus();
155
+ });
156
+
157
+ it("jumps to the first item on Home and the last item on End", async () => {
158
+ // Arrange
159
+ const user = userEvent.setup();
160
+ render(
161
+ <Dropdown.Root defaultOpen>
162
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
163
+ <Dropdown.Content>
164
+ <Dropdown.Item>Rename</Dropdown.Item>
165
+ <Dropdown.Item>Duplicate</Dropdown.Item>
166
+ <Dropdown.Item>Delete</Dropdown.Item>
167
+ </Dropdown.Content>
168
+ </Dropdown.Root>,
169
+ );
170
+ const rename = screen.getByRole("menuitem", { name: "Rename", hidden: true });
171
+ const del = screen.getByRole("menuitem", { name: "Delete", hidden: true });
172
+
173
+ // Act
174
+ await user.keyboard("{End}");
175
+
176
+ // Assert
177
+ expect(del).toHaveFocus();
178
+
179
+ // Act
180
+ await user.keyboard("{Home}");
181
+
182
+ // Assert
183
+ expect(rename).toHaveFocus();
184
+ });
185
+
186
+ it("skips items inside a closed sub-content popover when ArrowDown moves past a SubTrigger", async () => {
187
+ // Arrange — the parent Content's items are queried via querySelectorAll,
188
+ // which by default also matches menuitems inside any descendant
189
+ // SubContent popover. Those are not focusable while the sub is closed
190
+ // (display: none), so the user gets stuck on the SubTrigger. Arrow
191
+ // navigation must scope itself to the active popover.
192
+ const user = userEvent.setup();
193
+ render(
194
+ <Dropdown.Root defaultOpen>
195
+ <Dropdown.Trigger>File</Dropdown.Trigger>
196
+ <Dropdown.Content>
197
+ <Dropdown.Item>New</Dropdown.Item>
198
+ <Dropdown.Sub>
199
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
200
+ <Dropdown.SubContent>
201
+ <Dropdown.Item>Project A</Dropdown.Item>
202
+ <Dropdown.Item>Project B</Dropdown.Item>
203
+ </Dropdown.SubContent>
204
+ </Dropdown.Sub>
205
+ <Dropdown.Separator />
206
+ <Dropdown.CheckboxItem>Show bookmarks</Dropdown.CheckboxItem>
207
+ </Dropdown.Content>
208
+ </Dropdown.Root>,
209
+ );
210
+ const subTrigger = screen.getByRole("menuitem", {
211
+ name: "Open Recent",
212
+ hidden: true,
213
+ });
214
+ const showBookmarks = screen.getByRole("menuitemcheckbox", {
215
+ name: "Show bookmarks",
216
+ hidden: true,
217
+ });
218
+
219
+ // Act — focus starts on "New" (first item); ArrowDown lands on the
220
+ // SubTrigger; another ArrowDown must jump past the closed SubContent's
221
+ // items and land on the next parent-level item.
222
+ await user.keyboard("{ArrowDown}");
223
+ expect(subTrigger).toHaveFocus();
224
+
225
+ await user.keyboard("{ArrowDown}");
226
+
227
+ // Assert
228
+ expect(showBookmarks).toHaveFocus();
229
+ });
230
+
231
+ it("skips items inside a closed sub-content popover when ArrowUp moves past a SubTrigger", async () => {
232
+ // Arrange — the inverse of the ArrowDown case: ArrowUp from the item
233
+ // after the Sub must land on the SubTrigger, not on a sub item.
234
+ const user = userEvent.setup();
235
+ render(
236
+ <Dropdown.Root defaultOpen>
237
+ <Dropdown.Trigger>File</Dropdown.Trigger>
238
+ <Dropdown.Content>
239
+ <Dropdown.Sub>
240
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
241
+ <Dropdown.SubContent>
242
+ <Dropdown.Item>Project A</Dropdown.Item>
243
+ </Dropdown.SubContent>
244
+ </Dropdown.Sub>
245
+ <Dropdown.Item>New</Dropdown.Item>
246
+ </Dropdown.Content>
247
+ </Dropdown.Root>,
248
+ );
249
+ const subTrigger = screen.getByRole("menuitem", {
250
+ name: "Open Recent",
251
+ hidden: true,
252
+ });
253
+ const newItem = screen.getByRole("menuitem", {
254
+ name: "New",
255
+ hidden: true,
256
+ });
257
+
258
+ // Act — focus starts on the SubTrigger; move down to "New" then back up
259
+ await user.keyboard("{ArrowDown}");
260
+ expect(newItem).toHaveFocus();
261
+
262
+ await user.keyboard("{ArrowUp}");
263
+
264
+ // Assert
265
+ expect(subTrigger).toHaveFocus();
266
+ });
267
+
268
+ it("scopes ArrowDown navigation to items inside the open SubContent rather than wrapping into the parent menu", async () => {
269
+ // Arrange — when focus lives inside an open SubContent, ArrowDown must
270
+ // wrap within the sub's own items and never escape into siblings of
271
+ // the SubTrigger in the parent menu.
272
+ const user = userEvent.setup();
273
+ render(
274
+ <Dropdown.Root defaultOpen>
275
+ <Dropdown.Trigger>File</Dropdown.Trigger>
276
+ <Dropdown.Content>
277
+ <Dropdown.Sub defaultOpen>
278
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
279
+ <Dropdown.SubContent>
280
+ <Dropdown.Item>Project A</Dropdown.Item>
281
+ <Dropdown.Item>Project B</Dropdown.Item>
282
+ </Dropdown.SubContent>
283
+ </Dropdown.Sub>
284
+ <Dropdown.Item>New</Dropdown.Item>
285
+ </Dropdown.Content>
286
+ </Dropdown.Root>,
287
+ );
288
+ const projectA = screen.getByRole("menuitem", {
289
+ name: "Project A",
290
+ hidden: true,
291
+ });
292
+ const projectB = screen.getByRole("menuitem", {
293
+ name: "Project B",
294
+ hidden: true,
295
+ });
296
+
297
+ // Sub auto-focuses its first item on open
298
+ expect(projectA).toHaveFocus();
299
+
300
+ // Act — ArrowDown advances within the sub
301
+ await user.keyboard("{ArrowDown}");
302
+ expect(projectB).toHaveFocus();
303
+
304
+ // Act — ArrowDown again: wraps within the sub, NOT into "New" in the parent
305
+ await user.keyboard("{ArrowDown}");
306
+
307
+ // Assert
308
+ expect(projectA).toHaveFocus();
309
+ });
310
+ });
@@ -0,0 +1,347 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+
4
+ import { Dropdown } from "../Dropdown";
5
+
6
+ describe("Dropdown mouse interaction", () => {
7
+ it("adds data-highlighted to an Item when the pointer enters it", async () => {
8
+ // Arrange
9
+ const user = userEvent.setup();
10
+ render(
11
+ <Dropdown.Root defaultOpen>
12
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
13
+ <Dropdown.Content>
14
+ <Dropdown.Item>Rename</Dropdown.Item>
15
+ </Dropdown.Content>
16
+ </Dropdown.Root>,
17
+ );
18
+ const item = screen.getByRole("menuitem", { name: "Rename", hidden: true });
19
+
20
+ // Act
21
+ await user.hover(item);
22
+
23
+ // Assert
24
+ expect(item).toHaveAttribute("data-highlighted");
25
+ });
26
+
27
+ it("removes data-highlighted from an Item when the pointer leaves", async () => {
28
+ // Arrange
29
+ const user = userEvent.setup();
30
+ render(
31
+ <Dropdown.Root defaultOpen>
32
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
33
+ <Dropdown.Content>
34
+ <Dropdown.Item>Rename</Dropdown.Item>
35
+ </Dropdown.Content>
36
+ </Dropdown.Root>,
37
+ );
38
+ const item = screen.getByRole("menuitem", { name: "Rename", hidden: true });
39
+
40
+ // Act
41
+ await user.hover(item);
42
+ await user.unhover(item);
43
+
44
+ // Assert
45
+ expect(item).not.toHaveAttribute("data-highlighted");
46
+ });
47
+
48
+ it("removes data-highlighted from a CheckboxItem when the pointer leaves", async () => {
49
+ // Arrange — mirrors the plain-Item unhover contract for the tri-state
50
+ // checkbox variant, which has its own setHighlighted(false) path.
51
+ const user = userEvent.setup();
52
+ render(
53
+ <Dropdown.Root defaultOpen>
54
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
55
+ <Dropdown.Content>
56
+ <Dropdown.CheckboxItem>Show bookmarks</Dropdown.CheckboxItem>
57
+ </Dropdown.Content>
58
+ </Dropdown.Root>,
59
+ );
60
+ const item = screen.getByRole("menuitemcheckbox", {
61
+ name: "Show bookmarks",
62
+ hidden: true,
63
+ });
64
+
65
+ // Act
66
+ await user.hover(item);
67
+ expect(item).toHaveAttribute("data-highlighted");
68
+ await user.unhover(item);
69
+
70
+ // Assert
71
+ expect(item).not.toHaveAttribute("data-highlighted");
72
+ });
73
+
74
+ it("removes data-highlighted from a RadioItem when the pointer leaves", async () => {
75
+ // Arrange — mirrors the plain-Item unhover contract for the radio
76
+ // variant, which has its own setHighlighted(false) path.
77
+ const user = userEvent.setup();
78
+ render(
79
+ <Dropdown.Root defaultOpen>
80
+ <Dropdown.Trigger>Options</Dropdown.Trigger>
81
+ <Dropdown.Content>
82
+ <Dropdown.RadioGroup>
83
+ <Dropdown.RadioItem value="light">Light</Dropdown.RadioItem>
84
+ </Dropdown.RadioGroup>
85
+ </Dropdown.Content>
86
+ </Dropdown.Root>,
87
+ );
88
+ const item = screen.getByRole("menuitemradio", {
89
+ name: "Light",
90
+ hidden: true,
91
+ });
92
+
93
+ // Act
94
+ await user.hover(item);
95
+ expect(item).toHaveAttribute("data-highlighted");
96
+ await user.unhover(item);
97
+
98
+ // Assert
99
+ expect(item).not.toHaveAttribute("data-highlighted");
100
+ });
101
+
102
+ it("adds data-highlighted to a SubTrigger when the pointer enters it", async () => {
103
+ // Arrange
104
+ const user = userEvent.setup();
105
+ render(
106
+ <Dropdown.Root defaultOpen>
107
+ <Dropdown.Trigger>File</Dropdown.Trigger>
108
+ <Dropdown.Content>
109
+ <Dropdown.Sub>
110
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
111
+ <Dropdown.SubContent>
112
+ <Dropdown.Item>Project A</Dropdown.Item>
113
+ </Dropdown.SubContent>
114
+ </Dropdown.Sub>
115
+ </Dropdown.Content>
116
+ </Dropdown.Root>,
117
+ );
118
+ const subTrigger = screen.getByRole("menuitem", {
119
+ name: "Open Recent",
120
+ hidden: true,
121
+ });
122
+
123
+ // Act
124
+ await user.hover(subTrigger);
125
+
126
+ // Assert
127
+ expect(subTrigger).toHaveAttribute("data-highlighted");
128
+ });
129
+
130
+ it("opens the sub-menu and sets aria-expanded when the pointer enters the SubTrigger", async () => {
131
+ // Arrange
132
+ const user = userEvent.setup();
133
+ render(
134
+ <Dropdown.Root defaultOpen>
135
+ <Dropdown.Trigger>File</Dropdown.Trigger>
136
+ <Dropdown.Content>
137
+ <Dropdown.Sub>
138
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
139
+ <Dropdown.SubContent>
140
+ <Dropdown.Item>Project A</Dropdown.Item>
141
+ </Dropdown.SubContent>
142
+ </Dropdown.Sub>
143
+ </Dropdown.Content>
144
+ </Dropdown.Root>,
145
+ );
146
+ const subTrigger = screen.getByRole("menuitem", {
147
+ name: "Open Recent",
148
+ hidden: true,
149
+ });
150
+ const [, subMenu] = screen.getAllByRole("menu", { hidden: true });
151
+ expect(subMenu).not.toHaveAttribute("data-popover-open");
152
+ expect(subTrigger).toHaveAttribute("aria-expanded", "false");
153
+
154
+ // Act
155
+ await user.hover(subTrigger);
156
+
157
+ // Assert
158
+ expect(subMenu).toHaveAttribute("data-popover-open");
159
+ expect(subTrigger).toHaveAttribute("aria-expanded", "true");
160
+ });
161
+
162
+ it("keeps data-highlighted on a SubTrigger after the pointer leaves while its sub-menu stays open", async () => {
163
+ // Arrange
164
+ const user = userEvent.setup();
165
+ render(
166
+ <Dropdown.Root defaultOpen>
167
+ <Dropdown.Trigger>File</Dropdown.Trigger>
168
+ <Dropdown.Content>
169
+ <Dropdown.Sub defaultOpen>
170
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
171
+ <Dropdown.SubContent>
172
+ <Dropdown.Item>Project A</Dropdown.Item>
173
+ </Dropdown.SubContent>
174
+ </Dropdown.Sub>
175
+ </Dropdown.Content>
176
+ </Dropdown.Root>,
177
+ );
178
+ const subTrigger = screen.getByRole("menuitem", {
179
+ name: "Open Recent",
180
+ hidden: true,
181
+ });
182
+
183
+ // Act — hover then move the pointer away; sub-menu remains open
184
+ await user.hover(subTrigger);
185
+ await user.unhover(subTrigger);
186
+
187
+ // Assert — sub.open is true, so data-highlighted must persist
188
+ expect(subTrigger).toHaveAttribute("data-highlighted");
189
+ });
190
+
191
+ it("removes data-highlighted from a SubTrigger when the sub-menu closes and the pointer is not over it", async () => {
192
+ // Arrange — render with sub already open; focus auto-lands in sub-content
193
+ const user = userEvent.setup();
194
+ render(
195
+ <Dropdown.Root defaultOpen>
196
+ <Dropdown.Trigger>File</Dropdown.Trigger>
197
+ <Dropdown.Content>
198
+ <Dropdown.Sub defaultOpen>
199
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
200
+ <Dropdown.SubContent>
201
+ <Dropdown.Item>Project A</Dropdown.Item>
202
+ </Dropdown.SubContent>
203
+ </Dropdown.Sub>
204
+ </Dropdown.Content>
205
+ </Dropdown.Root>,
206
+ );
207
+ const subTrigger = screen.getByRole("menuitem", {
208
+ name: "Open Recent",
209
+ hidden: true,
210
+ });
211
+ // sub.open=true on render, so data-highlighted is present without any hover
212
+ expect(subTrigger).toHaveAttribute("data-highlighted");
213
+
214
+ // Act — ArrowLeft from inside the sub-content closes the sub-menu;
215
+ // focus never passes through the sub-trigger so hovered stays false
216
+ await user.keyboard("{ArrowLeft}");
217
+
218
+ // Assert — sub.open is now false and pointer never hovered the trigger
219
+ expect(subTrigger).not.toHaveAttribute("data-highlighted");
220
+ });
221
+
222
+ it("keeps data-highlighted on all ancestor SubTriggers while nested sub-menus are open", async () => {
223
+ // Arrange
224
+ render(
225
+ <Dropdown.Root defaultOpen>
226
+ <Dropdown.Trigger>File</Dropdown.Trigger>
227
+ <Dropdown.Content>
228
+ <Dropdown.Sub defaultOpen>
229
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
230
+ <Dropdown.SubContent>
231
+ <Dropdown.Sub defaultOpen>
232
+ <Dropdown.SubTrigger>From Cloud</Dropdown.SubTrigger>
233
+ <Dropdown.SubContent>
234
+ <Dropdown.Item>Dropbox</Dropdown.Item>
235
+ </Dropdown.SubContent>
236
+ </Dropdown.Sub>
237
+ </Dropdown.SubContent>
238
+ </Dropdown.Sub>
239
+ </Dropdown.Content>
240
+ </Dropdown.Root>,
241
+ );
242
+
243
+ // Assert — both levels are open, so both sub-triggers must be highlighted
244
+ const outerTrigger = screen.getByRole("menuitem", {
245
+ name: "Open Recent",
246
+ hidden: true,
247
+ });
248
+ const innerTrigger = screen.getByRole("menuitem", {
249
+ name: "From Cloud",
250
+ hidden: true,
251
+ });
252
+ expect(outerTrigger).toHaveAttribute("data-highlighted");
253
+ expect(innerTrigger).toHaveAttribute("data-highlighted");
254
+ });
255
+
256
+ it("closes an open sub-menu when the pointer moves onto a sibling item in the parent menu", async () => {
257
+ // Mirrors the keyboard behaviour (ArrowLeft returns focus to the parent
258
+ // and closes the sub). With the mouse, moving off the sub-trigger and
259
+ // onto another item in the same parent menu must likewise close the sub
260
+ // and clear the sub-trigger's data-highlighted, while the newly hovered
261
+ // item picks up data-highlighted.
262
+ const user = userEvent.setup();
263
+ render(
264
+ <Dropdown.Root defaultOpen>
265
+ <Dropdown.Trigger>File</Dropdown.Trigger>
266
+ <Dropdown.Content>
267
+ <Dropdown.Item>New</Dropdown.Item>
268
+ <Dropdown.Sub>
269
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
270
+ <Dropdown.SubContent>
271
+ <Dropdown.Item>Project A</Dropdown.Item>
272
+ </Dropdown.SubContent>
273
+ </Dropdown.Sub>
274
+ </Dropdown.Content>
275
+ </Dropdown.Root>,
276
+ );
277
+ const subTrigger = screen.getByRole("menuitem", {
278
+ name: "Open Recent",
279
+ hidden: true,
280
+ });
281
+ const sibling = screen.getByRole("menuitem", {
282
+ name: "New",
283
+ hidden: true,
284
+ });
285
+ const [, subMenu] = screen.getAllByRole("menu", { hidden: true });
286
+
287
+ // Act — hover sub-trigger to open, then move onto a sibling item
288
+ await user.hover(subTrigger);
289
+ expect(subMenu).toHaveAttribute("data-popover-open");
290
+ expect(subTrigger).toHaveAttribute("aria-expanded", "true");
291
+ expect(subTrigger).toHaveAttribute("data-highlighted");
292
+
293
+ await user.hover(sibling);
294
+
295
+ // Assert — sub has closed, its trigger is no longer highlighted, and the
296
+ // sibling owns data-highlighted.
297
+ expect(subMenu).not.toHaveAttribute("data-popover-open");
298
+ expect(subTrigger).toHaveAttribute("aria-expanded", "false");
299
+ expect(subTrigger).not.toHaveAttribute("data-highlighted");
300
+ expect(sibling).toHaveAttribute("data-highlighted");
301
+ });
302
+
303
+ it("supplants an open sibling sub when the pointer hovers a second SubTrigger", async () => {
304
+ // Two sibling subs in the same parent menu. Hovering A opens A, then
305
+ // hovering B must close A and open B — the new sub's registration
306
+ // invokes the previously-registered close callback on the way in,
307
+ // rather than waiting for a separate "leave A" event.
308
+ const user = userEvent.setup();
309
+ render(
310
+ <Dropdown.Root defaultOpen>
311
+ <Dropdown.Trigger>File</Dropdown.Trigger>
312
+ <Dropdown.Content>
313
+ <Dropdown.Sub>
314
+ <Dropdown.SubTrigger>Open Recent</Dropdown.SubTrigger>
315
+ <Dropdown.SubContent>
316
+ <Dropdown.Item>Project A</Dropdown.Item>
317
+ </Dropdown.SubContent>
318
+ </Dropdown.Sub>
319
+ <Dropdown.Sub>
320
+ <Dropdown.SubTrigger>Share</Dropdown.SubTrigger>
321
+ <Dropdown.SubContent>
322
+ <Dropdown.Item>Email</Dropdown.Item>
323
+ </Dropdown.SubContent>
324
+ </Dropdown.Sub>
325
+ </Dropdown.Content>
326
+ </Dropdown.Root>,
327
+ );
328
+ const subTriggerA = screen.getByRole("menuitem", {
329
+ name: "Open Recent",
330
+ hidden: true,
331
+ });
332
+ const subTriggerB = screen.getByRole("menuitem", {
333
+ name: "Share",
334
+ hidden: true,
335
+ });
336
+
337
+ // Act — open A via hover, then move onto B's trigger
338
+ await user.hover(subTriggerA);
339
+ expect(subTriggerA).toHaveAttribute("aria-expanded", "true");
340
+ await user.hover(subTriggerB);
341
+
342
+ // Assert — A has been supplanted by B without any sibling-item
343
+ // bouncing, proving B's registration ran the previous close callback.
344
+ expect(subTriggerA).toHaveAttribute("aria-expanded", "false");
345
+ expect(subTriggerB).toHaveAttribute("aria-expanded", "true");
346
+ });
347
+ });