@astryxdesign/core 0.1.7 → 0.1.8-canary.0c4d8d7

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 (428) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +30 -19
  3. package/dist/AppShell/AppShell.d.ts.map +1 -1
  4. package/dist/AppShell/AppShell.js +6 -2
  5. package/dist/AppShell/AppShellMobileContext.d.ts +8 -0
  6. package/dist/AppShell/AppShellMobileContext.d.ts.map +1 -1
  7. package/dist/Avatar/Avatar.d.ts +10 -5
  8. package/dist/Avatar/Avatar.d.ts.map +1 -1
  9. package/dist/Avatar/Avatar.js +12 -8
  10. package/dist/Avatar/AvatarSizeContext.d.ts +1 -1
  11. package/dist/Avatar/AvatarSizeContext.js +1 -1
  12. package/dist/Avatar/AvatarStatusDot.d.ts +2 -2
  13. package/dist/Avatar/AvatarStatusDot.js +2 -2
  14. package/dist/AvatarGroup/AvatarGroup.d.ts +2 -2
  15. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  16. package/dist/AvatarGroup/AvatarGroup.js +2 -2
  17. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
  18. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  19. package/dist/AvatarGroup/AvatarGroupOverflow.js +16 -14
  20. package/dist/Banner/Banner.d.ts +9 -1
  21. package/dist/Banner/Banner.d.ts.map +1 -1
  22. package/dist/Banner/Banner.js +31 -2
  23. package/dist/Button/Button.d.ts +8 -2
  24. package/dist/Button/Button.d.ts.map +1 -1
  25. package/dist/Button/Button.js +31 -2
  26. package/dist/ButtonGroup/ButtonGroup.d.ts +9 -1
  27. package/dist/ButtonGroup/ButtonGroup.d.ts.map +1 -1
  28. package/dist/ButtonGroup/ButtonGroup.js +30 -1
  29. package/dist/Card/Card.d.ts +8 -2
  30. package/dist/Card/Card.d.ts.map +1 -1
  31. package/dist/Card/Card.js +28 -2
  32. package/dist/Carousel/Carousel.d.ts +6 -2
  33. package/dist/Carousel/Carousel.d.ts.map +1 -1
  34. package/dist/Carousel/Carousel.js +55 -5
  35. package/dist/Chat/ChatComposer.d.ts +8 -0
  36. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  37. package/dist/Chat/ChatComposer.js +31 -6
  38. package/dist/Chat/ChatComposerDrawer.d.ts +2 -0
  39. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  40. package/dist/Chat/ChatComposerDrawer.js +11 -1
  41. package/dist/Chat/ChatComposerInput.d.ts +20 -3
  42. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  43. package/dist/Chat/ChatComposerInput.js +19 -3
  44. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  45. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  46. package/dist/Chat/ChatMessage.d.ts +1 -1
  47. package/dist/Chat/ChatMessage.d.ts.map +1 -1
  48. package/dist/Chat/ChatMessage.js +1 -1
  49. package/dist/Chat/ChatMessageMetadata.d.ts.map +1 -1
  50. package/dist/Chat/ChatSendButton.d.ts.map +1 -1
  51. package/dist/Chat/ChatToolCalls.d.ts +5 -1
  52. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  53. package/dist/Chat/ChatToolCalls.js +12 -3
  54. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  55. package/dist/Citation/Citation.d.ts.map +1 -1
  56. package/dist/Citation/Citation.js +10 -4
  57. package/dist/ClickableCard/ClickableCard.d.ts +9 -2
  58. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  59. package/dist/ClickableCard/ClickableCard.js +3 -0
  60. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  61. package/dist/CodeBlock/CodeBlock.js +8 -1
  62. package/dist/Collapsible/Collapsible.d.ts +11 -1
  63. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  64. package/dist/Collapsible/Collapsible.js +36 -4
  65. package/dist/CommandPalette/CommandPaletteInput.d.ts +7 -1
  66. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  67. package/dist/CommandPalette/CommandPaletteInput.js +6 -1
  68. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  69. package/dist/ContextMenu/ContextMenu.js +4 -3
  70. package/dist/ContextMenu/index.d.ts +3 -0
  71. package/dist/ContextMenu/index.d.ts.map +1 -1
  72. package/dist/ContextMenu/index.js +7 -1
  73. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  74. package/dist/DateTimeInput/DateTimeInput.js +10 -1
  75. package/dist/Dialog/Dialog.d.ts.map +1 -1
  76. package/dist/Dialog/Dialog.js +2 -1
  77. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  78. package/dist/DropdownMenu/DropdownMenu.js +8 -4
  79. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +88 -0
  80. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -0
  81. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +169 -0
  82. package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -1
  83. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  84. package/dist/DropdownMenu/DropdownMenuContext.js +18 -0
  85. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +68 -0
  86. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -0
  87. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +69 -0
  88. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +69 -0
  89. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -0
  90. package/dist/DropdownMenu/DropdownMenuRadioItem.js +178 -0
  91. package/dist/DropdownMenu/index.d.ts +5 -1
  92. package/dist/DropdownMenu/index.d.ts.map +1 -1
  93. package/dist/DropdownMenu/index.js +10 -1
  94. package/dist/DropdownMenu/menuItemRoles.d.ts +13 -0
  95. package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -0
  96. package/dist/DropdownMenu/menuItemRoles.js +15 -0
  97. package/dist/FieldStatus/FieldStatus.d.ts +8 -0
  98. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  99. package/dist/FieldStatus/FieldStatus.js +22 -4
  100. package/dist/FileInput/FileInput.d.ts.map +1 -1
  101. package/dist/FileInput/FileInput.js +10 -2
  102. package/dist/HoverCard/HoverCard.d.ts +9 -1
  103. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  104. package/dist/HoverCard/HoverCard.js +2 -0
  105. package/dist/HoverCard/useHoverCard.d.ts +8 -0
  106. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  107. package/dist/HoverCard/useHoverCard.js +7 -2
  108. package/dist/Icon/Icon.d.ts +43 -13
  109. package/dist/Icon/Icon.d.ts.map +1 -1
  110. package/dist/Icon/Icon.js +62 -27
  111. package/dist/Layer/useLayer.d.ts +6 -0
  112. package/dist/Layer/useLayer.d.ts.map +1 -1
  113. package/dist/Layer/useLayer.js +2 -0
  114. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  115. package/dist/MobileNav/MobileNav.js +6 -1
  116. package/dist/MobileNav/MobileNavToggle.d.ts.map +1 -1
  117. package/dist/MobileNav/MobileNavToggle.js +4 -0
  118. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  119. package/dist/MultiSelector/MultiSelector.js +39 -12
  120. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  121. package/dist/MultiSelector/hooks.js +16 -0
  122. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  123. package/dist/Outline/Outline.d.ts +68 -3
  124. package/dist/Outline/Outline.d.ts.map +1 -1
  125. package/dist/Outline/Outline.js +128 -26
  126. package/dist/Outline/useScrollSpy.d.ts +27 -7
  127. package/dist/Outline/useScrollSpy.d.ts.map +1 -1
  128. package/dist/Outline/useScrollSpy.js +202 -70
  129. package/dist/OverflowList/OverflowList.d.ts +20 -3
  130. package/dist/OverflowList/OverflowList.d.ts.map +1 -1
  131. package/dist/OverflowList/OverflowList.js +29 -4
  132. package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
  133. package/dist/PowerSearch/usePowerSearchSource.d.ts.map +1 -1
  134. package/dist/SelectableCard/SelectableCard.d.ts +9 -2
  135. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  136. package/dist/SelectableCard/SelectableCard.js +14 -11
  137. package/dist/Selector/Selector.d.ts.map +1 -1
  138. package/dist/Selector/Selector.js +4 -2
  139. package/dist/Selector/hooks.d.ts.map +1 -1
  140. package/dist/Selector/hooks.js +16 -0
  141. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  142. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  143. package/dist/SideNav/SideNavHeading.js +21 -8
  144. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  145. package/dist/Slider/Slider.d.ts.map +1 -1
  146. package/dist/Slider/Slider.js +15 -1
  147. package/dist/TabList/Tab.d.ts.map +1 -1
  148. package/dist/TabList/Tab.js +2 -2
  149. package/dist/TabList/TabList.d.ts.map +1 -1
  150. package/dist/TabList/TabList.js +2 -0
  151. package/dist/TabList/TabMenu.d.ts.map +1 -1
  152. package/dist/TabList/TabMenu.js +1 -1
  153. package/dist/Table/Table.d.ts +1 -1
  154. package/dist/Table/Table.js +1 -1
  155. package/dist/Table/index.d.ts +4 -0
  156. package/dist/Table/index.d.ts.map +1 -1
  157. package/dist/Table/index.js +2 -0
  158. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  159. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  160. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +24 -2
  161. package/dist/Table/plugins/rowStatus/index.d.ts +3 -0
  162. package/dist/Table/plugins/rowStatus/index.d.ts.map +1 -0
  163. package/dist/Table/plugins/rowStatus/index.js +3 -0
  164. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts +60 -0
  165. package/dist/Table/plugins/rowStatus/useTableRowStatus.d.ts.map +1 -0
  166. package/dist/Table/plugins/rowStatus/useTableRowStatus.js +145 -0
  167. package/dist/Table/plugins/selection/useTableSelection.d.ts +12 -0
  168. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  169. package/dist/Table/plugins/selection/useTableSelection.js +4 -1
  170. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  171. package/dist/Table/plugins/sortable/useTableSortable.js +18 -7
  172. package/dist/Table/plugins/tree/index.d.ts +5 -0
  173. package/dist/Table/plugins/tree/index.d.ts.map +1 -0
  174. package/dist/Table/plugins/tree/index.js +4 -0
  175. package/dist/Table/plugins/tree/useTableTreeData.d.ts +38 -0
  176. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -0
  177. package/dist/Table/plugins/tree/useTableTreeData.js +358 -0
  178. package/dist/Table/plugins/tree/useTableTreeState.d.ts +51 -0
  179. package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -0
  180. package/dist/Table/plugins/tree/useTableTreeState.js +175 -0
  181. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  182. package/dist/Table/useBaseTablePlugins.js +6 -3
  183. package/dist/Thumbnail/Thumbnail.d.ts +8 -1
  184. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  185. package/dist/Thumbnail/Thumbnail.js +46 -8
  186. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  187. package/dist/TimeInput/TimeInput.js +15 -4
  188. package/dist/Tokenizer/Tokenizer.d.ts +1 -1
  189. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  190. package/dist/Tokenizer/Tokenizer.js +16 -4
  191. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  192. package/dist/Toolbar/Toolbar.js +8 -1
  193. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  194. package/dist/TopNav/TopNavHeading.js +18 -8
  195. package/dist/TopNav/TopNavMegaMenu.js +10 -4
  196. package/dist/TopNav/TopNavMenu.d.ts +2 -1
  197. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  198. package/dist/TopNav/TopNavMenu.js +68 -6
  199. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  200. package/dist/TreeList/TreeListItem.js +5 -14
  201. package/dist/astryx.css +51 -16
  202. package/dist/astryx.umd.js +56 -56
  203. package/dist/astryx.umd.js.map +4 -4
  204. package/dist/authoring/doc.d.ts.map +1 -1
  205. package/dist/hooks/computeOverflow.d.ts +47 -0
  206. package/dist/hooks/computeOverflow.d.ts.map +1 -0
  207. package/dist/hooks/computeOverflow.js +213 -0
  208. package/dist/hooks/useOverflow.d.ts +19 -0
  209. package/dist/hooks/useOverflow.d.ts.map +1 -1
  210. package/dist/hooks/useOverflow.js +37 -25
  211. package/dist/theme/Theme.js +1 -1
  212. package/dist/theme/defineTheme.d.ts +1 -1
  213. package/dist/theme/defineTheme.js +1 -1
  214. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  215. package/dist/theme/generateThemeRules.js +51 -0
  216. package/dist/theme/hct.d.ts +0 -1
  217. package/dist/theme/hct.d.ts.map +1 -1
  218. package/dist/theme/hct.js +0 -3
  219. package/dist/utils/types.d.ts +13 -0
  220. package/dist/utils/types.d.ts.map +1 -1
  221. package/docs.mjs +32 -461
  222. package/locales/en.json +212 -172
  223. package/locales/fr-FR.json +10 -0
  224. package/locales/pseudo.json +30 -0
  225. package/package.json +3 -1
  226. package/scripts/postinstall.mjs +93 -0
  227. package/src/AppShell/AppShell.tsx +6 -0
  228. package/src/AppShell/AppShellMobileContext.tsx +8 -0
  229. package/src/AppShell/useAppShellMobile.doc.mjs +8 -0
  230. package/src/Avatar/Avatar.doc.mjs +6 -6
  231. package/src/Avatar/Avatar.tsx +16 -11
  232. package/src/Avatar/AvatarSizeContext.ts +1 -1
  233. package/src/Avatar/AvatarStatusDot.tsx +2 -2
  234. package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
  235. package/src/AvatarGroup/AvatarGroup.test.tsx +17 -3
  236. package/src/AvatarGroup/AvatarGroup.tsx +3 -3
  237. package/src/AvatarGroup/AvatarGroupOverflow.doc.mjs +4 -4
  238. package/src/AvatarGroup/AvatarGroupOverflow.tsx +21 -5
  239. package/src/Banner/Banner.doc.mjs +9 -0
  240. package/src/Banner/Banner.test.tsx +30 -0
  241. package/src/Banner/Banner.tsx +47 -3
  242. package/src/Button/Button.doc.mjs +9 -0
  243. package/src/Button/Button.test.tsx +49 -0
  244. package/src/Button/Button.tsx +25 -1
  245. package/src/ButtonGroup/ButtonGroup.doc.mjs +3 -0
  246. package/src/ButtonGroup/ButtonGroup.test.tsx +37 -0
  247. package/src/ButtonGroup/ButtonGroup.tsx +32 -0
  248. package/src/Card/Card.doc.mjs +9 -0
  249. package/src/Card/Card.test.tsx +21 -3
  250. package/src/Card/Card.tsx +35 -2
  251. package/src/Carousel/Carousel.doc.mjs +1 -0
  252. package/src/Carousel/Carousel.test.tsx +149 -1
  253. package/src/Carousel/Carousel.tsx +62 -5
  254. package/src/Chat/Chat.doc.mjs +1 -1
  255. package/src/Chat/ChatComposer.doc.mjs +9 -0
  256. package/src/Chat/ChatComposer.test.tsx +35 -0
  257. package/src/Chat/ChatComposer.tsx +58 -9
  258. package/src/Chat/ChatComposerDrawer.test.tsx +61 -0
  259. package/src/Chat/ChatComposerDrawer.tsx +11 -1
  260. package/src/Chat/ChatComposerInput.doc.mjs +7 -0
  261. package/src/Chat/ChatComposerInput.test.tsx +87 -0
  262. package/src/Chat/ChatComposerInput.tsx +35 -3
  263. package/src/Chat/ChatLayout.tsx +3 -1
  264. package/src/Chat/ChatLayoutScrollButton.tsx +3 -1
  265. package/src/Chat/ChatMessage.tsx +4 -2
  266. package/src/Chat/ChatMessageMetadata.tsx +1 -2
  267. package/src/Chat/ChatSendButton.tsx +5 -1
  268. package/src/Chat/ChatToolCalls.test.tsx +64 -0
  269. package/src/Chat/ChatToolCalls.tsx +13 -1
  270. package/src/Chat/useTriggerMenu.tsx +3 -1
  271. package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
  272. package/src/CheckboxInput/CheckboxInput.test.tsx +32 -1
  273. package/src/CheckboxList/CheckboxList.test.tsx +9 -1
  274. package/src/Citation/Citation.test.tsx +51 -0
  275. package/src/Citation/Citation.tsx +8 -2
  276. package/src/ClickableCard/ClickableCard.doc.mjs +2 -0
  277. package/src/ClickableCard/ClickableCard.test.tsx +34 -0
  278. package/src/ClickableCard/ClickableCard.tsx +11 -1
  279. package/src/CodeBlock/CodeBlock.test.tsx +56 -0
  280. package/src/CodeBlock/CodeBlock.tsx +9 -1
  281. package/src/Collapsible/Collapsible.doc.mjs +7 -0
  282. package/src/Collapsible/Collapsible.test.tsx +80 -0
  283. package/src/Collapsible/Collapsible.tsx +58 -5
  284. package/src/CommandPalette/CommandPaletteInput.doc.mjs +26 -9
  285. package/src/CommandPalette/CommandPaletteInput.test.tsx +34 -0
  286. package/src/CommandPalette/CommandPaletteInput.tsx +11 -0
  287. package/src/ContextMenu/ContextMenu.test.tsx +75 -1
  288. package/src/ContextMenu/ContextMenu.tsx +10 -5
  289. package/src/ContextMenu/index.ts +15 -0
  290. package/src/DateTimeInput/DateTimeInput.test.tsx +35 -0
  291. package/src/DateTimeInput/DateTimeInput.tsx +14 -2
  292. package/src/Dialog/Dialog.tsx +12 -2
  293. package/src/DropdownMenu/DropdownMenu.doc.mjs +23 -0
  294. package/src/DropdownMenu/DropdownMenu.tsx +11 -6
  295. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +95 -0
  296. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +236 -0
  297. package/src/DropdownMenu/DropdownMenuContext.tsx +29 -1
  298. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +7 -1
  299. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +80 -0
  300. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +118 -0
  301. package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +80 -0
  302. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +232 -0
  303. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +163 -0
  304. package/src/DropdownMenu/index.ts +23 -3
  305. package/src/DropdownMenu/menuItemRoles.ts +22 -0
  306. package/src/Field/Field.doc.mjs +1 -1
  307. package/src/Field/Field.test.tsx +57 -12
  308. package/src/Field/FieldLabel.doc.mjs +1 -1
  309. package/src/FieldStatus/FieldStatus.doc.mjs +17 -5
  310. package/src/FieldStatus/FieldStatus.test.tsx +78 -40
  311. package/src/FieldStatus/FieldStatus.tsx +22 -4
  312. package/src/FileInput/FileInput.test.tsx +52 -3
  313. package/src/FileInput/FileInput.tsx +13 -2
  314. package/src/HoverCard/HoverCard.doc.mjs +13 -0
  315. package/src/HoverCard/HoverCard.test.tsx +21 -2
  316. package/src/HoverCard/HoverCard.tsx +11 -0
  317. package/src/HoverCard/useHoverCard.doc.mjs +2 -0
  318. package/src/HoverCard/useHoverCard.tsx +24 -2
  319. package/src/Icon/Icon.doc.mjs +12 -0
  320. package/src/Icon/Icon.test.tsx +142 -0
  321. package/src/Icon/Icon.tsx +98 -30
  322. package/src/IconButton/IconButton.doc.mjs +8 -0
  323. package/src/IconButton/IconButton.test.tsx +15 -18
  324. package/src/Item/Item.doc.mjs +1 -1
  325. package/src/Layer/useLayer.tsx +8 -0
  326. package/src/MobileNav/MobileNav.tsx +12 -1
  327. package/src/MobileNav/MobileNavToggle.test.tsx +116 -0
  328. package/src/MobileNav/MobileNavToggle.tsx +9 -1
  329. package/src/MultiSelector/MultiSelector.doc.mjs +4 -2
  330. package/src/MultiSelector/MultiSelector.test.tsx +162 -1
  331. package/src/MultiSelector/MultiSelector.tsx +55 -12
  332. package/src/MultiSelector/hooks.ts +17 -0
  333. package/src/NumberInput/NumberInput.doc.mjs +2 -2
  334. package/src/NumberInput/NumberInput.test.tsx +11 -7
  335. package/src/NumberInput/NumberInput.tsx +1 -2
  336. package/src/Outline/Outline.doc.mjs +123 -3
  337. package/src/Outline/Outline.test.tsx +735 -0
  338. package/src/Outline/Outline.tsx +183 -33
  339. package/src/Outline/useScrollSpy.ts +248 -72
  340. package/src/OverflowList/OverflowList.doc.mjs +39 -6
  341. package/src/OverflowList/OverflowList.test.tsx +111 -0
  342. package/src/OverflowList/OverflowList.tsx +48 -8
  343. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +67 -0
  344. package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
  345. package/src/PowerSearch/usePowerSearchSource.ts +1 -2
  346. package/src/SelectableCard/SelectableCard.doc.mjs +2 -0
  347. package/src/SelectableCard/SelectableCard.test.tsx +46 -4
  348. package/src/SelectableCard/SelectableCard.tsx +27 -14
  349. package/src/Selector/Selector.test.tsx +69 -0
  350. package/src/Selector/Selector.tsx +5 -1
  351. package/src/Selector/SelectorOption.doc.mjs +1 -1
  352. package/src/Selector/hooks.ts +17 -0
  353. package/src/SideNav/SideNav.test.tsx +119 -3
  354. package/src/SideNav/SideNavCollapseButton.tsx +6 -1
  355. package/src/SideNav/SideNavHeading.tsx +24 -8
  356. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  357. package/src/SideNav/SideNavItem.tsx +5 -1
  358. package/src/Slider/Slider.test.tsx +62 -2
  359. package/src/Slider/Slider.tsx +15 -1
  360. package/src/Switch/Switch.doc.mjs +1 -1
  361. package/src/Switch/Switch.test.tsx +32 -1
  362. package/src/TabList/Tab.tsx +5 -1
  363. package/src/TabList/TabList.doc.mjs +3 -3
  364. package/src/TabList/TabList.test.tsx +66 -0
  365. package/src/TabList/TabList.tsx +8 -0
  366. package/src/TabList/TabMenu.tsx +4 -1
  367. package/src/Table/Table.doc.mjs +2 -0
  368. package/src/Table/Table.tsx +1 -1
  369. package/src/Table/index.ts +12 -0
  370. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +6 -2
  371. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +90 -1
  372. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +37 -5
  373. package/src/Table/plugins/rowStatus/index.ts +7 -0
  374. package/src/Table/plugins/rowStatus/useTableRowStatus.test.tsx +138 -0
  375. package/src/Table/plugins/rowStatus/useTableRowStatus.tsx +189 -0
  376. package/src/Table/plugins/selection/useTableSelection.test.tsx +16 -0
  377. package/src/Table/plugins/selection/useTableSelection.tsx +18 -1
  378. package/src/Table/plugins/sortable/useTableSortable.test.tsx +95 -9
  379. package/src/Table/plugins/sortable/useTableSortable.tsx +23 -7
  380. package/src/Table/plugins/tree/index.ts +12 -0
  381. package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +163 -0
  382. package/src/Table/plugins/tree/useTableTreeData.test.tsx +594 -0
  383. package/src/Table/plugins/tree/useTableTreeData.tsx +495 -0
  384. package/src/Table/plugins/tree/useTableTreeState.test.tsx +636 -0
  385. package/src/Table/plugins/tree/useTableTreeState.tsx +253 -0
  386. package/src/Table/useBaseTablePlugins.ts +6 -2
  387. package/src/Table/useTableRowStatus.doc.mjs +30 -0
  388. package/src/Table/useTableSelection.doc.mjs +65 -14
  389. package/src/Table/useTableTreeData.doc.mjs +75 -0
  390. package/src/Table/useTableTreeState.doc.mjs +98 -0
  391. package/src/TextArea/TextArea.doc.mjs +1 -1
  392. package/src/TextArea/TextArea.test.tsx +9 -1
  393. package/src/TextInput/TextInput.doc.mjs +1 -1
  394. package/src/Thumbnail/Thumbnail.doc.mjs +9 -0
  395. package/src/Thumbnail/Thumbnail.test.tsx +38 -8
  396. package/src/Thumbnail/Thumbnail.tsx +28 -1
  397. package/src/TimeInput/TimeInput.test.tsx +76 -1
  398. package/src/TimeInput/TimeInput.tsx +19 -6
  399. package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
  400. package/src/Tokenizer/Tokenizer.test.tsx +137 -2
  401. package/src/Tokenizer/Tokenizer.tsx +16 -4
  402. package/src/Toolbar/Toolbar.tsx +10 -3
  403. package/src/TopNav/TopNav.test.tsx +80 -1
  404. package/src/TopNav/TopNavHeading.tsx +24 -8
  405. package/src/TopNav/TopNavMegaMenu.test.tsx +149 -1
  406. package/src/TopNav/TopNavMegaMenu.tsx +10 -4
  407. package/src/TopNav/TopNavMenu.test.tsx +118 -4
  408. package/src/TopNav/TopNavMenu.tsx +83 -5
  409. package/src/TreeList/TreeList.test.tsx +32 -1
  410. package/src/TreeList/TreeListItem.tsx +19 -10
  411. package/src/authoring/doc.ts +1 -3
  412. package/src/docs-script.test.mjs +18 -13
  413. package/src/hooks/computeOverflow.test.ts +422 -0
  414. package/src/hooks/computeOverflow.ts +290 -0
  415. package/src/hooks/useOverflow.test.ts +96 -4
  416. package/src/hooks/useOverflow.ts +77 -33
  417. package/src/postinstall.test.mjs +71 -0
  418. package/src/theme/Theme.doc.mjs +4 -4
  419. package/src/theme/Theme.tsx +1 -1
  420. package/src/theme/defineTheme.ts +1 -1
  421. package/src/theme/generateThemeRules.test.ts +65 -0
  422. package/src/theme/generateThemeRules.ts +56 -0
  423. package/src/theme/hct.ts +0 -4
  424. package/src/utils/types.ts +14 -0
  425. package/dist/TreeList/treeListItem.markers.stylex.d.ts +0 -16
  426. package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +0 -1
  427. package/dist/TreeList/treeListItem.markers.stylex.js +0 -22
  428. package/src/TreeList/treeListItem.markers.stylex.ts +0 -20
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi} from 'vitest';
13
- import {render, screen} from '@testing-library/react';
13
+ import {fireEvent, render, screen} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {TopNav} from './TopNav';
16
16
  import {TopNavRenderContext} from './TopNavRenderContext';
