@astryxdesign/core 0.1.9 → 0.2.0-canary.136a00f

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 (560) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/Avatar/Avatar.d.ts +42 -3
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +190 -59
  5. package/dist/Avatar/AvatarStatusDot.d.ts +6 -4
  6. package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
  7. package/dist/AvatarGroup/AvatarGroup.d.ts +7 -1
  8. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  9. package/dist/AvatarGroup/AvatarGroup.js +58 -6
  10. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
  11. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  12. package/dist/AvatarGroup/AvatarGroupOverflow.js +7 -2
  13. package/dist/Banner/Banner.js +2 -2
  14. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  15. package/dist/Breadcrumbs/BreadcrumbItem.js +11 -4
  16. package/dist/Breadcrumbs/index.d.ts +1 -0
  17. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  18. package/dist/Breadcrumbs/index.js +2 -1
  19. package/dist/Button/Button.d.ts +1 -1
  20. package/dist/Button/Button.d.ts.map +1 -1
  21. package/dist/Button/Button.js +6 -4
  22. package/dist/Calendar/Calendar.d.ts.map +1 -1
  23. package/dist/Calendar/Calendar.js +50 -9
  24. package/dist/Calendar/dayCellUtils.d.ts +4 -4
  25. package/dist/Calendar/dayCellUtils.js +2 -2
  26. package/dist/Calendar/styles.d.ts +30 -31
  27. package/dist/Calendar/styles.d.ts.map +1 -1
  28. package/dist/Calendar/styles.js +30 -31
  29. package/dist/Card/Card.js +1 -1
  30. package/dist/Carousel/Carousel.d.ts.map +1 -1
  31. package/dist/Carousel/Carousel.js +22 -12
  32. package/dist/Chat/ChatComposerInput.js +1 -1
  33. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  34. package/dist/Chat/ChatDictationButton.js +3 -1
  35. package/dist/Chat/ChatLayout.js +4 -4
  36. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  37. package/dist/Chat/ChatMessageBubble.js +8 -8
  38. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  39. package/dist/Chat/ChatToolCalls.js +10 -3
  40. package/dist/Chat/useTriggerMenu.js +2 -2
  41. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  42. package/dist/CheckboxInput/CheckboxInput.js +6 -6
  43. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  44. package/dist/CheckboxList/CheckboxListItem.js +19 -4
  45. package/dist/Citation/Citation.d.ts +22 -1
  46. package/dist/Citation/Citation.d.ts.map +1 -1
  47. package/dist/Citation/Citation.js +20 -6
  48. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  49. package/dist/CodeBlock/CodeBlock.js +16 -10
  50. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  51. package/dist/Collapsible/Collapsible.js +3 -1
  52. package/dist/CommandPalette/CommandPalette.d.ts +1 -1
  53. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  54. package/dist/CommandPalette/CommandPalette.js +42 -3
  55. package/dist/CommandPalette/CommandPaletteItem.js +1 -1
  56. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  57. package/dist/ContextMenu/ContextMenu.js +23 -5
  58. package/dist/ContextMenu/index.d.ts +1 -0
  59. package/dist/ContextMenu/index.d.ts.map +1 -1
  60. package/dist/ContextMenu/index.js +3 -1
  61. package/dist/DateInput/DateInput.d.ts +10 -2
  62. package/dist/DateInput/DateInput.d.ts.map +1 -1
  63. package/dist/DateInput/DateInput.js +33 -19
  64. package/dist/DateRangeInput/DateRangeInput.d.ts +10 -2
  65. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  66. package/dist/DateRangeInput/DateRangeInput.js +23 -20
  67. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  68. package/dist/DateTimeInput/DateTimeInput.js +23 -18
  69. package/dist/Divider/Divider.d.ts +2 -2
  70. package/dist/Divider/Divider.d.ts.map +1 -1
  71. package/dist/Divider/Divider.js +13 -0
  72. package/dist/DropdownMenu/DropdownMenu.d.ts +6 -0
  73. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  74. package/dist/DropdownMenu/DropdownMenu.js +22 -5
  75. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  76. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +4 -1
  77. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  78. package/dist/DropdownMenu/DropdownMenuItem.js +5 -2
  79. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +8 -11
  80. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  81. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +3 -1
  82. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
  83. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  84. package/dist/DropdownMenu/DropdownMenuRadioItem.js +5 -2
  85. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +104 -0
  86. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -0
  87. package/dist/DropdownMenu/DropdownMenuSubMenu.js +427 -0
  88. package/dist/DropdownMenu/index.d.ts +10 -0
  89. package/dist/DropdownMenu/index.d.ts.map +1 -1
  90. package/dist/DropdownMenu/index.js +13 -0
  91. package/dist/DropdownMenu/menuItemHover.d.ts +23 -0
  92. package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -0
  93. package/dist/DropdownMenu/menuItemHover.js +33 -0
  94. package/dist/DropdownMenu/menuItemRoles.d.ts +8 -0
  95. package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -1
  96. package/dist/DropdownMenu/menuItemRoles.js +10 -1
  97. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  98. package/dist/DropdownMenu/renderDropdownItems.js +21 -6
  99. package/dist/Field/Field.d.ts +3 -1
  100. package/dist/Field/Field.d.ts.map +1 -1
  101. package/dist/Field/Field.js +4 -1
  102. package/dist/Field/FieldLabel.d.ts +2 -1
  103. package/dist/Field/FieldLabel.d.ts.map +1 -1
  104. package/dist/Field/FieldLabel.js +9 -5
  105. package/dist/Field/inputStyles.stylex.d.ts +1 -0
  106. package/dist/Field/inputStyles.stylex.d.ts.map +1 -1
  107. package/dist/Field/inputStyles.stylex.js +1 -0
  108. package/dist/FieldStatus/FieldStatus.d.ts +10 -0
  109. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  110. package/dist/FieldStatus/FieldStatus.js +39 -3
  111. package/dist/FileInput/FileInput.d.ts +10 -2
  112. package/dist/FileInput/FileInput.d.ts.map +1 -1
  113. package/dist/FileInput/FileInput.js +93 -65
  114. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  115. package/dist/HoverCard/useHoverCard.js +10 -4
  116. package/dist/Icon/Icon.d.ts +15 -1
  117. package/dist/Icon/Icon.d.ts.map +1 -1
  118. package/dist/Icon/Icon.js +19 -3
  119. package/dist/Item/Item.d.ts +16 -3
  120. package/dist/Item/Item.d.ts.map +1 -1
  121. package/dist/Item/Item.js +31 -6
  122. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  123. package/dist/Layer/layerAnimations.stylex.js +10 -2
  124. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  125. package/dist/Lightbox/Lightbox.js +19 -13
  126. package/dist/List/List.d.ts.map +1 -1
  127. package/dist/List/List.js +6 -3
  128. package/dist/List/ListItem.d.ts +11 -1
  129. package/dist/List/ListItem.d.ts.map +1 -1
  130. package/dist/List/ListItem.js +2 -0
  131. package/dist/Markdown/Markdown.d.ts.map +1 -1
  132. package/dist/Markdown/Markdown.js +7 -0
  133. package/dist/MetadataList/MetadataList.d.ts +1 -1
  134. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  135. package/dist/MetadataList/MetadataList.js +4 -2
  136. package/dist/MultiSelector/MultiSelector.d.ts +9 -1
  137. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  138. package/dist/MultiSelector/MultiSelector.js +80 -42
  139. package/dist/NavMenu/NavHeadingMenuItem.js +1 -1
  140. package/dist/NumberInput/NumberInput.d.ts +10 -2
  141. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  142. package/dist/NumberInput/NumberInput.js +22 -17
  143. package/dist/Outline/Outline.js +2 -2
  144. package/dist/Pagination/Pagination.d.ts.map +1 -1
  145. package/dist/Pagination/Pagination.js +11 -3
  146. package/dist/PowerSearch/PowerSearch.d.ts +9 -2
  147. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  148. package/dist/PowerSearch/PowerSearch.js +2 -0
  149. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  150. package/dist/ProgressBar/ProgressBar.js +6 -1
  151. package/dist/RadioList/RadioList.d.ts +2 -2
  152. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  153. package/dist/RadioList/RadioListItem.js +5 -5
  154. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  155. package/dist/Resizable/ResizeHandle.js +49 -19
  156. package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -1
  157. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  158. package/dist/SegmentedControl/SegmentedControlItem.js +5 -3
  159. package/dist/Selector/Selector.d.ts +8 -0
  160. package/dist/Selector/Selector.d.ts.map +1 -1
  161. package/dist/Selector/Selector.js +66 -17
  162. package/dist/SideNav/SideNav.d.ts.map +1 -1
  163. package/dist/SideNav/SideNav.js +5 -5
  164. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  165. package/dist/SideNav/SideNavCollapseButton.js +13 -10
  166. package/dist/SideNav/SideNavItem.d.ts +1 -1
  167. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  168. package/dist/SideNav/SideNavItem.js +12 -10
  169. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  170. package/dist/Skeleton/Skeleton.js +2 -2
  171. package/dist/Slider/Slider.d.ts.map +1 -1
  172. package/dist/Slider/Slider.js +87 -34
  173. package/dist/Spinner/Spinner.d.ts +3 -2
  174. package/dist/Spinner/Spinner.d.ts.map +1 -1
  175. package/dist/Spinner/Spinner.js +11 -2
  176. package/dist/Switch/Switch.d.ts +8 -1
  177. package/dist/Switch/Switch.d.ts.map +1 -1
  178. package/dist/Switch/Switch.js +171 -91
  179. package/dist/TabList/Tab.js +2 -2
  180. package/dist/TabList/TabList.d.ts +2 -11
  181. package/dist/TabList/TabList.d.ts.map +1 -1
  182. package/dist/TabList/TabList.js +5 -9
  183. package/dist/TabList/TabListContext.d.ts +0 -5
  184. package/dist/TabList/TabListContext.d.ts.map +1 -1
  185. package/dist/TabList/TabListContext.js +0 -5
  186. package/dist/TabList/TabMenu.js +1 -1
  187. package/dist/Table/BaseTable.d.ts.map +1 -1
  188. package/dist/Table/BaseTable.js +30 -4
  189. package/dist/Table/TableCell.js +3 -3
  190. package/dist/Table/TableHeaderCell.js +3 -3
  191. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  192. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +15 -11
  193. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  194. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +37 -27
  195. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  196. package/dist/Table/plugins/selection/useTableSelection.js +3 -0
  197. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
  198. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +15 -7
  199. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  200. package/dist/Table/plugins/tree/useTableTreeData.js +29 -23
  201. package/dist/Table/types.d.ts +32 -0
  202. package/dist/Table/types.d.ts.map +1 -1
  203. package/dist/TextArea/TextArea.d.ts +10 -1
  204. package/dist/TextArea/TextArea.d.ts.map +1 -1
  205. package/dist/TextArea/TextArea.js +18 -19
  206. package/dist/TextInput/TextInput.d.ts +10 -2
  207. package/dist/TextInput/TextInput.d.ts.map +1 -1
  208. package/dist/TextInput/TextInput.js +20 -17
  209. package/dist/Thumbnail/Thumbnail.js +2 -2
  210. package/dist/TimeInput/TimeInput.d.ts +10 -2
  211. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  212. package/dist/TimeInput/TimeInput.js +17 -17
  213. package/dist/Timestamp/Timestamp.d.ts +31 -3
  214. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  215. package/dist/Timestamp/Timestamp.js +47 -76
  216. package/dist/Timestamp/formatInstant.d.ts +41 -0
  217. package/dist/Timestamp/formatInstant.d.ts.map +1 -0
  218. package/dist/Timestamp/formatInstant.js +177 -0
  219. package/dist/Timestamp/index.d.ts +1 -0
  220. package/dist/Timestamp/index.d.ts.map +1 -1
  221. package/dist/Timestamp/tooltipEntries.d.ts +56 -0
  222. package/dist/Timestamp/tooltipEntries.d.ts.map +1 -0
  223. package/dist/Timestamp/tooltipEntries.js +163 -0
  224. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  225. package/dist/Toast/ToastViewport.js +61 -9
  226. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  227. package/dist/ToggleButton/ToggleButton.js +3 -1
  228. package/dist/Token/Token.d.ts +34 -4
  229. package/dist/Token/Token.d.ts.map +1 -1
  230. package/dist/Token/Token.js +85 -45
  231. package/dist/Token/TokenLink.d.ts +62 -0
  232. package/dist/Token/TokenLink.d.ts.map +1 -0
  233. package/dist/Token/TokenLink.js +76 -0
  234. package/dist/Token/index.d.ts +1 -1
  235. package/dist/Token/index.d.ts.map +1 -1
  236. package/dist/Tokenizer/Tokenizer.d.ts +9 -2
  237. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  238. package/dist/Tokenizer/Tokenizer.js +4 -2
  239. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  240. package/dist/Tooltip/useTooltip.js +17 -1
  241. package/dist/TopNav/TopNavItem.d.ts +3 -1
  242. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  243. package/dist/TopNav/TopNavItem.js +30 -3
  244. package/dist/TreeList/TreeList.d.ts.map +1 -1
  245. package/dist/TreeList/TreeList.js +1 -0
  246. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  247. package/dist/TreeList/TreeListBranches.js +8 -6
  248. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  249. package/dist/TreeList/TreeListItem.js +32 -14
  250. package/dist/Typeahead/BaseTypeahead.d.ts +9 -0
  251. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  252. package/dist/Typeahead/BaseTypeahead.js +22 -11
  253. package/dist/Typeahead/Typeahead.d.ts +9 -2
  254. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  255. package/dist/Typeahead/Typeahead.js +11 -2
  256. package/dist/Typeahead/TypeaheadItem.d.ts +1 -1
  257. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  258. package/dist/Typeahead/TypeaheadItem.js +20 -10
  259. package/dist/astryx.css +70 -30
  260. package/dist/astryx.umd.js +51 -51
  261. package/dist/astryx.umd.js.map +4 -4
  262. package/dist/hooks/index.d.ts +2 -0
  263. package/dist/hooks/index.d.ts.map +1 -1
  264. package/dist/hooks/index.js +1 -0
  265. package/dist/hooks/isRtlElement.d.ts +18 -0
  266. package/dist/hooks/isRtlElement.d.ts.map +1 -0
  267. package/dist/hooks/isRtlElement.js +27 -0
  268. package/dist/hooks/useAnnounce.d.ts +3 -0
  269. package/dist/hooks/useAnnounce.d.ts.map +1 -1
  270. package/dist/hooks/useAnnounce.js +39 -1
  271. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  272. package/dist/hooks/useFocusTrap.js +52 -11
  273. package/dist/hooks/useGridFocus.d.ts +6 -1
  274. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  275. package/dist/hooks/useGridFocus.js +12 -9
  276. package/dist/hooks/useHotkeys.d.ts +9 -0
  277. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  278. package/dist/hooks/useHotkeys.js +9 -0
  279. package/dist/hooks/useInputStatusIcon.d.ts +38 -0
  280. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -0
  281. package/dist/hooks/useInputStatusIcon.js +158 -0
  282. package/dist/hooks/useKeyboardHint.d.ts +1 -1
  283. package/dist/hooks/useKeyboardHint.d.ts.map +1 -1
  284. package/dist/hooks/useKeyboardHint.js +4 -2
  285. package/dist/hooks/useListFocus.d.ts +39 -1
  286. package/dist/hooks/useListFocus.d.ts.map +1 -1
  287. package/dist/hooks/useListFocus.js +54 -9
  288. package/dist/i18n/InternationalizationContext.d.ts +1 -0
  289. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  290. package/dist/i18n/InternationalizationContext.js +3 -0
  291. package/dist/i18n/InternationalizationProvider.d.ts +8 -1
  292. package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
  293. package/dist/i18n/InternationalizationProvider.js +5 -1
  294. package/dist/i18n/getLocaleDirection.d.ts +18 -0
  295. package/dist/i18n/getLocaleDirection.d.ts.map +1 -0
  296. package/dist/i18n/getLocaleDirection.js +29 -0
  297. package/dist/i18n/index.d.ts +6 -0
  298. package/dist/i18n/index.d.ts.map +1 -1
  299. package/dist/i18n/index.js +7 -1
  300. package/dist/i18n/useDirection.d.ts +2 -0
  301. package/dist/i18n/useDirection.d.ts.map +1 -0
  302. package/dist/i18n/useDirection.js +27 -0
  303. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  304. package/dist/theme/derivedVarRegistry.js +13 -0
  305. package/dist/utils/index.d.ts +1 -0
  306. package/dist/utils/index.d.ts.map +1 -1
  307. package/dist/utils/index.js +2 -1
  308. package/dist/utils/plainDate.d.ts +17 -0
  309. package/dist/utils/plainDate.d.ts.map +1 -1
  310. package/dist/utils/plainDate.js +23 -4
  311. package/dist/utils/rtlStyles.d.ts +24 -0
  312. package/dist/utils/rtlStyles.d.ts.map +1 -0
  313. package/dist/utils/rtlStyles.js +27 -0
  314. package/locales/en.json +112 -0
  315. package/locales/pseudo.json +84 -0
  316. package/package.json +6 -1
  317. package/src/Avatar/Avatar.doc.mjs +50 -3
  318. package/src/Avatar/Avatar.test.tsx +218 -0
  319. package/src/Avatar/Avatar.tsx +285 -79
  320. package/src/Avatar/AvatarStatusDot.doc.mjs +4 -4
  321. package/src/Avatar/AvatarStatusDot.tsx +6 -4
  322. package/src/AvatarGroup/AvatarGroup.doc.mjs +2 -0
  323. package/src/AvatarGroup/AvatarGroup.test.tsx +162 -2
  324. package/src/AvatarGroup/AvatarGroup.tsx +58 -3
  325. package/src/AvatarGroup/AvatarGroupOverflow.tsx +5 -2
  326. package/src/Banner/Banner.tsx +6 -6
  327. package/src/Breadcrumbs/BreadcrumbItem.tsx +10 -10
  328. package/src/Breadcrumbs/Breadcrumbs.test.tsx +52 -1
  329. package/src/Breadcrumbs/index.ts +4 -0
  330. package/src/Button/Button.doc.mjs +0 -4
  331. package/src/Button/Button.test.tsx +16 -0
  332. package/src/Button/Button.tsx +6 -4
  333. package/src/Calendar/Calendar.test.tsx +170 -20
  334. package/src/Calendar/Calendar.tsx +73 -16
  335. package/src/Calendar/dayCellUtils.test.ts +18 -18
  336. package/src/Calendar/dayCellUtils.ts +2 -2
  337. package/src/Calendar/styles.ts +33 -39
  338. package/src/Card/Card.tsx +1 -1
  339. package/src/Carousel/Carousel.tsx +25 -6
  340. package/src/Chat/ChatComposer.doc.mjs +2 -2
  341. package/src/Chat/ChatComposerInput.doc.mjs +1 -1
  342. package/src/Chat/ChatComposerInput.tsx +2 -2
  343. package/src/Chat/ChatDictationButton.tsx +7 -1
  344. package/src/Chat/ChatLayout.tsx +4 -4
  345. package/src/Chat/ChatMessageBubble.tsx +15 -10
  346. package/src/Chat/ChatSystemMessage.test.tsx +5 -0
  347. package/src/Chat/ChatToolCalls.tsx +9 -2
  348. package/src/Chat/useTriggerMenu.tsx +1 -1
  349. package/src/CheckboxInput/CheckboxInput.doc.mjs +6 -0
  350. package/src/CheckboxInput/CheckboxInput.test.tsx +23 -0
  351. package/src/CheckboxInput/CheckboxInput.tsx +21 -6
  352. package/src/CheckboxList/CheckboxList.doc.mjs +6 -0
  353. package/src/CheckboxList/CheckboxList.test.tsx +94 -3
  354. package/src/CheckboxList/CheckboxListItem.tsx +17 -10
  355. package/src/Citation/Citation.doc.mjs +4 -3
  356. package/src/Citation/Citation.test.tsx +97 -0
  357. package/src/Citation/Citation.tsx +43 -11
  358. package/src/CodeBlock/CodeBlock.test.tsx +17 -0
  359. package/src/CodeBlock/CodeBlock.tsx +38 -11
  360. package/src/Collapsible/Collapsible.doc.mjs +1 -0
  361. package/src/Collapsible/Collapsible.test.tsx +7 -0
  362. package/src/Collapsible/Collapsible.tsx +16 -6
  363. package/src/CommandPalette/CommandPalette.test.tsx +95 -1
  364. package/src/CommandPalette/CommandPalette.tsx +41 -2
  365. package/src/CommandPalette/CommandPaletteItem.tsx +1 -1
  366. package/src/ContextMenu/ContextMenu.test.tsx +59 -2
  367. package/src/ContextMenu/ContextMenu.tsx +22 -11
  368. package/src/ContextMenu/index.ts +5 -0
  369. package/src/DateInput/DateInput.doc.mjs +25 -0
  370. package/src/DateInput/DateInput.test.tsx +230 -0
  371. package/src/DateInput/DateInput.tsx +51 -29
  372. package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
  373. package/src/DateRangeInput/DateRangeInput.test.tsx +140 -0
  374. package/src/DateRangeInput/DateRangeInput.tsx +42 -30
  375. package/src/DateTimeInput/DateTimeInput.doc.mjs +22 -1
  376. package/src/DateTimeInput/DateTimeInput.test.tsx +112 -0
  377. package/src/DateTimeInput/DateTimeInput.tsx +44 -39
  378. package/src/Divider/Divider.test.tsx +32 -8
  379. package/src/Divider/Divider.tsx +14 -1
  380. package/src/DropdownMenu/DropdownMenu.test.tsx +60 -0
  381. package/src/DropdownMenu/DropdownMenu.tsx +31 -12
  382. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +8 -2
  383. package/src/DropdownMenu/DropdownMenuItem.tsx +9 -3
  384. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +4 -12
  385. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +9 -10
  386. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +9 -3
  387. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +20 -2
  388. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +88 -0
  389. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +635 -0
  390. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +553 -0
  391. package/src/DropdownMenu/index.ts +16 -0
  392. package/src/DropdownMenu/menuItemHover.ts +39 -0
  393. package/src/DropdownMenu/menuItemRoles.ts +9 -0
  394. package/src/DropdownMenu/renderDropdownItems.tsx +29 -15
  395. package/src/Field/Field.tsx +14 -9
  396. package/src/Field/FieldLabel.test.tsx +25 -0
  397. package/src/Field/FieldLabel.tsx +12 -3
  398. package/src/Field/inputStyles.stylex.ts +2 -0
  399. package/src/FieldStatus/FieldStatus.test.tsx +72 -0
  400. package/src/FieldStatus/FieldStatus.tsx +48 -2
  401. package/src/FileInput/FileInput.doc.mjs +23 -2
  402. package/src/FileInput/FileInput.test.tsx +104 -13
  403. package/src/FileInput/FileInput.tsx +106 -79
  404. package/src/HoverCard/useHoverCard.tsx +15 -3
  405. package/src/Icon/Icon.doc.mjs +13 -0
  406. package/src/Icon/Icon.test.tsx +89 -0
  407. package/src/Icon/Icon.tsx +51 -3
  408. package/src/Item/Item.doc.mjs +5 -0
  409. package/src/Item/Item.test.tsx +88 -0
  410. package/src/Item/Item.tsx +57 -11
  411. package/src/Layer/layerAnimations.stylex.ts +23 -0
  412. package/src/Lightbox/Lightbox.tsx +15 -7
  413. package/src/List/List.test.tsx +17 -29
  414. package/src/List/List.tsx +6 -1
  415. package/src/List/ListItem.doc.mjs +12 -0
  416. package/src/List/ListItem.tsx +13 -0
  417. package/src/Markdown/Markdown.test.tsx +21 -0
  418. package/src/Markdown/Markdown.tsx +4 -0
  419. package/src/MetadataList/MetadataList.test.tsx +24 -0
  420. package/src/MetadataList/MetadataList.tsx +6 -2
  421. package/src/MultiSelector/MultiSelector.doc.mjs +20 -0
  422. package/src/MultiSelector/MultiSelector.test.tsx +402 -0
  423. package/src/MultiSelector/MultiSelector.tsx +120 -59
  424. package/src/NavMenu/NavHeadingMenuItem.tsx +1 -1
  425. package/src/NumberInput/NumberInput.doc.mjs +21 -0
  426. package/src/NumberInput/NumberInput.test.tsx +71 -0
  427. package/src/NumberInput/NumberInput.tsx +35 -26
  428. package/src/Outline/Outline.test.tsx +12 -12
  429. package/src/Outline/Outline.tsx +2 -2
  430. package/src/Pagination/Pagination.tsx +11 -3
  431. package/src/PowerSearch/PowerSearch.doc.mjs +15 -0
  432. package/src/PowerSearch/PowerSearch.test.tsx +23 -0
  433. package/src/PowerSearch/PowerSearch.tsx +10 -1
  434. package/src/ProgressBar/ProgressBar.test.tsx +34 -0
  435. package/src/ProgressBar/ProgressBar.tsx +16 -1
  436. package/src/RadioList/RadioList.doc.mjs +6 -0
  437. package/src/RadioList/RadioList.test.tsx +26 -4
  438. package/src/RadioList/RadioList.tsx +2 -2
  439. package/src/RadioList/RadioListItem.tsx +12 -5
  440. package/src/Resizable/ResizeHandle.test.tsx +94 -0
  441. package/src/Resizable/ResizeHandle.tsx +70 -20
  442. package/src/SegmentedControl/SegmentedControl.test.tsx +28 -0
  443. package/src/SegmentedControl/SegmentedControlItem.tsx +22 -2
  444. package/src/Selector/Selector.doc.mjs +15 -0
  445. package/src/Selector/Selector.test.tsx +370 -0
  446. package/src/Selector/Selector.tsx +88 -20
  447. package/src/SideNav/SideNav.tsx +1 -8
  448. package/src/SideNav/SideNavCollapseButton.tsx +9 -7
  449. package/src/SideNav/SideNavItem.tsx +5 -3
  450. package/src/Skeleton/Skeleton.test.tsx +16 -0
  451. package/src/Skeleton/Skeleton.tsx +13 -1
  452. package/src/Slider/Slider.doc.mjs +6 -0
  453. package/src/Slider/Slider.test.tsx +240 -12
  454. package/src/Slider/Slider.tsx +96 -23
  455. package/src/Spinner/Spinner.test.tsx +10 -8
  456. package/src/Spinner/Spinner.tsx +15 -5
  457. package/src/Switch/Switch.doc.mjs +142 -42
  458. package/src/Switch/Switch.test.tsx +111 -0
  459. package/src/Switch/Switch.tsx +149 -32
  460. package/src/TabList/Tab.tsx +2 -2
  461. package/src/TabList/TabList.doc.mjs +0 -6
  462. package/src/TabList/TabList.test.tsx +3 -15
  463. package/src/TabList/TabList.tsx +6 -17
  464. package/src/TabList/TabListContext.ts +0 -6
  465. package/src/TabList/TabMenu.tsx +2 -2
  466. package/src/Table/BaseTable.tsx +34 -2
  467. package/src/Table/Table.doc.mjs +11 -0
  468. package/src/Table/Table.test.tsx +85 -0
  469. package/src/Table/TableCell.tsx +3 -3
  470. package/src/Table/TableHeaderCell.tsx +3 -3
  471. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +9 -6
  472. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +19 -0
  473. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +27 -18
  474. package/src/Table/plugins/selection/useTableSelection.test.tsx +61 -3
  475. package/src/Table/plugins/selection/useTableSelection.tsx +3 -0
  476. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +38 -9
  477. package/src/Table/plugins/tree/useTableTreeData.tsx +17 -13
  478. package/src/Table/types.ts +33 -0
  479. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  480. package/src/TextArea/TextArea.doc.mjs +21 -0
  481. package/src/TextArea/TextArea.test.tsx +23 -0
  482. package/src/TextArea/TextArea.tsx +27 -26
  483. package/src/TextInput/TextInput.doc.mjs +21 -0
  484. package/src/TextInput/TextInput.test.tsx +150 -0
  485. package/src/TextInput/TextInput.tsx +28 -25
  486. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  487. package/src/Thumbnail/Thumbnail.tsx +1 -1
  488. package/src/TimeInput/TimeInput.doc.mjs +21 -0
  489. package/src/TimeInput/TimeInput.test.tsx +23 -0
  490. package/src/TimeInput/TimeInput.tsx +24 -25
  491. package/src/Timestamp/Timestamp.doc.mjs +32 -6
  492. package/src/Timestamp/Timestamp.test.tsx +371 -0
  493. package/src/Timestamp/Timestamp.tsx +96 -93
  494. package/src/Timestamp/formatInstant.ts +218 -0
  495. package/src/Timestamp/index.ts +4 -0
  496. package/src/Timestamp/tooltipEntries.test.ts +552 -0
  497. package/src/Timestamp/tooltipEntries.ts +210 -0
  498. package/src/Toast/ToastViewport.test.tsx +218 -8
  499. package/src/Toast/ToastViewport.tsx +71 -12
  500. package/src/ToggleButton/ToggleButton.test.tsx +24 -0
  501. package/src/ToggleButton/ToggleButton.tsx +19 -1
  502. package/src/Token/Token.test.tsx +166 -15
  503. package/src/Token/Token.tsx +104 -47
  504. package/src/Token/TokenLink.tsx +107 -0
  505. package/src/Token/index.ts +1 -1
  506. package/src/Tokenizer/Tokenizer.doc.mjs +23 -1
  507. package/src/Tokenizer/Tokenizer.test.tsx +23 -0
  508. package/src/Tokenizer/Tokenizer.tsx +14 -2
  509. package/src/Tooltip/Tooltip.test.tsx +46 -0
  510. package/src/Tooltip/useTooltip.tsx +20 -0
  511. package/src/TopNav/TopNav.test.tsx +93 -2
  512. package/src/TopNav/TopNavItem.doc.mjs +3 -3
  513. package/src/TopNav/TopNavItem.tsx +35 -6
  514. package/src/TopNav/TopNavMegaMenuFeaturedCard.doc.mjs +1 -1
  515. package/src/TreeList/TreeList.doc.mjs +6 -0
  516. package/src/TreeList/TreeList.test.tsx +50 -10
  517. package/src/TreeList/TreeList.tsx +5 -0
  518. package/src/TreeList/TreeListBranches.tsx +8 -6
  519. package/src/TreeList/TreeListItem.tsx +33 -11
  520. package/src/Typeahead/BaseTypeahead.tsx +42 -15
  521. package/src/Typeahead/Typeahead.doc.mjs +13 -0
  522. package/src/Typeahead/Typeahead.test.tsx +112 -0
  523. package/src/Typeahead/Typeahead.tsx +17 -1
  524. package/src/Typeahead/TypeaheadItem.tsx +2 -1
  525. package/src/__tests__/forcedColors.ts +60 -0
  526. package/src/__tests__/inputWidthContract.test.tsx +62 -0
  527. package/src/doc-mjs.d.ts +19 -0
  528. package/src/hooks/index.ts +6 -0
  529. package/src/hooks/isRtlElement.ts +27 -0
  530. package/src/hooks/useAnnounce.test.tsx +75 -2
  531. package/src/hooks/useAnnounce.ts +47 -1
  532. package/src/hooks/useFocusTrap.test.tsx +104 -0
  533. package/src/hooks/useFocusTrap.ts +61 -12
  534. package/src/hooks/useGridFocus.doc.mjs +3 -3
  535. package/src/hooks/useGridFocus.test.tsx +41 -0
  536. package/src/hooks/useGridFocus.ts +21 -10
  537. package/src/hooks/useHotkeys.ts +9 -0
  538. package/src/hooks/useInputStatusIcon.test.tsx +162 -0
  539. package/src/hooks/useInputStatusIcon.tsx +224 -0
  540. package/src/hooks/useKeyboardHint.test.tsx +22 -0
  541. package/src/hooks/useKeyboardHint.tsx +6 -2
  542. package/src/hooks/useListFocus.doc.mjs +25 -3
  543. package/src/hooks/useListFocus.test.tsx +153 -1
  544. package/src/hooks/useListFocus.ts +102 -9
  545. package/src/i18n/InternationalizationContext.ts +4 -0
  546. package/src/i18n/InternationalizationProvider.tsx +12 -2
  547. package/src/i18n/__tests__/getLocaleDirection.test.ts +40 -0
  548. package/src/i18n/__tests__/useDirection.test.tsx +66 -0
  549. package/src/i18n/getLocaleDirection.ts +29 -0
  550. package/src/i18n/index.ts +6 -0
  551. package/src/i18n/useDirection.ts +29 -0
  552. package/src/reset.css +0 -26
  553. package/src/reset.test.ts +60 -0
  554. package/src/theme/derivedVarRegistry.test.ts +1 -0
  555. package/src/theme/derivedVarRegistry.ts +6 -0
  556. package/src/theme/generateThemeRules.test.ts +42 -0
  557. package/src/utils/index.ts +2 -0
  558. package/src/utils/plainDate.test.ts +24 -6
  559. package/src/utils/plainDate.ts +18 -4
  560. package/src/utils/rtlStyles.ts +27 -0
