@astryxdesign/core 0.1.8 → 0.1.9

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 (487) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/AppShell/AppShell.js +6 -2
  4. package/dist/AppShell/AppShellMobileContext.d.ts +8 -0
  5. package/dist/AppShell/AppShellMobileContext.d.ts.map +1 -1
  6. package/dist/Avatar/Avatar.d.ts +18 -2
  7. package/dist/Avatar/Avatar.d.ts.map +1 -1
  8. package/dist/Avatar/Avatar.js +63 -7
  9. package/dist/Avatar/AvatarStatusDot.d.ts +33 -5
  10. package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
  11. package/dist/Avatar/AvatarStatusDot.js +144 -19
  12. package/dist/Banner/Banner.d.ts +9 -1
  13. package/dist/Banner/Banner.d.ts.map +1 -1
  14. package/dist/Banner/Banner.js +31 -2
  15. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +31 -2
  16. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  17. package/dist/Breadcrumbs/BreadcrumbItem.js +208 -6
  18. package/dist/Breadcrumbs/index.d.ts +5 -0
  19. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  20. package/dist/Breadcrumbs/index.js +9 -1
  21. package/dist/Button/Button.d.ts +8 -2
  22. package/dist/Button/Button.d.ts.map +1 -1
  23. package/dist/Button/Button.js +31 -2
  24. package/dist/ButtonGroup/ButtonGroup.d.ts +9 -1
  25. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  26. package/dist/ButtonGroup/ButtonGroup.js +30 -1
  27. package/dist/Calendar/Calendar.d.ts.map +1 -1
  28. package/dist/Calendar/Calendar.js +30 -2
  29. package/dist/Calendar/dayCellUtils.js +1 -1
  30. package/dist/Card/Card.d.ts +8 -2
  31. package/dist/Card/Card.d.ts.map +1 -1
  32. package/dist/Card/Card.js +28 -2
  33. package/dist/Carousel/Carousel.d.ts +4 -1
  34. package/dist/Carousel/Carousel.d.ts.map +1 -1
  35. package/dist/Carousel/Carousel.js +24 -4
  36. package/dist/Chat/ChatComposer.d.ts +8 -0
  37. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  38. package/dist/Chat/ChatComposer.js +46 -7
  39. package/dist/Chat/ChatComposerDrawer.d.ts +2 -0
  40. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  41. package/dist/Chat/ChatComposerDrawer.js +11 -1
  42. package/dist/Chat/ChatComposerInput.d.ts +20 -3
  43. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  44. package/dist/Chat/ChatComposerInput.js +35 -3
  45. package/dist/Chat/ChatContext.d.ts +33 -0
  46. package/dist/Chat/ChatContext.d.ts.map +1 -1
  47. package/dist/Chat/ChatContext.js +16 -2
  48. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  49. package/dist/Chat/ChatDictationButton.js +1 -1
  50. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  51. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  52. package/dist/Chat/ChatLayoutScrollButton.js +4 -4
  53. package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
  54. package/dist/Chat/ChatSendButton.d.ts.map +1 -1
  55. package/dist/Chat/ChatSendButton.js +7 -1
  56. package/dist/Chat/ChatToolCalls.d.ts +5 -1
  57. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  58. package/dist/Chat/ChatToolCalls.js +18 -9
  59. package/dist/Chat/index.d.ts +2 -1
  60. package/dist/Chat/index.d.ts.map +1 -1
  61. package/dist/Chat/index.js +1 -1
  62. package/dist/Chat/useChatStreamScroll.d.ts +2 -1
  63. package/dist/Chat/useChatStreamScroll.d.ts.map +1 -1
  64. package/dist/Chat/useChatStreamScroll.js +22 -8
  65. package/dist/Chat/useSpeechRecognition.d.ts +0 -1
  66. package/dist/Chat/useSpeechRecognition.d.ts.map +1 -1
  67. package/dist/Chat/useSpeechRecognition.js +1 -1
  68. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  69. package/dist/CheckboxInput/CheckboxInput.js +1 -1
  70. package/dist/CheckboxList/CheckboxListItem.d.ts +20 -1
  71. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  72. package/dist/CheckboxList/CheckboxListItem.js +11 -1
  73. package/dist/ClickableCard/ClickableCard.d.ts +9 -2
  74. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  75. package/dist/ClickableCard/ClickableCard.js +5 -2
  76. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  77. package/dist/CodeBlock/CodeBlock.js +8 -1
  78. package/dist/CommandPalette/CommandPalette.d.ts +1 -1
  79. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  80. package/dist/CommandPalette/CommandPalette.js +3 -1
  81. package/dist/CommandPalette/CommandPaletteGroup.d.ts.map +1 -1
  82. package/dist/CommandPalette/CommandPaletteGroup.js +9 -2
  83. package/dist/CommandPalette/CommandPaletteInput.d.ts +7 -1
  84. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  85. package/dist/CommandPalette/CommandPaletteInput.js +6 -1
  86. package/dist/DateInput/DateInput.d.ts +47 -1
  87. package/dist/DateInput/DateInput.d.ts.map +1 -1
  88. package/dist/DateInput/DateInput.js +26 -2
  89. package/dist/DateInput/index.d.ts +1 -1
  90. package/dist/DateInput/index.d.ts.map +1 -1
  91. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  92. package/dist/DateTimeInput/DateTimeInput.js +10 -1
  93. package/dist/Dialog/Dialog.d.ts +3 -0
  94. package/dist/Dialog/Dialog.d.ts.map +1 -1
  95. package/dist/Dialog/Dialog.js +57 -6
  96. package/dist/Dialog/DialogContext.d.ts +7 -0
  97. package/dist/Dialog/DialogContext.d.ts.map +1 -1
  98. package/dist/Dialog/DialogContext.js +2 -2
  99. package/dist/Dialog/DialogHeader.d.ts +5 -2
  100. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  101. package/dist/Dialog/DialogHeader.js +6 -1
  102. package/dist/Divider/Divider.js +1 -1
  103. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -2
  104. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  105. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +1 -2
  106. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -2
  107. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  108. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +1 -2
  109. package/dist/Field/Field.d.ts.map +1 -1
  110. package/dist/Field/Field.js +2 -3
  111. package/dist/Field/FieldLabel.d.ts +1 -1
  112. package/dist/Field/FieldLabel.d.ts.map +1 -1
  113. package/dist/Field/FieldLabel.js +42 -15
  114. package/dist/FieldStatus/FieldStatus.d.ts +8 -0
  115. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  116. package/dist/FieldStatus/FieldStatus.js +22 -4
  117. package/dist/FileInput/FileInput.d.ts.map +1 -1
  118. package/dist/FileInput/FileInput.js +10 -2
  119. package/dist/HoverCard/HoverCard.d.ts +9 -1
  120. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  121. package/dist/HoverCard/HoverCard.js +2 -0
  122. package/dist/HoverCard/useHoverCard.d.ts +8 -0
  123. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  124. package/dist/HoverCard/useHoverCard.js +7 -2
  125. package/dist/Icon/Icon.d.ts +7 -2
  126. package/dist/Icon/Icon.d.ts.map +1 -1
  127. package/dist/Item/Item.d.ts +6 -1
  128. package/dist/Item/Item.d.ts.map +1 -1
  129. package/dist/Item/Item.js +22 -1
  130. package/dist/Layer/useLayer.d.ts +6 -0
  131. package/dist/Layer/useLayer.d.ts.map +1 -1
  132. package/dist/Layer/useLayer.js +2 -0
  133. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  134. package/dist/MobileNav/MobileNav.js +6 -1
  135. package/dist/MobileNav/MobileNavToggle.d.ts.map +1 -1
  136. package/dist/MobileNav/MobileNavToggle.js +4 -0
  137. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  138. package/dist/MultiSelector/MultiSelector.js +39 -12
  139. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  140. package/dist/MultiSelector/hooks.js +16 -0
  141. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  142. package/dist/Outline/Outline.d.ts +0 -1
  143. package/dist/Outline/Outline.d.ts.map +1 -1
  144. package/dist/Outline/Outline.js +0 -1
  145. package/dist/OverflowList/OverflowList.d.ts +20 -3
  146. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  147. package/dist/OverflowList/OverflowList.js +29 -4
  148. package/dist/Popover/Popover.d.ts.map +1 -1
  149. package/dist/Popover/Popover.js +3 -2
  150. package/dist/Popover/usePopover.d.ts.map +1 -1
  151. package/dist/Popover/usePopover.js +3 -9
  152. package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
  153. package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
  154. package/dist/SelectableCard/SelectableCard.d.ts +9 -2
  155. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  156. package/dist/SelectableCard/SelectableCard.js +16 -13
  157. package/dist/Selector/Selector.d.ts.map +1 -1
  158. package/dist/Selector/Selector.js +4 -2
  159. package/dist/Selector/hooks.d.ts.map +1 -1
  160. package/dist/Selector/hooks.js +16 -0
  161. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  162. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  163. package/dist/SideNav/SideNavHeading.js +21 -8
  164. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  165. package/dist/Slider/Slider.d.ts.map +1 -1
  166. package/dist/Slider/Slider.js +19 -2
  167. package/dist/Stack/stack.stylex.d.ts +9 -9
  168. package/dist/Table/BaseTable.d.ts.map +1 -1
  169. package/dist/Table/BaseTable.js +3 -2
  170. package/dist/Table/index.d.ts +2 -0
  171. package/dist/Table/index.d.ts.map +1 -1
  172. package/dist/Table/index.js +1 -0
  173. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  174. package/dist/Table/plugins/rowStatus/index.d.ts +3 -0
  175. package/dist/Table/plugins/rowStatus/index.d.ts.map +1 -0
  176. package/dist/Table/plugins/rowStatus/index.js +3 -0
  177. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts +60 -0
  178. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -0
  179. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +145 -0
  180. package/dist/Table/plugins/selection/useTableSelection.d.ts +13 -0
  181. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  182. package/dist/Table/plugins/selection/useTableSelection.js +4 -1
  183. package/dist/Table/plugins/tree/useTableTreeData.d.ts +16 -0
  184. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  185. package/dist/Table/plugins/tree/useTableTreeData.js +87 -0
  186. package/dist/Table/plugins/tree/useTableTreeState.d.ts +6 -0
  187. package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -1
  188. package/dist/Table/plugins/tree/useTableTreeState.js +22 -1
  189. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  190. package/dist/Table/useBaseTablePlugins.js +5 -3
  191. package/dist/Thumbnail/Thumbnail.d.ts +26 -5
  192. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  193. package/dist/Thumbnail/Thumbnail.js +97 -54
  194. package/dist/Thumbnail/thumbnail.markers.stylex.d.ts +9 -0
  195. package/dist/Thumbnail/thumbnail.markers.stylex.d.ts.map +1 -0
  196. package/dist/Thumbnail/thumbnail.markers.stylex.js +14 -0
  197. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  198. package/dist/TimeInput/TimeInput.js +15 -4
  199. package/dist/Timestamp/Timestamp.d.ts +3 -1
  200. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  201. package/dist/Timestamp/Timestamp.js +8 -6
  202. package/dist/Toast/useToast.d.ts.map +1 -1
  203. package/dist/Toast/useToast.js +2 -5
  204. package/dist/ToggleButton/ToggleButtonGroup.d.ts +0 -1
  205. package/dist/ToggleButton/ToggleButtonGroup.d.ts.map +1 -1
  206. package/dist/ToggleButton/ToggleButtonGroup.js +1 -1
  207. package/dist/Tokenizer/Tokenizer.d.ts +1 -1
  208. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  209. package/dist/Tokenizer/Tokenizer.js +16 -4
  210. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  211. package/dist/TopNav/TopNavHeading.js +18 -8
  212. package/dist/TopNav/TopNavMegaMenu.js +10 -4
  213. package/dist/TopNav/TopNavMegaMenuFeaturedCard.d.ts +9 -1
  214. package/dist/TopNav/TopNavMegaMenuFeaturedCard.d.ts.map +1 -1
  215. package/dist/TopNav/TopNavMegaMenuFeaturedCard.js +8 -1
  216. package/dist/TopNav/TopNavMenu.d.ts +2 -1
  217. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  218. package/dist/TopNav/TopNavMenu.js +68 -6
  219. package/dist/TreeList/TreeList.d.ts +12 -3
  220. package/dist/TreeList/TreeList.d.ts.map +1 -1
  221. package/dist/TreeList/TreeList.js +2 -0
  222. package/dist/TreeList/TreeListBranches.d.ts +5 -0
  223. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  224. package/dist/TreeList/TreeListBranches.js +10 -4
  225. package/dist/TreeList/TreeListItem.d.ts +8 -2
  226. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  227. package/dist/TreeList/TreeListItem.js +27 -7
  228. package/dist/TreeList/TreeListTypes.d.ts +25 -1
  229. package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
  230. package/dist/TreeList/index.d.ts +2 -2
  231. package/dist/TreeList/index.d.ts.map +1 -1
  232. package/dist/astryx.css +28 -18
  233. package/dist/astryx.umd.js +51 -51
  234. package/dist/astryx.umd.js.map +4 -4
  235. package/dist/authoring/doc.d.ts.map +1 -1
  236. package/dist/docs-types.d.ts +8 -0
  237. package/dist/docs-types.d.ts.map +1 -1
  238. package/dist/hooks/computeOverflow.d.ts +47 -0
  239. package/dist/hooks/computeOverflow.d.ts.map +1 -0
  240. package/dist/hooks/computeOverflow.js +213 -0
  241. package/dist/hooks/index.d.ts +1 -0
  242. package/dist/hooks/index.d.ts.map +1 -1
  243. package/dist/hooks/index.js +2 -1
  244. package/dist/hooks/useDevWarning.d.ts +18 -0
  245. package/dist/hooks/useDevWarning.d.ts.map +1 -0
  246. package/dist/hooks/useDevWarning.js +49 -0
  247. package/dist/hooks/useOverflow.d.ts +19 -0
  248. package/dist/hooks/useOverflow.d.ts.map +1 -1
  249. package/dist/hooks/useOverflow.js +36 -25
  250. package/dist/i18n/resolve.d.ts.map +1 -1
  251. package/dist/i18n/resolve.js +7 -18
  252. package/dist/theme/Theme.d.ts.map +1 -1
  253. package/dist/theme/Theme.js +2 -7
  254. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  255. package/dist/theme/generateThemeRules.js +51 -0
  256. package/dist/theme/hct.d.ts +0 -1
  257. package/dist/theme/hct.d.ts.map +1 -1
  258. package/dist/theme/hct.js +1 -1
  259. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  260. package/dist/theme/syntax/defineSyntaxTheme.js +2 -1
  261. package/dist/utils/devWarning.d.ts +42 -0
  262. package/dist/utils/devWarning.d.ts.map +1 -0
  263. package/dist/utils/devWarning.js +88 -0
  264. package/dist/utils/index.d.ts +3 -1
  265. package/dist/utils/index.d.ts.map +1 -1
  266. package/dist/utils/index.js +3 -2
  267. package/dist/utils/plainDate.d.ts +30 -0
  268. package/dist/utils/plainDate.d.ts.map +1 -1
  269. package/dist/utils/plainDate.js +53 -0
  270. package/dist/utils/types.d.ts +13 -0
  271. package/dist/utils/types.d.ts.map +1 -1
  272. package/locales/en.json +24 -0
  273. package/locales/fr-FR.json +14 -0
  274. package/locales/pseudo.json +18 -0
  275. package/package.json +1 -1
  276. package/src/AppShell/AppShell.tsx +6 -0
  277. package/src/AppShell/AppShellMobileContext.tsx +8 -0
  278. package/src/AppShell/useAppShellMobile.doc.mjs +8 -0
  279. package/src/Avatar/Avatar.doc.mjs +15 -3
  280. package/src/Avatar/Avatar.test.tsx +156 -1
  281. package/src/Avatar/Avatar.tsx +96 -4
  282. package/src/Avatar/AvatarStatusDot.doc.mjs +23 -12
  283. package/src/Avatar/AvatarStatusDot.test.tsx +284 -0
  284. package/src/Avatar/AvatarStatusDot.tsx +178 -21
  285. package/src/Banner/Banner.doc.mjs +9 -0
  286. package/src/Banner/Banner.test.tsx +30 -0
  287. package/src/Banner/Banner.tsx +47 -3
  288. package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +26 -0
  289. package/src/Breadcrumbs/BreadcrumbItem.tsx +333 -14
  290. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -0
  291. package/src/Breadcrumbs/Breadcrumbs.test.tsx +284 -2
  292. package/src/Breadcrumbs/index.ts +27 -0
  293. package/src/Button/Button.doc.mjs +9 -0
  294. package/src/Button/Button.test.tsx +49 -0
  295. package/src/Button/Button.tsx +25 -1
  296. package/src/ButtonGroup/ButtonGroup.doc.mjs +3 -0
  297. package/src/ButtonGroup/ButtonGroup.test.tsx +37 -0
  298. package/src/ButtonGroup/ButtonGroup.tsx +32 -0
  299. package/src/Calendar/Calendar.doc.mjs +12 -1
  300. package/src/Calendar/Calendar.test.tsx +193 -0
  301. package/src/Calendar/Calendar.tsx +38 -16
  302. package/src/Calendar/dayCellUtils.test.ts +5 -0
  303. package/src/Calendar/dayCellUtils.ts +1 -1
  304. package/src/Card/Card.doc.mjs +9 -0
  305. package/src/Card/Card.test.tsx +21 -3
  306. package/src/Card/Card.tsx +35 -2
  307. package/src/Carousel/Carousel.doc.mjs +1 -0
  308. package/src/Carousel/Carousel.test.tsx +53 -0
  309. package/src/Carousel/Carousel.tsx +34 -4
  310. package/src/Chat/ChatComposer.doc.mjs +12 -3
  311. package/src/Chat/ChatComposer.test.tsx +115 -0
  312. package/src/Chat/ChatComposer.tsx +73 -9
  313. package/src/Chat/ChatComposerDrawer.test.tsx +61 -0
  314. package/src/Chat/ChatComposerDrawer.tsx +11 -1
  315. package/src/Chat/ChatComposerInput.doc.mjs +7 -0
  316. package/src/Chat/ChatComposerInput.test.tsx +103 -0
  317. package/src/Chat/ChatComposerInput.tsx +49 -3
  318. package/src/Chat/ChatContext.tsx +37 -10
  319. package/src/Chat/ChatDictationButton.tsx +4 -1
  320. package/src/Chat/ChatLayout.tsx +3 -1
  321. package/src/Chat/ChatLayoutScrollButton.tsx +7 -2
  322. package/src/Chat/ChatMessageMetadata.tsx +1 -2
  323. package/src/Chat/ChatSendButton.tsx +11 -1
  324. package/src/Chat/ChatToolCalls.test.tsx +64 -0
  325. package/src/Chat/ChatToolCalls.tsx +25 -4
  326. package/src/Chat/index.ts +5 -1
  327. package/src/Chat/useChatStreamScroll.test.tsx +49 -0
  328. package/src/Chat/useChatStreamScroll.ts +27 -10
  329. package/src/Chat/useSpeechRecognition.ts +1 -1
  330. package/src/Chat/useTriggerMenu.tsx +3 -1
  331. package/src/CheckboxInput/CheckboxInput.test.tsx +32 -1
  332. package/src/CheckboxInput/CheckboxInput.tsx +1 -1
  333. package/src/CheckboxList/CheckboxList.test.tsx +81 -3
  334. package/src/CheckboxList/CheckboxListItem.doc.mjs +33 -5
  335. package/src/CheckboxList/CheckboxListItem.tsx +41 -5
  336. package/src/ClickableCard/ClickableCard.doc.mjs +2 -0
  337. package/src/ClickableCard/ClickableCard.test.tsx +34 -0
  338. package/src/ClickableCard/ClickableCard.tsx +13 -3
  339. package/src/CodeBlock/CodeBlock.test.tsx +56 -0
  340. package/src/CodeBlock/CodeBlock.tsx +9 -1
  341. package/src/CommandPalette/CommandPalette.doc.mjs +1 -0
  342. package/src/CommandPalette/CommandPalette.tsx +3 -1
  343. package/src/CommandPalette/CommandPaletteGroup.test.tsx +71 -0
  344. package/src/CommandPalette/CommandPaletteGroup.tsx +12 -1
  345. package/src/CommandPalette/CommandPaletteInput.doc.mjs +26 -9
  346. package/src/CommandPalette/CommandPaletteInput.test.tsx +34 -0
  347. package/src/CommandPalette/CommandPaletteInput.tsx +11 -0
  348. package/src/ContextMenu/ContextMenu.test.tsx +193 -0
  349. package/src/DateInput/DateInput.doc.mjs +16 -0
  350. package/src/DateInput/DateInput.test.tsx +108 -0
  351. package/src/DateInput/DateInput.tsx +68 -3
  352. package/src/DateInput/index.ts +1 -0
  353. package/src/DateTimeInput/DateTimeInput.test.tsx +35 -0
  354. package/src/DateTimeInput/DateTimeInput.tsx +14 -2
  355. package/src/Dialog/Dialog.doc.mjs +2 -2
  356. package/src/Dialog/Dialog.test.tsx +77 -4
  357. package/src/Dialog/Dialog.tsx +89 -5
  358. package/src/Dialog/DialogContext.ts +9 -2
  359. package/src/Dialog/DialogHeader.doc.mjs +2 -2
  360. package/src/Dialog/DialogHeader.tsx +9 -2
  361. package/src/Divider/Divider.tsx +2 -2
  362. package/src/DropdownMenu/DropdownMenu.test.tsx +169 -0
  363. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -2
  364. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +1 -2
  365. package/src/Field/Field.test.tsx +57 -12
  366. package/src/Field/Field.tsx +6 -5
  367. package/src/Field/FieldLabel.tsx +8 -0
  368. package/src/FieldStatus/FieldStatus.doc.mjs +17 -5
  369. package/src/FieldStatus/FieldStatus.test.tsx +78 -40
  370. package/src/FieldStatus/FieldStatus.tsx +22 -4
  371. package/src/FileInput/FileInput.test.tsx +52 -3
  372. package/src/FileInput/FileInput.tsx +13 -2
  373. package/src/HoverCard/HoverCard.doc.mjs +13 -0
  374. package/src/HoverCard/HoverCard.test.tsx +21 -2
  375. package/src/HoverCard/HoverCard.tsx +11 -0
  376. package/src/HoverCard/useHoverCard.doc.mjs +2 -0
  377. package/src/HoverCard/useHoverCard.tsx +24 -2
  378. package/src/Icon/Icon.doc.mjs +3 -2
  379. package/src/Icon/Icon.tsx +7 -2
  380. package/src/IconButton/IconButton.doc.mjs +8 -0
  381. package/src/IconButton/IconButton.test.tsx +15 -18
  382. package/src/Item/Item.test.tsx +40 -18
  383. package/src/Item/Item.tsx +38 -2
  384. package/src/Layer/useLayer.tsx +8 -0
  385. package/src/List/List.test.tsx +9 -8
  386. package/src/MobileNav/MobileNav.tsx +12 -1
  387. package/src/MobileNav/MobileNavToggle.test.tsx +116 -0
  388. package/src/MobileNav/MobileNavToggle.tsx +9 -1
  389. package/src/MultiSelector/MultiSelector.doc.mjs +4 -2
  390. package/src/MultiSelector/MultiSelector.test.tsx +172 -3
  391. package/src/MultiSelector/MultiSelector.tsx +55 -12
  392. package/src/MultiSelector/hooks.ts +17 -0
  393. package/src/NumberInput/NumberInput.test.tsx +11 -7
  394. package/src/NumberInput/NumberInput.tsx +1 -2
  395. package/src/Outline/Outline.tsx +0 -1
  396. package/src/OverflowList/OverflowList.doc.mjs +39 -6
  397. package/src/OverflowList/OverflowList.test.tsx +111 -0
  398. package/src/OverflowList/OverflowList.tsx +48 -8
  399. package/src/Popover/Popover.tsx +7 -4
  400. package/src/Popover/usePopover.tsx +9 -13
  401. package/src/PowerSearch/PowerSearch.doc.mjs +14 -0
  402. package/src/PowerSearch/PowerSearch.test.tsx +20 -0
  403. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +67 -0
  404. package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
  405. package/src/PowerSearch/usePowerSearchSource.ts +1 -2
  406. package/src/SelectableCard/SelectableCard.doc.mjs +2 -0
  407. package/src/SelectableCard/SelectableCard.test.tsx +46 -4
  408. package/src/SelectableCard/SelectableCard.tsx +29 -16
  409. package/src/Selector/Selector.test.tsx +69 -0
  410. package/src/Selector/Selector.tsx +5 -1
  411. package/src/Selector/hooks.ts +17 -0
  412. package/src/SideNav/SideNav.test.tsx +119 -3
  413. package/src/SideNav/SideNavCollapseButton.tsx +6 -1
  414. package/src/SideNav/SideNavHeading.tsx +24 -8
  415. package/src/SideNav/SideNavItem.tsx +5 -1
  416. package/src/Slider/Slider.test.tsx +75 -2
  417. package/src/Slider/Slider.tsx +21 -5
  418. package/src/Switch/Switch.test.tsx +32 -1
  419. package/src/Table/BaseTable.tsx +3 -5
  420. package/src/Table/index.ts +5 -0
  421. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +6 -2
  422. package/src/Table/plugins/rowStatus/index.ts +7 -0
  423. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +138 -0
  424. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +189 -0
  425. package/src/Table/plugins/selection/useTableSelection.test.tsx +16 -0
  426. package/src/Table/plugins/selection/useTableSelection.tsx +19 -1
  427. package/src/Table/plugins/tree/useTableTreeData.test.tsx +135 -3
  428. package/src/Table/plugins/tree/useTableTreeData.tsx +128 -1
  429. package/src/Table/plugins/tree/useTableTreeState.test.tsx +104 -0
  430. package/src/Table/plugins/tree/useTableTreeState.tsx +46 -2
  431. package/src/Table/useBaseTablePlugins.ts +10 -6
  432. package/src/Table/useTableRowStatus.doc.mjs +30 -0
  433. package/src/Table/useTableSelection.doc.mjs +65 -14
  434. package/src/Table/useTableTreeData.doc.mjs +29 -0
  435. package/src/Table/useTableTreeState.doc.mjs +2 -2
  436. package/src/TextArea/TextArea.test.tsx +9 -1
  437. package/src/Thumbnail/Thumbnail.doc.mjs +19 -10
  438. package/src/Thumbnail/Thumbnail.test.tsx +98 -7
  439. package/src/Thumbnail/Thumbnail.tsx +141 -41
  440. package/src/Thumbnail/thumbnail.markers.stylex.ts +12 -0
  441. package/src/TimeInput/TimeInput.test.tsx +76 -1
  442. package/src/TimeInput/TimeInput.tsx +19 -6
  443. package/src/Timestamp/Timestamp.doc.mjs +4 -4
  444. package/src/Timestamp/Timestamp.test.tsx +32 -0
  445. package/src/Timestamp/Timestamp.tsx +28 -8
  446. package/src/Toast/useToast.tsx +7 -8
  447. package/src/ToggleButton/ToggleButtonGroup.tsx +1 -1
  448. package/src/Tokenizer/Tokenizer.doc.mjs +15 -1
  449. package/src/Tokenizer/Tokenizer.test.tsx +183 -2
  450. package/src/Tokenizer/Tokenizer.tsx +16 -4
  451. package/src/TopNav/TopNav.test.tsx +125 -1
  452. package/src/TopNav/TopNavHeading.tsx +24 -8
  453. package/src/TopNav/TopNavMegaMenu.test.tsx +149 -1
  454. package/src/TopNav/TopNavMegaMenu.tsx +10 -4
  455. package/src/TopNav/TopNavMegaMenuFeaturedCard.doc.mjs +3 -3
  456. package/src/TopNav/TopNavMegaMenuFeaturedCard.tsx +19 -2
  457. package/src/TopNav/TopNavMenu.test.tsx +118 -4
  458. package/src/TopNav/TopNavMenu.tsx +83 -5
  459. package/src/TreeList/TreeList.doc.mjs +22 -0
  460. package/src/TreeList/TreeList.test.tsx +232 -0
  461. package/src/TreeList/TreeList.tsx +18 -2
  462. package/src/TreeList/TreeListBranches.tsx +16 -2
  463. package/src/TreeList/TreeListItem.tsx +51 -11
  464. package/src/TreeList/TreeListTypes.ts +27 -1
  465. package/src/TreeList/index.ts +2 -2
  466. package/src/authoring/doc.ts +1 -3
  467. package/src/docs-types.ts +5 -0
  468. package/src/hooks/computeOverflow.test.ts +422 -0
  469. package/src/hooks/computeOverflow.ts +290 -0
  470. package/src/hooks/index.ts +2 -0
  471. package/src/hooks/useDevWarning.test.tsx +45 -0
  472. package/src/hooks/useDevWarning.ts +54 -0
  473. package/src/hooks/useOverflow.test.ts +96 -4
  474. package/src/hooks/useOverflow.ts +74 -33
  475. package/src/i18n/InternationalizationProvider.doc.mjs +99 -2
  476. package/src/i18n/resolve.ts +9 -21
  477. package/src/theme/Theme.tsx +11 -14
  478. package/src/theme/generateThemeRules.test.ts +65 -0
  479. package/src/theme/generateThemeRules.ts +56 -0
  480. package/src/theme/hct.ts +1 -1
  481. package/src/theme/syntax/defineSyntaxTheme.ts +5 -6
  482. package/src/utils/devWarning.test.ts +56 -0
  483. package/src/utils/devWarning.ts +102 -0
  484. package/src/utils/index.ts +6 -0
  485. package/src/utils/plainDate.test.ts +22 -0
  486. package/src/utils/plainDate.ts +61 -0
  487. package/src/utils/types.ts +14 -0