@@ -297,6 +297,85 @@ describe('TopNavHeading', () => {
297
297
  render(<TopNavHeading heading="Test" ref={ref} />);
298
298
  expect(ref).toHaveBeenCalledWith(expect.any(HTMLElement));
299
299
  });
300
+
301
+ // ===========================================================================
302
+ // Menu popover semantics — the popup must not be announced as a modal
303
+ // dialog, and role="menu" must be scoped to the actual menu items so the
304
+ // heading button is not an invalid child of the menu.
305
+ // ===========================================================================
306
+
307
+ // The popover layer keeps `popover` content display:none in jsdom even
308
+ // when open, hiding it from role queries — so these tests assert the
309
+ // popup's ARIA semantics at the DOM level instead.
310
+ describe('menu popover semantics', () => {
311
+ const menuItems = (
312
+ <>
313
+ <div role="menuitem">Alpha</div>
314
+ <div role="menuitem">Beta</div>
315
+ </>
316
+ );
317
+
318
+ it('does not wrap the heading menu popup in a modal dialog', async () => {
319
+ const user = userEvent.setup();
320
+ render(<TopNavHeading heading="My App" menu={menuItems} />);
321
+ await user.click(screen.getByRole('button', {name: 'Open menu'}));
322
+ expect(document.querySelector('[role="dialog"]')).toBeNull();
323
+ expect(document.querySelector('[aria-modal="true"]')).toBeNull();
324
+ });
325
+
326
+ it('scopes role="menu" to only menuitem children with an accessible name', async () => {
327
+ const user = userEvent.setup();
328
+ render(<TopNavHeading heading="My App" menu={menuItems} />);
329
+ await user.click(screen.getByRole('button', {name: 'Open menu'}));
330
+ const menu = document.querySelector('[role="menu"]');
331
+ expect(menu).not.toBeNull();
332
+ expect(menu).toHaveAttribute('aria-label', 'My App');
333
+ const children = Array.from(menu!.children);
334
+ expect(children.length).toBeGreaterThan(0);
335
+ for (const child of children) {
336
+ expect(child).toHaveAttribute('role', 'menuitem');
337
+ }
338
+ });
339
+
340
+ it('keeps the popup heading button outside the menu and closes on click', async () => {
341
+ const user = userEvent.setup();
342
+ render(<TopNavHeading heading="My App" menu={menuItems} />);
343
+ const trigger = screen.getByRole('button', {name: 'Open menu'});
344
+ await user.click(trigger);
345
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
346
+ const menu = document.querySelector('[role="menu"]');
347
+ expect(menu).not.toBeNull();
348
+ // The heading replica button in the popup is a sibling of the menu,
349
+ // not an invalid menu child.
350
+ const headingButton = Array.from(
351
+ document.querySelectorAll('button'),
352
+ ).find(b => b !== trigger && b.textContent?.includes('My App'));
353
+ expect(headingButton).toBeDefined();
354
+ expect(menu!.contains(headingButton!)).toBe(false);
355
+ // Clicking it still closes the popup.
356
+ fireEvent.click(headingButton!);
357
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
358
+ });
359
+
360
+ it('applies the same semantics in mixed mode (menu + hrefs)', async () => {
361
+ const user = userEvent.setup();
362
+ render(
363
+ <TopNavHeading
364
+ heading="Product"
365
+ headingHref="/product"
366
+ menu={menuItems}
367
+ />,
368
+ );
369
+ await user.click(screen.getByRole('button', {name: 'Open menu'}));
370
+ expect(document.querySelector('[role="dialog"]')).toBeNull();
371
+ const menu = document.querySelector('[role="menu"]');
372
+ expect(menu).not.toBeNull();
373
+ expect(menu).toHaveAttribute('aria-label', 'Product');
374
+ for (const child of Array.from(menu!.children)) {
375
+ expect(child).toHaveAttribute('role', 'menuitem');
376
+ }
377
+ });
378
+ });
300
379
  });