@@ -0,0 +1,635 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file DropdownMenuSubMenu.test.tsx
5
+ * @input vitest, @testing-library/react, DropdownMenu + DropdownMenuSubMenu
6
+ * @output Unit tests for DropdownMenuSubMenu (#3829)
7
+ */
8
+
9
+ import {describe, it, expect, vi, beforeEach} from 'vitest';
10
+ import {useState} from 'react';
11
+ import {render, screen, waitFor} from '@testing-library/react';
12
+ import userEvent from '@testing-library/user-event';
13
+ import {DropdownMenu} from './DropdownMenu';
14
+ import {DropdownMenuItem} from './DropdownMenuItem';
15
+ import {DropdownMenuSubMenu} from './DropdownMenuSubMenu';
16
+
17
+ beforeEach(() => {
18
+ HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
19
+ this.setAttribute('popover-open', '');
20
+ const event = new Event('toggle', {bubbles: false});
21
+ Object.defineProperty(event, 'newState', {value: 'open'});
22
+ this.dispatchEvent(event);
23
+ });
24
+ HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
25
+ this.removeAttribute('popover-open');
26
+ const event = new Event('toggle', {bubbles: false});
27
+ Object.defineProperty(event, 'newState', {value: 'closed'});
28
+ this.dispatchEvent(event);
29
+ });
30
+ const originalMatches = HTMLElement.prototype.matches;
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ (HTMLElement.prototype as any).matches = function (
33
+ selector: string,
34
+ ): boolean {
35
+ if (selector === ':popover-open') {
36
+ return this.hasAttribute('popover-open');
37
+ }
38
+ return originalMatches.call(this, selector);
39
+ };
40
+ });
41
+
42
+ function MoveMenu({onMove}: {onMove?: (folder: string) => void} = {}) {
43
+ return (
44
+ <DropdownMenu button={{label: 'Actions'}}>
45
+ <DropdownMenuItem label="Rename" onClick={() => {}} />
46
+ <DropdownMenuSubMenu label="Move to">
47
+ <DropdownMenuItem label="Folder A" onClick={() => onMove?.('a')} />
48
+ <DropdownMenuItem label="Folder B" onClick={() => onMove?.('b')} />
49
+ </DropdownMenuSubMenu>
50
+ </DropdownMenu>
51
+ );
52
+ }
53
+
54
+ describe('DropdownMenuSubMenu', () => {
55
+ it('renders the trigger with aria-haspopup and collapsed aria-expanded', async () => {
56
+ const user = userEvent.setup();
57
+ render(<MoveMenu />);
58
+ await user.click(screen.getByRole('button', {name: /Actions/}));
59
+ const trigger = screen.getByRole('menuitem', {
60
+ name: /Move to/,
61
+ hidden: true,
62
+ });
63
+ expect(trigger).toHaveAttribute('aria-haspopup', 'menu');
64
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
65
+ });
66
+
67
+ it('opens the flyout on trigger click and exposes its items', async () => {
68
+ const user = userEvent.setup();
69
+ render(<MoveMenu />);
70
+ await user.click(screen.getByRole('button', {name: /Actions/}));
71
+ const trigger = screen.getByRole('menuitem', {
72
+ name: /Move to/,
73
+ hidden: true,
74
+ });
75
+ await user.click(trigger);
76
+ await waitFor(() => {
77
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
78
+ });
79
+ expect(
80
+ screen.getByRole('menuitem', {name: 'Folder A', hidden: true}),
81
+ ).toBeInTheDocument();
82
+ });
83
+
84
+ it('opens on ArrowRight and returns focus to the trigger on ArrowLeft', async () => {
85
+ const user = userEvent.setup();
86
+ render(<MoveMenu />);
87
+ await user.click(screen.getByRole('button', {name: /Actions/}));
88
+ const trigger = screen.getByRole('menuitem', {
89
+ name: /Move to/,
90
+ hidden: true,
91
+ });
92
+ // The menu focuses its first item (Rename) on open via rAF; wait for that
93
+ // to settle before moving focus to the submenu trigger, so the deferred
94
+ // focus can't steal it back mid-test.
95
+ await waitFor(() => {
96
+ expect(
97
+ screen.getByRole('menuitem', {name: 'Rename', hidden: true}),
98
+ ).toHaveFocus();
99
+ });
100
+ trigger.focus();
101
+ await user.keyboard('{ArrowRight}');
102
+ await waitFor(() => {
103
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
104
+ });
105
+ // Focus moved into the flyout's first item.
106
+ await waitFor(() => {
107
+ expect(
108
+ screen.getByRole('menuitem', {name: 'Folder A', hidden: true}),
109
+ ).toHaveFocus();
110
+ });
111
+ await user.keyboard('{ArrowLeft}');
112
+ await waitFor(() => {
113
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
114
+ });
115
+ expect(trigger).toHaveFocus();
116
+ });
117
+
118
+ it('names the flyout from its trigger via aria-labelledby', async () => {
119
+ const user = userEvent.setup();
120
+ render(<MoveMenu />);
121
+ await user.click(screen.getByRole('button', {name: /Actions/}));
122
+ const trigger = screen.getByRole('menuitem', {
123
+ name: /Move to/,
124
+ hidden: true,
125
+ });
126
+ await user.click(trigger);
127
+ const flyout = await screen.findByRole('menu', {
128
+ name: /Move to/,
129
+ hidden: true,
130
+ });
131
+ expect(flyout).toHaveAttribute('aria-labelledby', trigger.id);
132
+ });
133
+
134
+ it('invokes the nested item handler on selection', async () => {
135
+ const user = userEvent.setup();
136
+ const onMove = vi.fn();
137
+ render(<MoveMenu onMove={onMove} />);
138
+ await user.click(screen.getByRole('button', {name: /Actions/}));
139
+ await user.click(
140
+ screen.getByRole('menuitem', {name: /Move to/, hidden: true}),
141
+ );
142
+ await user.click(
143
+ await screen.findByRole('menuitem', {name: 'Folder A', hidden: true}),
144
+ );
145
+ expect(onMove).toHaveBeenCalledWith('a');
146
+ });
147
+
148
+ it('closes the whole menu after selecting a nested item', async () => {
149
+ // Regression: the nested closeMenu only closed the flyout, leaving the
150
+ // root menu open. Selecting a leaf must dismiss the entire stack. The root
151
+ // menu's open state is reflected on its trigger button's aria-expanded.
152
+ const user = userEvent.setup();
153
+ render(<MoveMenu />);
154
+ const button = screen.getByRole('button', {name: /Actions/});
155
+ await user.click(button);
156
+ expect(button).toHaveAttribute('aria-expanded', 'true');
157
+ await user.click(
158
+ screen.getByRole('menuitem', {name: /Move to/, hidden: true}),
159
+ );
160
+ await user.click(
161
+ await screen.findByRole('menuitem', {name: 'Folder A', hidden: true}),
162
+ );
163
+ // The whole stack is dismissed — the root menu closed too.
164
+ await waitFor(() => {
165
+ expect(button).toHaveAttribute('aria-expanded', 'false');
166
+ });
167
+ });
168
+
169
+ it('activates a nested item with the Enter key and closes the menu', async () => {
170
+ const user = userEvent.setup();
171
+ const onMove = vi.fn();
172
+ render(<MoveMenu onMove={onMove} />);
173
+ const button = screen.getByRole('button', {name: /Actions/});
174
+ await user.click(button);
175
+ const trigger = screen.getByRole('menuitem', {
176
+ name: /Move to/,
177
+ hidden: true,
178
+ });
179
+ // Let the root menu's open-focus (Rename, via rAF) settle before moving
180
+ // focus to the submenu trigger, so it can't steal focus back mid-test.
181
+ await waitFor(() => {
182
+ expect(
183
+ screen.getByRole('menuitem', {name: 'Rename', hidden: true}),
184
+ ).toHaveFocus();
185
+ });
186
+ trigger.focus();
187
+ await user.keyboard('{ArrowRight}');
188
+ await waitFor(() => {
189
+ expect(
190
+ screen.getByRole('menuitem', {name: 'Folder A', hidden: true}),
191
+ ).toHaveFocus();
192
+ });
193
+ await user.keyboard('{Enter}');
194
+ expect(onMove).toHaveBeenCalledWith('a');
195
+ await waitFor(() => {
196
+ expect(button).toHaveAttribute('aria-expanded', 'false');
197
+ });
198
+ });
199
+
200
+ it('does not open a disabled submenu', async () => {
201
+ const user = userEvent.setup();
202
+ render(
203
+ <DropdownMenu button={{label: 'Actions'}}>
204
+ <DropdownMenuSubMenu label="Move to" isDisabled>
205
+ <DropdownMenuItem label="Folder A" onClick={() => {}} />
206
+ </DropdownMenuSubMenu>
207
+ </DropdownMenu>,
208
+ );
209
+ await user.click(screen.getByRole('button', {name: /Actions/}));
210
+ const trigger = screen.getByRole('menuitem', {
211
+ name: /Move to/,
212
+ hidden: true,
213
+ });
214
+ expect(trigger).toHaveAttribute('aria-disabled', 'true');
215
+ await user.click(trigger);
216
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
217
+ });
218
+
219
+ it('steps through every flyout item with ArrowDown without skipping', async () => {
220
+ // Regression: the flyout renders inline inside the parent menu's
221
+ // roving-focus container, so an unstopped ArrowDown bubbled to the parent
222
+ // and moved focus a second time — skipping the middle item.
223
+ const user = userEvent.setup();
224
+ render(
225
+ <DropdownMenu button={{label: 'Actions'}}>
226
+ <DropdownMenuSubMenu label="Move to">
227
+ <DropdownMenuItem label="Projects" onClick={() => {}} />
228
+ <DropdownMenuItem label="Archive" onClick={() => {}} />
229
+ <DropdownMenuItem label="Trash" onClick={() => {}} />
230
+ </DropdownMenuSubMenu>
231
+ </DropdownMenu>,
232
+ );
233
+ await user.click(screen.getByRole('button', {name: /Actions/}));
234
+ const trigger = screen.getByRole('menuitem', {
235
+ name: /Move to/,
236
+ hidden: true,
237
+ });
238
+ trigger.focus();
239
+ await user.keyboard('{ArrowRight}');
240
+ // Opens and focuses the first item.
241
+ await waitFor(() => {
242
+ expect(
243
+ screen.getByRole('menuitem', {name: 'Projects', hidden: true}),
244
+ ).toHaveFocus();
245
+ });
246
+ // ArrowDown lands on the MIDDLE item (previously skipped to Trash).
247
+ await user.keyboard('{ArrowDown}');
248
+ await waitFor(() => {
249
+ expect(
250
+ screen.getByRole('menuitem', {name: 'Archive', hidden: true}),
251
+ ).toHaveFocus();
252
+ });
253
+ // ArrowDown again lands on the last item.
254
+ await user.keyboard('{ArrowDown}');
255
+ await waitFor(() => {
256
+ expect(
257
+ screen.getByRole('menuitem', {name: 'Trash', hidden: true}),
258
+ ).toHaveFocus();
259
+ });
260
+ // ArrowUp returns to the middle item (no skip in reverse either).
261
+ await user.keyboard('{ArrowUp}');
262
+ await waitFor(() => {
263
+ expect(
264
+ screen.getByRole('menuitem', {name: 'Archive', hidden: true}),
265
+ ).toHaveFocus();
266
+ });
267
+ });
268
+ });
269
+
270
+ describe('DropdownMenu data-driven submenus', () => {
271
+ it('renders a submenu when an item declares nested items', async () => {
272
+ const user = userEvent.setup();
273
+ const onMove = vi.fn();
274
+ render(
275
+ <DropdownMenu
276
+ button={{label: 'Actions'}}
277
+ items={[
278
+ {label: 'Rename', onClick: () => {}},
279
+ {
280
+ label: 'Move to',
281
+ items: [
282
+ {
283
+ label: 'Folder A',
284
+ onClick: () => {
285
+ onMove('a');
286
+ },
287
+ },
288
+ {
289
+ label: 'Folder B',
290
+ onClick: () => {
291
+ onMove('b');
292
+ },
293
+ },
294
+ ],
295
+ },
296
+ ]}
297
+ />,
298
+ );
299
+ await user.click(screen.getByRole('button', {name: /Actions/}));
300
+ const trigger = screen.getByRole('menuitem', {
301
+ name: /Move to/,
302
+ hidden: true,
303
+ });
304
+ expect(trigger).toHaveAttribute('aria-haspopup', 'menu');
305
+ await user.click(trigger);
306
+ await user.click(
307
+ await screen.findByRole('menuitem', {name: 'Folder B', hidden: true}),
308
+ );
309
+ expect(onMove).toHaveBeenCalledWith('b');
310
+ });
311
+
312
+ it('keyboard-reaches an item positioned after a submenu row', async () => {
313
+ // Regression: the submenu flyout renders inline inside the root menu, so
314
+ // the root's item query swept in the (hidden) flyout items. Arrow nav then
315
+ // stalled on those unfocusable items and never reached "Delete" below the
316
+ // submenu row.
317
+ const user = userEvent.setup();
318
+ const onDelete = vi.fn();
319
+ render(
320
+ <DropdownMenu
321
+ button={{label: 'Actions'}}
322
+ items={[
323
+ {label: 'Rename', onClick: () => {}},
324
+ {
325
+ label: 'Move to',
326
+ items: [
327
+ {label: 'Folder A', onClick: () => {}},
328
+ {label: 'Folder B', onClick: () => {}},
329
+ ],
330
+ },
331
+ {type: 'divider'},
332
+ {label: 'Delete', onClick: onDelete},
333
+ ]}
334
+ />,
335
+ );
336
+ await user.click(screen.getByRole('button', {name: /Actions/}));
337
+ await waitFor(() => {
338
+ expect(
339
+ screen.getByRole('menuitem', {name: 'Rename', hidden: true}),
340
+ ).toHaveFocus();
341
+ });
342
+ // Rename → Move to → Delete (three ArrowDowns, no stalling on hidden
343
+ // flyout items).
344
+ await user.keyboard('{ArrowDown}');
345
+ await waitFor(() => {
346
+ expect(
347
+ screen.getByRole('menuitem', {name: /Move to/, hidden: true}),
348
+ ).toHaveFocus();
349
+ });
350
+ await user.keyboard('{ArrowDown}');
351
+ await waitFor(() => {
352
+ expect(
353
+ screen.getByRole('menuitem', {name: 'Delete', hidden: true}),
354
+ ).toHaveFocus();
355
+ });
356
+ await user.keyboard('{Enter}');
357
+ expect(onDelete).toHaveBeenCalled();
358
+ });
359
+ });
360
+
361
+ describe('DropdownMenuSubMenu accessibility (WCAG 2.2 / APG)', () => {
362
+ // 2.1.2 No Keyboard Trap + APG submenu contract: Escape closes the current
363
+ // submenu and returns focus to its trigger, leaving the parent menu open.
364
+ it('closes only the submenu on Escape and restores focus to the trigger', async () => {
365
+ const user = userEvent.setup();
366
+ render(<MoveMenu />);
367
+ const button = screen.getByRole('button', {name: /Actions/});
368
+ await user.click(button);
369
+ const trigger = screen.getByRole('menuitem', {
370
+ name: /Move to/,
371
+ hidden: true,
372
+ });
373
+ await waitFor(() => {
374
+ expect(
375
+ screen.getByRole('menuitem', {name: 'Rename', hidden: true}),
376
+ ).toHaveFocus();
377
+ });
378
+ trigger.focus();
379
+ await user.keyboard('{ArrowRight}');
380
+ await waitFor(() => {
381
+ expect(
382
+ screen.getByRole('menuitem', {name: 'Folder A', hidden: true}),
383
+ ).toHaveFocus();
384
+ });
385
+ await user.keyboard('{Escape}');
386
+ // Submenu collapsed, focus back on the trigger…
387
+ await waitFor(() => {
388
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
389
+ });
390
+ expect(trigger).toHaveFocus();
391
+ // …but the parent menu is still open (Escape didn't dismiss everything).
392
+ expect(button).toHaveAttribute('aria-expanded', 'true');
393
+ });
394
+
395
+ // 2.1.1 Keyboard: type-ahead is operable inside the flyout.
396
+ it('supports first-character type-ahead within the flyout', async () => {
397
+ const user = userEvent.setup();
398
+ render(
399
+ <DropdownMenu button={{label: 'Actions'}}>
400
+ <DropdownMenuSubMenu label="Move to">
401
+ <DropdownMenuItem label="Apple" onClick={() => {}} />
402
+ <DropdownMenuItem label="Banana" onClick={() => {}} />
403
+ <DropdownMenuItem label="Cherry" onClick={() => {}} />
404
+ </DropdownMenuSubMenu>
405
+ </DropdownMenu>,
406
+ );
407
+ await user.click(screen.getByRole('button', {name: /Actions/}));
408
+ const trigger = screen.getByRole('menuitem', {
409
+ name: /Move to/,
410
+ hidden: true,
411
+ });
412
+ trigger.focus();
413
+ await user.keyboard('{ArrowRight}');
414
+ await waitFor(() => {
415
+ expect(
416
+ screen.getByRole('menuitem', {name: 'Apple', hidden: true}),
417
+ ).toHaveFocus();
418
+ });
419
+ await user.keyboard('c');
420
+ await waitFor(() => {
421
+ expect(
422
+ screen.getByRole('menuitem', {name: 'Cherry', hidden: true}),
423
+ ).toHaveFocus();
424
+ });
425
+ });
426
+
427
+ // The core of the inline-flyout fix: a submenu nested inside a submenu must
428
+ // not double-handle arrow keys (each level self-scopes via boundarySelector),
429
+ // so navigation in the deepest flyout steps one item at a time.
430
+ it('navigates a two-level nested submenu without double-stepping', async () => {
431
+ const user = userEvent.setup();
432
+ const onPick = vi.fn();
433
+ render(
434
+ <DropdownMenu button={{label: 'Share'}}>
435
+ <DropdownMenuItem label="Copy link" onClick={() => {}} />
436
+ <DropdownMenuSubMenu label="Share to">
437
+ <DropdownMenuItem label="Email" onClick={() => {}} />
438
+ <DropdownMenuSubMenu label="Team">
439
+ <DropdownMenuItem
440
+ label="Design"
441
+ onClick={() => {
442
+ onPick('design');
443
+ }}
444
+ />
445
+ <DropdownMenuItem
446
+ label="Eng"
447
+ onClick={() => {
448
+ onPick('eng');
449
+ }}
450
+ />
451
+ <DropdownMenuItem
452
+ label="Data"
453
+ onClick={() => {
454
+ onPick('data');
455
+ }}
456
+ />
457
+ </DropdownMenuSubMenu>
458
+ </DropdownMenuSubMenu>
459
+ </DropdownMenu>,
460
+ );
461
+ await user.click(screen.getByRole('button', {name: /Share/}));
462
+ const shareTo = screen.getByRole('menuitem', {
463
+ name: /Share to/,
464
+ hidden: true,
465
+ });
466
+ // Let the root menu's open-focus (Copy link, via rAF) settle first so it
467
+ // can't steal focus back after we move to the submenu trigger.
468
+ await waitFor(() => {
469
+ expect(
470
+ screen.getByRole('menuitem', {name: 'Copy link', hidden: true}),
471
+ ).toHaveFocus();
472
+ });
473
+ shareTo.focus();
474
+ await user.keyboard('{ArrowRight}');
475
+ // First flyout: focus on Email.
476
+ await waitFor(() => {
477
+ expect(
478
+ screen.getByRole('menuitem', {name: 'Email', hidden: true}),
479
+ ).toHaveFocus();
480
+ });
481
+ // Down to the nested submenu trigger, then open it.
482
+ await user.keyboard('{ArrowDown}');
483
+ const teamTrigger = screen.getByRole('menuitem', {
484
+ name: /Team/,
485
+ hidden: true,
486
+ });
487
+ await waitFor(() => {
488
+ expect(teamTrigger).toHaveFocus();
489
+ });
490
+ await user.keyboard('{ArrowRight}');
491
+ // Deepest flyout: Design → Eng → Data, one step per press (no skipping).
492
+ await waitFor(() => {
493
+ expect(
494
+ screen.getByRole('menuitem', {name: 'Design', hidden: true}),
495
+ ).toHaveFocus();
496
+ });
497
+ await user.keyboard('{ArrowDown}');
498
+ await waitFor(() => {
499
+ expect(
500
+ screen.getByRole('menuitem', {name: 'Eng', hidden: true}),
501
+ ).toHaveFocus();
502
+ });
503
+ await user.keyboard('{ArrowDown}');
504
+ await waitFor(() => {
505
+ expect(
506
+ screen.getByRole('menuitem', {name: 'Data', hidden: true}),
507
+ ).toHaveFocus();
508
+ });
509
+ await user.keyboard('{Enter}');
510
+ expect(onPick).toHaveBeenCalledWith('data');
511
+ });
512
+
513
+ // 1.4.13 Content on Hover or Focus: the flyout stays open while the pointer
514
+ // is over it (moving onto the flyout must not dismiss it).
515
+ it('keeps the flyout open while the pointer is over it', async () => {
516
+ const user = userEvent.setup();
517
+ render(<MoveMenu />);
518
+ await user.click(screen.getByRole('button', {name: /Actions/}));
519
+ const trigger = screen.getByRole('menuitem', {
520
+ name: /Move to/,
521
+ hidden: true,
522
+ });
523
+ await user.click(trigger);
524
+ const flyout = await screen.findByRole('menu', {
525
+ name: /Move to/,
526
+ hidden: true,
527
+ });
528
+ await user.hover(flyout);
529
+ await user.hover(
530
+ screen.getByRole('menuitem', {name: 'Folder A', hidden: true}),
531
+ );
532
+ // Still open after moving the pointer onto the flyout and its items.
533
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
534
+ });
535
+
536
+ // Async/loading submenu (hasSpinner): the flyout may contain no focusable
537
+ // items yet (only a disabled "Loading…" row). Opening it must still move
538
+ // keyboard ownership INTO the flyout — otherwise focus stays on the parent
539
+ // list, arrow keys rove the parent while the empty flyout stays open, and
540
+ // Enter re-triggers into a broken state.
541
+ it('moves focus into a loading (item-less) flyout so keyboard ownership transfers', async () => {
542
+ const user = userEvent.setup();
543
+ render(
544
+ <DropdownMenu button={{label: 'Actions'}}>
545
+ <DropdownMenuItem label="Rename" onClick={() => {}} />
546
+ <DropdownMenuSubMenu label="Move to" hasSpinner>
547
+ <DropdownMenuItem label="Loading…" isDisabled onClick={() => {}} />
548
+ </DropdownMenuSubMenu>
549
+ <DropdownMenuItem label="Delete" onClick={() => {}} />
550
+ </DropdownMenu>,
551
+ );
552
+ await user.click(screen.getByRole('button', {name: /Actions/}));
553
+ const trigger = screen.getByRole('menuitem', {
554
+ name: /Move to/,
555
+ hidden: true,
556
+ });
557
+ await waitFor(() => {
558
+ expect(
559
+ screen.getByRole('menuitem', {name: 'Rename', hidden: true}),
560
+ ).toHaveFocus();
561
+ });
562
+ trigger.focus();
563
+ await user.keyboard('{ArrowRight}');
564
+ await waitFor(() => {
565
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
566
+ });
567
+ // The flyout has no focusable items, so focus lands on the flyout container
568
+ // itself — NOT on a parent item (Rename/Delete).
569
+ const flyout = screen.getByRole('menu', {name: /Move to/, hidden: true});
570
+ await waitFor(() => {
571
+ expect(flyout).toHaveFocus();
572
+ });
573
+ expect(
574
+ screen.getByRole('menuitem', {name: 'Rename', hidden: true}),
575
+ ).not.toHaveFocus();
576
+ expect(
577
+ screen.getByRole('menuitem', {name: 'Delete', hidden: true}),
578
+ ).not.toHaveFocus();
579
+ // Left/Escape from the loading flyout closes it and returns to the trigger.
580
+ await user.keyboard('{ArrowLeft}');
581
+ await waitFor(() => {
582
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
583
+ });
584
+ expect(trigger).toHaveFocus();
585
+ });
586
+
587
+ it('roves to the first item once a loading flyout resolves', async () => {
588
+ // After children load, ArrowDown from the focused container moves onto the
589
+ // first real item (container reports index -1, so next = first).
590
+ const user = userEvent.setup();
591
+ function AsyncSubmenu() {
592
+ const [loaded, setLoaded] = useState(false);
593
+ return (
594
+ <DropdownMenu button={{label: 'Actions'}}>
595
+ <DropdownMenuSubMenu
596
+ label="Move to"
597
+ hasSpinner={!loaded}
598
+ onOpenChange={open => {
599
+ if (open) {
600
+ setLoaded(true);
601
+ }
602
+ }}>
603
+ {loaded ? (
604
+ <>
605
+ <DropdownMenuItem label="Folder A" onClick={() => {}} />
606
+ <DropdownMenuItem label="Folder B" onClick={() => {}} />
607
+ </>
608
+ ) : (
609
+ <DropdownMenuItem
610
+ label="Loading…"
611
+ isDisabled
612
+ onClick={() => {}}
613
+ />
614
+ )}
615
+ </DropdownMenuSubMenu>
616
+ </DropdownMenu>
617
+ );
618
+ }
619
+ render(<AsyncSubmenu />);
620
+ await user.click(screen.getByRole('button', {name: /Actions/}));
621
+ const trigger = screen.getByRole('menuitem', {
622
+ name: /Move to/,
623
+ hidden: true,
624
+ });
625
+ trigger.focus();
626
+ await user.keyboard('{ArrowRight}');
627
+ // Children resolve on open; ArrowDown moves from the container to Folder A.
628
+ await user.keyboard('{ArrowDown}');
629
+ await waitFor(() => {
630
+ expect(
631
+ screen.getByRole('menuitem', {name: 'Folder A', hidden: true}),
632
+ ).toHaveFocus();
633
+ });
634
+ });
635
+ });