@@ -332,6 +332,10 @@ export function TopNavHeading({
332
332
 
333
333
  const popover = usePopover({
334
334
  dialogLabel: t('@astryx.topNav.heading.dialogLabel'),
335
+ // The popup exposes its own role="menu" semantics; a role="dialog"
336
+ // aria-modal wrapper would announce "dialog, Navigation menu" around a
337
+ // menu (the anti-pattern removed in a478a3dcf).
338
+ role: 'none',
335
339
  hasCloseButton: false,
336
340
  });
337
341
 
@@ -511,13 +515,19 @@ export function TopNavHeading({
511
515
  {popover.render(
512
516
  <div
513
517
  ref={menuRef}
514
- role="menu"
515
518
  {...stylex.props(styles.popoverContent)}
516
519
  {...contentProps}>
517
520
  {popoverHeadingContent}
518
- <NavHeadingCloseContext value={closeMenuCtx}>
519
- {menu}
520
- </NavHeadingCloseContext>
521
+ {/* The menu role is scoped to the actual menu items so the
522
+ heading button above stays a valid sibling, not an invalid
523
+ child of a role="menu" element. */}
524
+ <div
525
+ role="menu"
526
+ aria-label={heading ?? t('@astryx.topNav.heading.dialogLabel')}>
527
+ <NavHeadingCloseContext value={closeMenuCtx}>
528
+ {menu}
529
+ </NavHeadingCloseContext>
530
+ </div>
521
531
  </div>,
522
532
  {
523
533
  placement: 'below',
@@ -575,13 +585,19 @@ export function TopNavHeading({
575
585
  {popover.render(
576
586
  <div
577
587
  ref={menuRef}
578
- role="menu"
579
588
  {...stylex.props(styles.popoverContent)}
580
589
  {...contentProps}>
581
590
  {popoverHeadingContent}
582
- <NavHeadingCloseContext value={closeMenuCtx}>
583
- {menu}
584
- </NavHeadingCloseContext>
591
+ {/* The menu role is scoped to the actual menu items so the
592
+ heading button above stays a valid sibling, not an invalid
593
+ child of a role="menu" element. */}
594
+ <div
595
+ role="menu"
596
+ aria-label={heading ?? t('@astryx.topNav.heading.dialogLabel')}>
597
+ <NavHeadingCloseContext value={closeMenuCtx}>
598
+ {menu}
599
+ </NavHeadingCloseContext>
600
+ </div>
585
601
  </div>,
586
602
  {
587
603
  placement: 'below',
@@ -9,7 +9,7 @@
9
9
  * SYNC: When TopNavMegaMenu changes, update tests to match new behavior
10
10
  */
11
11
 
12
- import {describe, it, expect, vi} from 'vitest';
12
+ import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
13
13
  import {render, screen} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {TopNavMegaMenu} from './TopNavMegaMenu';
@@ -75,6 +75,154 @@ describe('TopNavMegaMenu — default mode', () => {
75
75
  });
76
76
  });
77
77
 
78
+ // =============================================================================
79
+ // Popup semantics (default mode)
80
+ // =============================================================================
81
+
82
+ describe('TopNavMegaMenu — popup semantics', () => {
83
+ // Mock the Popover API (not implemented in jsdom).
84
+ const originalMatches = HTMLElement.prototype.matches;
85
+
86
+ beforeAll(() => {
87
+ HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
88
+ this.setAttribute('popover-open', '');
89
+ const event = new Event('toggle', {bubbles: false});
90
+ Object.defineProperty(event, 'newState', {value: 'open'});
91
+ this.dispatchEvent(event);
92
+ });
93
+ HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
94
+ this.removeAttribute('popover-open');
95
+ const event = new Event('toggle', {bubbles: false});
96
+ Object.defineProperty(event, 'newState', {value: 'closed'});
97
+ this.dispatchEvent(event);
98
+ });
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ (HTMLElement.prototype as any).matches = function (
101
+ selector: string,
102
+ ): boolean {
103
+ if (selector === ':popover-open') {
104
+ return this.hasAttribute('popover-open');
105
+ }
106
+ return originalMatches.call(this, selector);
107
+ };
108
+ });
109
+
110
+ afterAll(() => {
111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
+ (HTMLElement.prototype as any).matches = originalMatches;
113
+ });
114
+
115
+ it('trigger aria-controls resolves to the popup element', async () => {
116
+ const user = userEvent.setup();
117
+ render(
118
+ <TopNavMegaMenu
119
+ label="Products"
120
+ items={<TopNavMegaMenuItem title="Analytics" href="/analytics" />}
121
+ />,
122
+ );
123
+
124
+ const trigger = screen.getByRole('button', {name: 'Products'});
125
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
126
+
127
+ // aria-controls must be present and point at the real popup element.
128
+ const controlsId = trigger.getAttribute('aria-controls');
129
+ expect(controlsId).toBeTruthy();
130
+ const popup = document.getElementById(controlsId as string);
131
+ expect(popup).not.toBeNull();
132
+
133
+ await user.click(trigger);
134
+
135
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
136
+ // The referenced element is the popup that contains the panel content.
137
+ expect(popup).toContainElement(
138
+ screen.getByRole('group', {name: 'Products', hidden: true}),
139
+ );
140
+ });
141
+
142
+ it('does not wrap the panel in a role="dialog" aria-modal element', async () => {
143
+ const user = userEvent.setup();
144
+ render(
145
+ <TopNavMegaMenu
146
+ label="Products"
147
+ items={<TopNavMegaMenuItem title="Analytics" href="/analytics" />}
148
+ />,
149
+ );
150
+
151
+ await user.click(screen.getByRole('button', {name: 'Products'}));
152
+
153
+ // Focus stays on the trigger while the panel is open, so a modal dialog
154
+ // wrapper would tell assistive tech the focused control is inert.
155
+ expect(
156
+ screen.queryByRole('dialog', {hidden: true}),
157
+ ).not.toBeInTheDocument();
158
+ expect(document.querySelector('[aria-modal="true"]')).toBeNull();
159
+ });
160
+
161
+ it('does not expose role="menu" — a link grid is not an ARIA menu', async () => {
162
+ const user = userEvent.setup();
163
+ render(
164
+ <TopNavMegaMenu
165
+ label="Products"
166
+ items={
167
+ <>
168
+ <TopNavMegaMenuItem title="Analytics" href="/analytics" />
169
+ <TopNavMegaMenuItem title="Reports" href="/reports" />
170
+ </>
171
+ }
172
+ />,
173
+ );
174
+
175
+ await user.click(screen.getByRole('button', {name: 'Products'}));
176
+
177
+ // Per the WAI-ARIA APG, mega menu panels of navigation links must not use
178
+ // the menu role (reserved for action menus with menuitem children).
179
+ expect(screen.queryByRole('menu', {hidden: true})).not.toBeInTheDocument();
180
+ expect(screen.queryAllByRole('menuitem', {hidden: true})).toHaveLength(0);
181
+ });
182
+
183
+ it('exposes the panel as a labeled group', async () => {
184
+ const user = userEvent.setup();
185
+ render(
186
+ <TopNavMegaMenu
187
+ label="Products"
188
+ items={<TopNavMegaMenuItem title="Analytics" href="/analytics" />}
189
+ />,
190
+ );
191
+
192
+ await user.click(screen.getByRole('button', {name: 'Products'}));
193
+
194
+ expect(
195
+ screen.getByRole('group', {name: 'Products', hidden: true}),
196
+ ).toBeInTheDocument();
197
+ });
198
+
199
+ it('keeps item links with accessible names inside the panel', async () => {
200
+ const user = userEvent.setup();
201
+ render(
202
+ <TopNavMegaMenu
203
+ label="Products"
204
+ items={
205
+ <>
206
+ <TopNavMegaMenuItem title="Analytics" href="/analytics" />
207
+ <TopNavMegaMenuItem title="Reports" href="/reports" />
208
+ </>
209
+ }
210
+ />,
211
+ );
212
+
213
+ await user.click(screen.getByRole('button', {name: 'Products'}));
214
+
215
+ const analytics = screen.getByRole('link', {
216
+ name: /Analytics/,
217
+ hidden: true,
218
+ });
219
+ expect(analytics).toHaveAttribute('href', '/analytics');
220
+ expect(
221
+ screen.getByRole('link', {name: /Reports/, hidden: true}),
222
+ ).toHaveAttribute('href', '/reports');
223
+ });
224
+ });
225
+
78
226
  // =============================================================================
79
227
  // Mobile-bar mode — should be hidden
80
228
  // =============================================================================
@@ -354,7 +354,11 @@ function DefaultMegaMenu({
354
354
  }, [onOpenChange]);
355
355
 
356
356
  const popover = usePopover({
357
- dialogLabel: label,
357
+ // role: 'none' — the panel exposes its own role="group" labeled by
358
+ // `label`. Focus stays on the trigger while the panel is open, so a
359
+ // role="dialog" aria-modal="true" wrapper would announce an unnamed
360
+ // modal dialog around a grid of links.
361
+ role: 'none',
358
362
  // hasSurface: false — mega menu provides its own surface (panelContainer)
359
363
  // with border-top and custom overflow. Animation is applied via the
360
364
  // render() call's xstyle prop (panelAnimation), not the hook options.
@@ -434,8 +438,7 @@ function DefaultMegaMenu({
434
438
  <button
435
439
  ref={mergeRefs(triggerButtonRef, ref)}
436
440
  type="button"
437
- aria-haspopup="true"
438
- aria-expanded={popover.isOpen}
441
+ {...popover.triggerProps}
439
442
  onClick={handleClick}
440
443
  onMouseEnter={handleMouseEnter}
441
444
  onMouseLeave={handleMouseLeave}
@@ -454,7 +457,10 @@ function DefaultMegaMenu({
454
457
  </button>
455
458
  {popover.render(
456
459
  <div
457
- role="menu"
460
+ // role="group" — a mega menu is a browsing grid of links, not an
461
+ // ARIA menu of menuitems (per the WAI-ARIA APG, the menu role is
462
+ // for action menus; link mega menus are the documented anti-case).
463
+ role="group"
458
464
  aria-label={label}
459
465
  onMouseEnter={handleMouseEnter}
460
466
  onMouseLeave={handleMouseLeave}
@@ -27,7 +27,7 @@ export const docs = {
27
27
  {
28
28
  name: 'imageAlt',
29
29
  type: 'string',
30
- description: 'Alt text for the image.',
30
+ description: 'Alt text for the image. When omitted, the image is explicitly decorative (alt="", role="presentation", aria-hidden) — usually correct since the card already has a visible title. Provide it only when the image conveys information beyond the title and description.',
31
31
  },
32
32
  {
33
33
  name: 'linkLabel',
@@ -71,7 +71,7 @@ export const docsZh = {
71
71
  {
72
72
  name: 'imageAlt',
73
73
  type: 'string',
74
- description: '图片的替代文本。',
74
+ description: '图片的替代文本。省略时图片会被显式标记为装饰性(alt=""、role="presentation"、aria-hidden)——由于卡片已有可见标题,这通常是正确的。仅当图片传达标题和描述之外的信息时才提供。',
75
75
  },
76
76
  {
77
77
  name: 'linkLabel',
@@ -99,7 +99,7 @@ export const docsDense = {
99
99
  title: 'Card title.',
100
100
  description: 'Description below title.',
101
101
  image: 'Image URL above body.',
102
- imageAlt: 'Image alt text.',
102
+ imageAlt: 'Image alt text. Omitted → explicitly decorative (hidden from screen readers); usually correct since card has visible title.',
103
103
  linkLabel: 'CTA link text.',
104
104
  linkHref: 'CTA link URL.',
105
105
  children: 'Custom content below standard body.',
@@ -84,7 +84,15 @@ export interface TopNavMegaMenuFeaturedCardProps extends BaseProps<HTMLDivElemen
84
84
  description?: string;
85
85
  /** Optional image URL displayed above the body. */
86
86
  image?: string;
87
- /** Alt text for the image. */
87
+ /**
88
+ * Alt text for the image.
89
+ *
90
+ * When omitted, the image is explicitly decorative (`alt=""` +
91
+ * `role="presentation"` + `aria-hidden`, matching Avatar) and hidden from
92
+ * assistive technology. Since the card already has a visible `title`, a
93
+ * decorative image is usually correct; pass `imageAlt` only when the image
94
+ * conveys information beyond the title and description.
95
+ */
88
96
  imageAlt?: string;
89
97
  /** CTA link text. */
90
98
  linkLabel?: string;
@@ -149,7 +157,16 @@ export function TopNavMegaMenuFeaturedCard({
149
157
  )}
150
158
  {...rest}>
151
159
  {image && (
152
- <img src={image} alt={imageAlt ?? ''} {...stylex.props(styles.image)} />
160
+ // Without `imageAlt`, the image is explicitly decorative rather than
161
+ // silently empty-alt, matching Avatar's handling of unnamed images.
162
+ // Usually correct here: the card already has a visible title.
163
+ <img
164
+ src={image}
165
+ alt={imageAlt ?? ''}
166
+ role={imageAlt ? undefined : 'presentation'}
167
+ aria-hidden={imageAlt ? undefined : true}
168
+ {...stylex.props(styles.image)}
169
+ />
153
170
  )}
154
171
  <div {...stylex.props(styles.body)}>
155
172
  <span {...stylex.props(styles.title)}>{title}</span>
@@ -9,8 +9,9 @@
9
9
  * SYNC: When TopNavMenu changes, update tests to match new behavior
10
10
  */
11
11
 
12
- import {describe, it, expect} from 'vitest';
13
- import {render, screen} from '@testing-library/react';
12
+ import {describe, it, expect, vi} from 'vitest';
13
+ import {render, screen, fireEvent} from '@testing-library/react';
14
+ import userEvent from '@testing-library/user-event';
14
15
  import {TopNavMenu} from './TopNavMenu';
15
16
 
16
17
  const mockItems = [
@@ -32,10 +33,13 @@ describe('TopNavMenu', () => {
32
33
  expect(screen.getByRole('button', {name: 'Products'})).toBeInTheDocument();
33
34
  });
34
35
 
35
- it('trigger has aria-haspopup attribute', () => {
36
+ it('trigger announces a menu popup, not a dialog', () => {
36
37
  render(<TopNavMenu label="Products" items={mockItems} />);
37
38
  const trigger = screen.getByRole('button', {name: 'Products'});
38
- expect(trigger).toHaveAttribute('aria-haspopup', 'dialog');
39
+ // usePopover with role:'none' emits aria-haspopup="true" (the ARIA
40
+ // synonym for "menu") because the exposed semantics of the popup are its
41
+ // child role="menu", not a dialog.
42
+ expect(trigger).toHaveAttribute('aria-haspopup', 'true');
39
43
  });
40
44
 
41
45
  it('renders with custom items', () => {
@@ -57,3 +61,113 @@ describe('TopNavMenu', () => {
57
61
  expect(screen.getByRole('button', {name: 'Menu'})).toBeInTheDocument();
58
62
  });
59
63
  });
64
+
65
+ describe('menu semantics (APG)', () => {
66
+ it('does not wrap the popup in a role="dialog" / aria-modal shell', () => {
67
+ render(<TopNavMenu label="Products" items={mockItems} />);
68
+ // The popup's exposed semantics are the role="menu" container itself —
69
+ // no dialog wrapper, no aria-modal.
70
+ expect(
71
+ screen.queryByRole('dialog', {hidden: true}),
72
+ ).not.toBeInTheDocument();
73
+ expect(document.querySelector('[aria-modal]')).toBeNull();
74
+ expect(
75
+ screen.getByRole('menu', {name: 'Products', hidden: true}),
76
+ ).toBeInTheDocument();
77
+ });
78
+ });
79
+
80
+ describe('keyboard navigation (APG menu pattern)', () => {
81
+ it('exposes exactly one tab stop among menu items (roving tabindex)', async () => {
82
+ const user = userEvent.setup();
83
+ render(<TopNavMenu label="Products" items={mockItems} />);
84
+ await user.click(screen.getByRole('button', {name: 'Products'}));
85
+
86
+ const items = screen.getAllByRole('menuitem', {hidden: true});
87
+ expect(items).toHaveLength(2);
88
+ const tabbable = items.filter(el => el.getAttribute('tabindex') === '0');
89
+ expect(tabbable).toHaveLength(1);
90
+ });
91
+
92
+ it('moves focus with ArrowDown/ArrowUp and the tab stop follows', async () => {
93
+ const user = userEvent.setup();
94
+ render(<TopNavMenu label="Products" items={mockItems} />);
95
+ await user.click(screen.getByRole('button', {name: 'Products'}));
96
+
97
+ const menu = screen.getByRole('menu', {hidden: true});
98
+ const items = screen.getAllByRole('menuitem', {hidden: true});
99
+ items[0].focus();
100
+
101
+ fireEvent.keyDown(menu, {key: 'ArrowDown'});
102
+ expect(items[1]).toHaveFocus();
103
+ expect(items[1]).toHaveAttribute('tabindex', '0');
104
+ expect(items[0]).toHaveAttribute('tabindex', '-1');
105
+
106
+ fireEvent.keyDown(menu, {key: 'ArrowUp'});
107
+ expect(items[0]).toHaveFocus();
108
+ expect(items[0]).toHaveAttribute('tabindex', '0');
109
+ expect(items[1]).toHaveAttribute('tabindex', '-1');
110
+ });
111
+
112
+ it('wraps focus at both ends', async () => {
113
+ const user = userEvent.setup();
114
+ render(<TopNavMenu label="Products" items={mockItems} />);
115
+ await user.click(screen.getByRole('button', {name: 'Products'}));
116
+
117
+ const menu = screen.getByRole('menu', {hidden: true});
118
+ const items = screen.getAllByRole('menuitem', {hidden: true});
119
+
120
+ items[1].focus();
121
+ fireEvent.keyDown(menu, {key: 'ArrowDown'});
122
+ expect(items[0]).toHaveFocus();
123
+
124
+ fireEvent.keyDown(menu, {key: 'ArrowUp'});
125
+ expect(items[1]).toHaveFocus();
126
+ });
127
+
128
+ it('typeahead moves focus to the item matching the typed character', async () => {
129
+ const user = userEvent.setup();
130
+ render(<TopNavMenu label="Products" items={mockItems} />);
131
+ await user.click(screen.getByRole('button', {name: 'Products'}));
132
+
133
+ const menu = screen.getByRole('menu', {hidden: true});
134
+ fireEvent.keyDown(menu, {key: 'm'});
135
+ expect(
136
+ screen.getByRole('menuitem', {name: /Messaging/, hidden: true}),
137
+ ).toHaveFocus();
138
+ });
139
+
140
+ it('activates a focused onClick-only item with Enter', async () => {
141
+ const user = userEvent.setup();
142
+ const onClick = vi.fn();
143
+ render(<TopNavMenu label="Menu" items={[{title: 'Action', onClick}]} />);
144
+ await user.click(screen.getByRole('button', {name: 'Menu'}));
145
+
146
+ const item = screen.getByRole('menuitem', {hidden: true});
147
+ item.focus();
148
+ fireEvent.keyDown(item, {key: 'Enter'});
149
+ expect(onClick).toHaveBeenCalledOnce();
150
+ });
151
+
152
+ it('still activates an item on click', async () => {
153
+ const user = userEvent.setup();
154
+ const onClick = vi.fn();
155
+ render(<TopNavMenu label="Menu" items={[{title: 'Action', onClick}]} />);
156
+ await user.click(screen.getByRole('button', {name: 'Menu'}));
157
+
158
+ await user.click(screen.getByRole('menuitem', {hidden: true}));
159
+ expect(onClick).toHaveBeenCalledOnce();
160
+ });
161
+
162
+ it('closes the menu with Escape', async () => {
163
+ const user = userEvent.setup();
164
+ render(<TopNavMenu label="Products" items={mockItems} />);
165
+ const trigger = screen.getByRole('button', {name: 'Products'});
166
+ await user.click(trigger);
167
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
168
+
169
+ const menu = screen.getByRole('menu', {hidden: true});
170
+ fireEvent.keyDown(menu, {key: 'Escape'});
171
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
172
+ });
173
+ });
@@ -4,7 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file TopNavMenu.tsx
7
- * @input Uses React, StyleX, useHoverCard, TopNavItem tokens
7
+ * @input Uses React, StyleX, usePopover, useMenuHover, useListFocus,
8
+ * useTypeahead, TopNavItem tokens
8
9
  * @output Exports TopNavMenu component and related types
9
10
  * @position Navigation item with hover-triggered overflow menu for TopNav
10
11
  *
@@ -15,10 +16,18 @@
15
16
  * - /packages/cli/templates/blocks/components/TopNav/ (showcase blocks)
16
17
  */
17
18
 
18
- import React, {useId, useRef, useState, type ReactNode} from 'react';
19
+ import React, {
20
+ useCallback,
21
+ useId,
22
+ useRef,
23
+ useState,
24
+ type ReactNode,
25
+ } from 'react';
19
26
  import * as stylex from '@stylexjs/stylex';
20
27
  import {usePopover} from '../Popover/usePopover';
21
28
  import {useMenuHover} from '../hooks/useMenuHover';
29
+ import {useListFocus} from '../hooks/useListFocus';
30
+ import {useTypeahead} from '../hooks/useTypeahead';
22
31
  import {getIcon} from '../Icon/globalIconRegistry';
23
32
  import {mergeProps, mergeRefs} from '../utils';
24
33
  import type {BaseProps} from '../BaseProps';
@@ -332,7 +341,10 @@ export function TopNavMenu({
332
341
  const triggerButtonRef = useRef<HTMLButtonElement | null>(null);
333
342
 
334
343
  const popover = usePopover({
335
- dialogLabel: label,
344
+ // The popup's own role="menu" is the exposed semantics; a modal dialog
345
+ // wrapper would announce an unnamed dialog around the menu and make the
346
+ // trigger claim aria-haspopup="dialog" for menu content (see TabMenu).
347
+ role: 'none',
336
348
  xstyle: styles.menuOffset,
337
349
  });
338
350
 
@@ -353,6 +365,65 @@ export function TopNavMenu({
353
365
  ref,
354
366
  );
355
367
 
368
+ // The desktop popup is a composite menu widget per the APG menu pattern:
369
+ // a single roving tab stop with ArrowUp/ArrowDown traversal (wrapping),
370
+ // Home/End, and first-character typeahead. The hook owns item tabindex —
371
+ // items render tabIndex={-1} and exactly one is promoted to 0. The
372
+ // composition mirrors NavHeadingMenu.
373
+ const {listRef, handleKeyDown, handleFocus, focusItem} =
374
+ useListFocus<HTMLDivElement>({
375
+ itemSelector: '[role="menuitem"]',
376
+ hasRovingTabIndex: true,
377
+ onEscape: popover.hide,
378
+ });
379
+
380
+ // First-character typeahead over the menu items (menus-11).
381
+ const getMenuItems = useCallback(
382
+ (): HTMLElement[] =>
383
+ listRef.current
384
+ ? Array.from(
385
+ listRef.current.querySelectorAll<HTMLElement>('[role="menuitem"]'),
386
+ )
387
+ : [],
388
+ [listRef],
389
+ );
390
+ const typeahead = useTypeahead({
391
+ getItemLabels: () => getMenuItems().map(el => el.textContent),
392
+ onMatch: focusItem,
393
+ getCurrentIndex: () =>
394
+ getMenuItems().findIndex(
395
+ el =>
396
+ el === document.activeElement || el.contains(document.activeElement),
397
+ ),
398
+ });
399
+
400
+ // Extend useListFocus with Enter/Space activation. Items rendered without an
401
+ // `href` are `<div role="menuitem">` elements, which have no native keyboard
402
+ // activation — without this, Enter/Space on a focused onClick-only item does
403
+ // nothing. Anchor items (with `href`) already activate on Enter natively.
404
+ const menuKeyDown = useCallback(
405
+ (e: React.KeyboardEvent) => {
406
+ if (e.key === 'Enter' || e.key === ' ') {
407
+ const focused = document.activeElement as HTMLElement | null;
408
+ if (focused?.getAttribute('role') === 'menuitem') {
409
+ e.preventDefault();
410
+ focused.click();
411
+ return;
412
+ }
413
+ }
414
+ if (typeahead.onKeyDown(e)) {
415
+ e.preventDefault();
416
+ return;
417
+ }
418
+ handleKeyDown(e);
419
+ },
420
+ [handleKeyDown, typeahead],
421
+ );
422
+
423
+ // Menu container carries both the hover hook's ref (for its open/close
424
+ // focus management) and the list-focus ref (for roving tabindex/typeahead).
425
+ const setMenuRef = mergeRefs<HTMLDivElement>(menuRef, listRef);
426
+
356
427
  // Mobile bar: hide menus entirely
357
428
  if (renderMode === 'mobile-bar') {
358
429
  return null;
@@ -437,10 +508,15 @@ export function TopNavMenu({
437
508
  </button>
438
509
  {popover.render(
439
510
  <div
440
- ref={menuRef}
511
+ ref={setMenuRef}
441
512
  role="menu"
442
513
  aria-label={label}
443
514
  {...contentProps}
515
+ // After the contentProps spread so the full APG composition (roving
516
+ // tabindex + typeahead + Enter/Space activation) replaces the hover
517
+ // hook's basic arrow-key handler.
518
+ onKeyDown={menuKeyDown}
519
+ onFocus={handleFocus}
444
520
  {...stylex.props(styles.menuContainer)}>
445
521
  {items.map(item => {
446
522
  const Element = item.href ? 'a' : 'div';
@@ -448,7 +524,9 @@ export function TopNavMenu({
448
524
  <Element
449
525
  key={getMenuItemKey(item)}
450
526
  role="menuitem"
451
- tabIndex={popover.isOpen ? 0 : -1}
527
+ // Single tab stop: useListFocus owns the roving tabindex and
528
+ // promotes exactly one item to 0.
529
+ tabIndex={-1}
452
530
  href={item.href}
453
531
  onClick={item.onClick}
454
532
  {...stylex.props(styles.menuItem)}>
@@ -26,6 +26,9 @@ export const docs = {
26
26
  targets: [
27
27
  {className: 'astryx-tree-list', visualProps: ['density']},
28
28
  {className: 'astryx-tree-list-item', visualProps: ['density'], states: ['selected', 'disabled']},
29
+ {className: 'astryx-tree-list-chevron', states: ['state']},
30
+ {className: 'astryx-tree-list-item-label', states: ['selected']},
31
+ {className: 'astryx-tree-list-guide'},
29
32
  ],
30
33
  },
31
34
  components: [
@@ -47,6 +50,13 @@ export const docs = {
47
50
  description: 'Spacing density for items.',
48
51
  default: "'balanced'",
49
52
  },
53
+ {
54
+ name: 'variant',
55
+ type: "'lineGuides' | 'noGuides'",
56
+ description:
57
+ 'Visual treatment of the hierarchy guide lines. lineGuides shows connector lines; noGuides hides them, keeping indentation. Orthogonal to density.',
58
+ default: "'lineGuides'",
59
+ },
50
60
  {
51
61
  name: 'header',
52
62
  type: 'ReactNode',
@@ -90,6 +100,9 @@ export const docsZh = {
90
100
  targets: [
91
101
  {className: 'astryx-tree-list', visualProps: ['density']},
92
102
  {className: 'astryx-tree-list-item', visualProps: ['density'], states: ['selected', 'disabled']},
103
+ {className: 'astryx-tree-list-chevron', states: ['state']},
104
+ {className: 'astryx-tree-list-item-label', states: ['selected']},
105
+ {className: 'astryx-tree-list-guide'},
93
106
  ],
94
107
  },
95
108
  components: [
@@ -112,6 +125,13 @@ export const docsZh = {
112
125
  description: '项目的间距密度。',
113
126
  default: "'balanced'",
114
127
  },
128
+ {
129
+ name: 'variant',
130
+ type: "'lineGuides' | 'noGuides'",
131
+ description:
132
+ '层级引导线的视觉呈现。lineGuides 显示连接线;noGuides 隐藏连接线并保留缩进。与 density 正交。',
133
+ default: "'lineGuides'",
134
+ },
115
135
  {
116
136
  name: 'header',
117
137
  type: 'ReactNode',
@@ -156,6 +176,7 @@ export const docsDense = {
156
176
  propDescriptions: {
157
177
  items: 'Recursive tree item data w/ id, label, optional children + isExpanded.',
158
178
  density: 'Spacing density for items.',
179
+ variant: 'Guide-line treatment: lineGuides shows connectors, noGuides hides them (indent kept). Orthogonal to density.',
159
180
  header: 'Header content, linked to tree via aria-labelledby.',
160
181
  xstyle: 'StyleX styles for layout. Must be stylex.create() value.',
161
182
  },
@@ -167,6 +188,7 @@ export const docsDense = {
167
188
  propDescriptions: {
168
189
  items: 'Recursive tree item data w/ id, label, optional children + isExpanded.',
169
190
  density: 'Spacing density for items.',
191
+ variant: 'Guide-line treatment: lineGuides shows connectors, noGuides hides them (indent kept). Orthogonal to density.',
170
192
  header: 'Header content, linked to tree via aria-labelledby.',
171
193
  xstyle: 'StyleX styles for layout. Must be stylex.create() value.',
172
194
  },