301
380
 
302
381
  describe('NavIcon', () => {
@@ -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}
@@ -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)}>
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi} from 'vitest';
13
- import {render, screen} from '@testing-library/react';
13
+ import {render, screen, fireEvent} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {TreeList} from './TreeList';
16
16
  import type {TreeListItemData} from './TreeListTypes';
@@ -251,6 +251,37 @@ describe('TreeList', () => {
251
251
  expect(screen.getByText('Leaf')).toBeInTheDocument();
252
252
  });
253
253
 
254
+ // ===========================================================================
255
+ // Focus-visible outline scoping (regression: focusing a parent row must not
256
+ // leak the ring onto descendant rows — see #4130)
257
+ // ===========================================================================
258
+
259
+ it('scopes the focus-visible outline to the focused row, not its descendants', () => {
260
+ render(<TreeList items={deepItems} />);
261
+ const root = screen.getByText('Root').closest('li')!;
262
+ const mid = screen.getByText('Mid').closest('li')!;
263
+ const leaf = screen.getByText('Leaf').closest('li')!;
264
+
265
+ // A keydown before .focus() establishes keyboard modality so jsdom's
266
+ // :focus-visible heuristic applies deterministically, regardless of
267
+ // pointer events left over from other tests in this file.
268
+ fireEvent.keyDown(document.body, {key: 'Tab'});
269
+ root.focus();
270
+ expect(root).toHaveFocus();
271
+
272
+ expect(
273
+ getComputedStyle(root).getPropertyValue('--_tree-focus-outline'),
274
+ ).toContain('solid');
275
+ // Mid and Leaf are DOM descendants of Root's <li> (nested <ul role="group">
276
+ // subtrees) — their own outline var must stay unset, not inherit Root's.
277
+ expect(
278
+ getComputedStyle(mid).getPropertyValue('--_tree-focus-outline'),
279
+ ).toBe('none');
280
+ expect(
281
+ getComputedStyle(leaf).getPropertyValue('--_tree-focus-outline'),
282
+ ).toBe('none');
283
+ });
284
+
254
285
  // ===========================================================================
255
286
  // Interactive items
256
287
  // ===========================================================================