@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
@@ -31,18 +31,13 @@ describe('Item', () => {
31
31
  });
32
32
 
33
33
  it('renders marker', () => {
34
- render(
35
- <Item label="Item" marker={<span data-testid="marker">•</span>} />,
36
- );
34
+ render(<Item label="Item" marker={<span data-testid="marker">•</span>} />);
37
35
  expect(screen.getByTestId('marker')).toBeInTheDocument();
38
36
  });
39
37
 
40
38
  it('renders startContent', () => {
41
39
  render(
42
- <Item
43
- label="Item"
44
- startContent={<span data-testid="avatar">A</span>}
45
- />,
40
+ <Item label="Item" startContent={<span data-testid="avatar">A</span>} />,
46
41
  );
47
42
  expect(screen.getByTestId('avatar')).toBeInTheDocument();
48
43
  });
@@ -255,12 +250,7 @@ describe('Item', () => {
255
250
  it('does not fire onClick when disabled item is clicked', async () => {
256
251
  const onClick = vi.fn();
257
252
  render(
258
- <Item
259
- label="Disabled"
260
- onClick={onClick}
261
- isDisabled
262
- data-testid="item"
263
- />,
253
+ <Item label="Disabled" onClick={onClick} isDisabled data-testid="item" />,
264
254
  );
265
255
  const item = screen.getByTestId('item');
266
256
  item.dispatchEvent(new MouseEvent('click', {bubbles: true}));
@@ -276,14 +266,46 @@ describe('Item', () => {
276
266
  // Selected state
277
267
  // ===========================================================================
278
268
 
279
- it('applies aria-selected when isSelected', () => {
269
+ it('conveys selection via aria-current on the default div root', () => {
270
+ // aria-selected is invalid ARIA on a generic div (axe: aria-allowed-attr),
271
+ // so selection is exposed via aria-current, which is valid on any element.
280
272
  render(<Item label="Selected" isSelected data-testid="item" />);
281
- expect(screen.getByTestId('item')).toHaveAttribute('aria-selected', 'true');
273
+ const item = screen.getByTestId('item');
274
+ expect(item).not.toHaveAttribute('aria-selected');
275
+ expect(item).toHaveAttribute('aria-current', 'true');
282
276
  });
283
277
 
284
- it('does not apply aria-selected when not selected', () => {
285
- render(<Item label="Not Selected" data-testid="item" />);
286
- expect(screen.getByTestId('item')).not.toHaveAttribute('aria-selected');
278
+ it('applies aria-selected (not aria-current) when the role permits it', () => {
279
+ render(
280
+ <Item label="Selected" isSelected role="option" data-testid="item" />,
281
+ );
282
+ const item = screen.getByTestId('item');
283
+ expect(item).toHaveAttribute('aria-selected', 'true');
284
+ // A permitted role uses aria-selected; aria-current would be redundant.
285
+ expect(item).not.toHaveAttribute('aria-current');
286
+ });
287
+
288
+ it('falls back to aria-current when the role does not permit aria-selected', () => {
289
+ render(
290
+ <Item label="Selected" isSelected role="menuitem" data-testid="item" />,
291
+ );
292
+ const item = screen.getByTestId('item');
293
+ expect(item).not.toHaveAttribute('aria-selected');
294
+ expect(item).toHaveAttribute('aria-current', 'true');
295
+ });
296
+
297
+ it('applies neither aria-selected nor aria-current when not selected', () => {
298
+ render(<Item label="Not Selected" role="option" data-testid="item" />);
299
+ const item = screen.getByTestId('item');
300
+ expect(item).not.toHaveAttribute('aria-selected');
301
+ expect(item).not.toHaveAttribute('aria-current');
302
+ });
303
+
304
+ it('lets a consumer-provided aria-current win over the selection default', () => {
305
+ render(
306
+ <Item label="Step" isSelected aria-current="step" data-testid="item" />,
307
+ );
308
+ expect(screen.getByTestId('item')).toHaveAttribute('aria-current', 'step');
287
309
  });
288
310
 
289
311
  // ===========================================================================
package/src/Item/Item.tsx CHANGED
@@ -131,7 +131,12 @@ export interface ItemProps extends BaseProps<HTMLElement> {
131
131
  isHighlighted?: boolean;
132
132
 
133
133
  /**
134
- * Selected state.
134
+ * Selected state. Always applies the selected visual styling. When `role`
135
+ * permits it (option, tab, row, gridcell, columnheader, rowheader, treeitem)
136
+ * the state is exposed as `aria-selected`; otherwise (e.g. a listitem or a
137
+ * bare div, where `aria-selected` is invalid ARIA) it falls back to
138
+ * `aria-current="true"` so assistive tech is still told which item is
139
+ * selected. A consumer-provided `aria-current` always wins.
135
140
  * @default false
136
141
  */
137
142
  isSelected?: boolean;
@@ -148,6 +153,24 @@ export interface ItemProps extends BaseProps<HTMLElement> {
148
153
  'data-testid'?: string;
149
154
  }
150
155
 
156
+ // =============================================================================
157
+ // Constants
158
+ // =============================================================================
159
+
160
+ /**
161
+ * Roles on which WAI-ARIA permits the aria-selected attribute.
162
+ * https://www.w3.org/TR/wai-aria-1.2/#aria-selected
163
+ */
164
+ const ARIA_SELECTED_ROLES = new Set([
165
+ 'option',
166
+ 'tab',
167
+ 'row',
168
+ 'gridcell',
169
+ 'columnheader',
170
+ 'rowheader',
171
+ 'treeitem',
172
+ ]);
173
+
151
174
  // =============================================================================
152
175
  // Styles
153
176
  // =============================================================================
@@ -346,6 +369,11 @@ export function Item({
346
369
  // handles keyboard access. Skip the invisible button/anchor and put
347
370
  // onClick directly on the root element instead.
348
371
  const hasParentRole = role != null;
372
+ // aria-selected is only valid on selectable roles (option, tab, treeitem,
373
+ // grid cells). On the default div/li root the attribute is invalid ARIA
374
+ // (axe: aria-allowed-attr), so selection stays visual-only there — callers
375
+ // that need selection semantics pass a permitted role.
376
+ const allowsAriaSelected = role != null && ARIA_SELECTED_ROLES.has(role);
349
377
 
350
378
  const isStringLabel = typeof label === 'string';
351
379
  const isStringDescription = typeof description === 'string';
@@ -471,7 +499,15 @@ export function Item({
471
499
  <Component
472
500
  ref={ref as React.Ref<never>}
473
501
  {...restProps}
474
- aria-selected={isSelected || undefined}
502
+ aria-selected={(allowsAriaSelected && isSelected) || undefined}
503
+ // aria-selected is invalid on roles that don't permit it (listitem, a
504
+ // bare div, etc.). For those, convey selection via aria-current — valid
505
+ // on any element — so the state still reaches AT. Written after
506
+ // {...restProps} so it must defer to a consumer-provided aria-current.
507
+ aria-current={
508
+ restProps['aria-current'] ??
509
+ (isSelected && !allowsAriaSelected ? true : undefined)
510
+ }
475
511
  aria-disabled={isDisabled || undefined}
476
512
  {...mergeProps(
477
513
  themeProps('item', {density, align}),
@@ -100,6 +100,12 @@ export interface ContextRenderProps {
100
100
  * non-hashed selector for the layer.
101
101
  */
102
102
  role?: string;
103
+ /**
104
+ * Accessible name applied to the popover container via `aria-label`.
105
+ * Pair with `role` so layers with a named role (e.g. `'dialog'`) expose a
106
+ * proper name to assistive technology.
107
+ */
108
+ 'aria-label'?: string;
103
109
  /**
104
110
  * StyleX styles for the popover container.
105
111
  */
@@ -525,6 +531,7 @@ export function useLayer(
525
531
  alignment = 'center',
526
532
  positioning = 'anchor',
527
533
  role,
534
+ 'aria-label': ariaLabel,
528
535
  xstyle,
529
536
  className: extraClassName,
530
537
  style: extraStyle,
@@ -561,6 +568,7 @@ export function useLayer(
561
568
  ref={popoverRefCallback}
562
569
  id={id}
563
570
  role={role}
571
+ aria-label={ariaLabel}
564
572
  popover={lightDismiss ? 'auto' : 'manual'}
565
573
  className={combinedClassName}
566
574
  style={{...stylexResult.style, ...anchorStyle, ...extraStyle}}
@@ -436,11 +436,7 @@ describe('List', () => {
436
436
  it('sets target on anchor when provided', () => {
437
437
  const {container} = render(
438
438
  <List>
439
- <ListItem
440
- label="External"
441
- href="https://example.com"
442
- target="_blank"
443
- />
439
+ <ListItem label="External" href="https://example.com" target="_blank" />
444
440
  </List>,
445
441
  );
446
442
  const anchor = container.querySelector('a');
@@ -500,17 +496,21 @@ describe('List', () => {
500
496
  // Selected state
501
497
  // ===========================================================================
502
498
 
503
- it('applies aria-selected when isSelected', () => {
499
+ it('conveys selection via aria-current when isSelected', () => {
500
+ // aria-selected is not permitted on an li (role listitem, axe:
501
+ // aria-allowed-attr), so selection is exposed via aria-current — valid on
502
+ // any element — so screen-reader users are still told which item is chosen.
504
503
  const {container} = render(
505
504
  <List>
506
505
  <ListItem label="Selected" isSelected onClick={() => {}} />
507
506
  </List>,
508
507
  );
509
508
  const item = container.querySelector('.astryx-item');
510
- expect(item).toHaveAttribute('aria-selected', 'true');
509
+ expect(item).not.toHaveAttribute('aria-selected');
510
+ expect(item).toHaveAttribute('aria-current', 'true');
511
511
  });
512
512
 
513
- it('does not apply aria-selected when not selected', () => {
513
+ it('applies neither aria-selected nor aria-current when not selected', () => {
514
514
  const {container} = render(
515
515
  <List>
516
516
  <ListItem label="Not Selected" />
@@ -518,6 +518,7 @@ describe('List', () => {
518
518
  );
519
519
  const li = container.querySelector('li');
520
520
  expect(li).not.toHaveAttribute('aria-selected');
521
+ expect(li).not.toHaveAttribute('aria-current');
521
522
  });
522
523
 
523
524
  // ===========================================================================
@@ -29,6 +29,7 @@
29
29
  import {
30
30
  useCallback,
31
31
  useEffect,
32
+ useId,
32
33
  useMemo,
33
34
  useRef,
34
35
  useState,
@@ -295,6 +296,10 @@ export function MobileNav({
295
296
  // Read from AppShell context as fallback
296
297
  const appShellMobile = useAppShellMobile();
297
298
  const isOpen = isOpenProp ?? appShellMobile.isMobileNavOpen;
299
+ // Share the id from AppShell context so the toggle's aria-controls resolves to
300
+ // this drawer; fall back to a locally generated id when used standalone.
301
+ const fallbackId = useId();
302
+ const dialogId = appShellMobile.mobileNavId || fallbackId;
298
303
  const onOpenChange = useMemo(
299
304
  () =>
300
305
  onOpenChangeProp ??
@@ -411,6 +416,7 @@ export function MobileNav({
411
416
  return (
412
417
  <dialog
413
418
  ref={mergeRefs(ref, dialogRef)}
419
+ id={dialogId}
414
420
  {...mergeProps(
415
421
  themeProps('mobile-nav', {side: resolvedSide}),
416
422
  stylex.props(
@@ -425,7 +431,12 @@ export function MobileNav({
425
431
  )}
426
432
  {...rest}
427
433
  data-testid={testId}
428
- aria-label={label ?? (typeof header === 'string' ? header : t('@astryx.mobileNav.navigation'))}
434
+ aria-label={
435
+ label ??
436
+ (typeof header === 'string'
437
+ ? header
438
+ : t('@astryx.mobileNav.navigation'))
439
+ }
429
440
  onClick={composeEventHandlers(onClickProp, handleDialogClick)}
430
441
  onCancel={handleCancel}>
431
442
  {/* Drawer panel — tabIndex so showModal() focuses the drawer, not the close button */}
@@ -0,0 +1,116 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file MobileNavToggle.test.tsx
5
+ * @input Uses vitest, @testing-library/react, AppShell + SideNav
6
+ * @output Unit tests for MobileNavToggle ARIA wiring
7
+ * @position Testing; validates MobileNavToggle.tsx exposes aria-expanded and
8
+ * aria-controls so screen-reader users know the drawer state and target.
9
+ *
10
+ * SYNC: When MobileNavToggle.tsx changes, update tests to match new behavior
11
+ */
12
+
13
+ import {
14
+ describe,
15
+ it,
16
+ expect,
17
+ vi,
18
+ beforeAll,
19
+ beforeEach,
20
+ afterEach,
21
+ } from 'vitest';
22
+ import {render, screen, fireEvent} from '@testing-library/react';
23
+ import {AppShell} from '../AppShell/AppShell';
24
+ import {SideNav, SideNavItem, SideNavSection} from '../SideNav';
25
+
26
+ // jsdom doesn't implement showModal/close on <dialog>, so we mock them
27
+ beforeAll(() => {
28
+ HTMLDialogElement.prototype.showModal =
29
+ HTMLDialogElement.prototype.showModal ||
30
+ function (this: HTMLDialogElement) {
31
+ this.setAttribute('open', '');
32
+ };
33
+ HTMLDialogElement.prototype.close =
34
+ HTMLDialogElement.prototype.close ||
35
+ function (this: HTMLDialogElement) {
36
+ this.removeAttribute('open');
37
+ };
38
+ });
39
+
40
+ class MockResizeObserver {
41
+ observe() {}
42
+ unobserve() {}
43
+ disconnect() {}
44
+ }
45
+ vi.stubGlobal('ResizeObserver', MockResizeObserver);
46
+
47
+ function createMockMatchMedia(matches: boolean) {
48
+ return {
49
+ matches,
50
+ media: '',
51
+ onchange: null,
52
+ addEventListener: vi.fn(),
53
+ removeEventListener: vi.fn(),
54
+ addListener: vi.fn(),
55
+ removeListener: vi.fn(),
56
+ dispatchEvent: vi.fn(),
57
+ };
58
+ }
59
+
60
+ beforeEach(() => {
61
+ // Below the breakpoint so the mobile nav (and its toggle) render.
62
+ vi.stubGlobal(
63
+ 'matchMedia',
64
+ vi.fn().mockReturnValue(createMockMatchMedia(true)),
65
+ );
66
+ });
67
+
68
+ afterEach(() => {
69
+ vi.restoreAllMocks();
70
+ });
71
+
72
+ function TestShell() {
73
+ return (
74
+ <AppShell
75
+ sideNav={
76
+ <SideNav>
77
+ <SideNavSection title="Test" isHeaderHidden>
78
+ <SideNavItem label="Home" />
79
+ </SideNavSection>
80
+ </SideNav>
81
+ }
82
+ mobileNav={{breakpoint: 'md'}}>
83
+ <div>Content</div>
84
+ </AppShell>
85
+ );
86
+ }
87
+
88
+ describe('MobileNavToggle ARIA wiring', () => {
89
+ it('exposes aria-expanded="false" when the drawer is closed', () => {
90
+ render(<TestShell />);
91
+
92
+ const toggle = screen.getByRole('button', {name: /open navigation/i});
93
+ expect(toggle).toHaveAttribute('aria-expanded', 'false');
94
+ });
95
+
96
+ it('exposes aria-expanded="true" after opening the drawer', () => {
97
+ render(<TestShell />);
98
+
99
+ const toggle = screen.getByRole('button', {name: /open navigation/i});
100
+ fireEvent.click(toggle);
101
+ expect(toggle).toHaveAttribute('aria-expanded', 'true');
102
+ });
103
+
104
+ it('references the nav drawer via aria-controls that resolves to the dialog', () => {
105
+ render(<TestShell />);
106
+
107
+ const toggle = screen.getByRole('button', {name: /open navigation/i});
108
+ const controls = toggle.getAttribute('aria-controls');
109
+ expect(controls).toBeTruthy();
110
+
111
+ const target = document.getElementById(controls!);
112
+ expect(target).not.toBeNull();
113
+ expect(target).toBe(screen.getAllByRole('dialog', {hidden: true})[0]);
114
+ expect(target?.tagName).toBe('DIALOG');
115
+ });
116
+ });
@@ -69,7 +69,13 @@ export function MobileNavToggle({
69
69
  }: MobileNavToggleProps) {
70
70
  const t = useTranslator();
71
71
  const label = labelFromProps ?? t('@astryx.mobileNav.toggle.open');
72
- const {isMobile, isMobileNavEnabled, toggleMobileNav} = useAppShellMobile();
72
+ const {
73
+ isMobile,
74
+ isMobileNavEnabled,
75
+ isMobileNavOpen,
76
+ mobileNavId,
77
+ toggleMobileNav,
78
+ } = useAppShellMobile();
73
79
 
74
80
  // Don't render above the breakpoint or when mobile nav is disabled
75
81
  if (!isMobile || !isMobileNavEnabled) {
@@ -83,6 +89,8 @@ export function MobileNavToggle({
83
89
  label={label}
84
90
  icon={children ?? <Icon icon="menu" color="inherit" />}
85
91
  onClick={toggleMobileNav}
92
+ aria-expanded={isMobileNavOpen}
93
+ aria-controls={mobileNavId || undefined}
86
94
  data-testid={testId ?? 'mobile-nav-toggle'}
87
95
  xstyle={xstyle}
88
96
  className={className}
@@ -99,7 +99,8 @@ export const docs = {
99
99
  {
100
100
  name: 'hasSearch',
101
101
  type: 'boolean',
102
- description: 'Whether to show a search input for filtering options.',
102
+ description:
103
+ 'Whether to show a search input for filtering options. As the user types, the match count (or "No results found") is announced to screen readers via a polite live region.',
103
104
  },
104
105
  {
105
106
  name: 'searchPlaceholder',
@@ -239,7 +240,8 @@ export const docsZh = {
239
240
  hasSearch: '是否显示用于过滤选项的搜索输入。',
240
241
  searchPlaceholder: '搜索输入的占位文本。',
241
242
  isDisabled: '禁用选择器。',
242
- htmlName: '用于表单提交的 HTML name 属性。为每个已选值渲染一个隐藏输入,类似原生多选。',
243
+ htmlName:
244
+ '用于表单提交的 HTML name 属性。为每个已选值渲染一个隐藏输入,类似原生多选。',
243
245
  disabledMessage:
244
246
  '解释选择器被禁用的原因。与 isDisabled 一起使用时,悬停/键盘聚焦时显示工具提示,并通过 aria-disabled 保持触发器可聚焦(仍无法激活)。请使用此属性,而不是用 Tooltip 包裹被禁用的选择器。',
245
247
  isLabelHidden: '视觉上隐藏标签同时保持其可访问性。',
@@ -10,7 +10,13 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
- import {render, screen, fireEvent, waitFor} from '@testing-library/react';
13
+ import {
14
+ render,
15
+ screen,
16
+ fireEvent,
17
+ waitFor,
18
+ within,
19
+ } from '@testing-library/react';
14
20
  import userEvent from '@testing-library/user-event';
15
21
  import {MultiSelector} from './MultiSelector';
16
22
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
@@ -360,6 +366,30 @@ describe('MultiSelector', () => {
360
366
  expect(activeId).toBeTruthy();
361
367
  });
362
368
 
369
+ it('End/Home jump the highlight to the last/first option (non-search)', async () => {
370
+ const user = userEvent.setup();
371
+ render(
372
+ <MultiSelector
373
+ label="Fruit"
374
+ options={defaultOptions}
375
+ value={[]}
376
+ onChange={() => {}}
377
+ />,
378
+ );
379
+
380
+ const trigger = screen.getByRole('combobox');
381
+ await user.click(trigger);
382
+ const options = screen.getAllByRole('option', h);
383
+
384
+ await user.keyboard('{End}');
385
+ expect(trigger).toHaveAttribute(
386
+ 'aria-activedescendant',
387
+ options[options.length - 1].id,
388
+ );
389
+ await user.keyboard('{Home}');
390
+ expect(trigger).toHaveAttribute('aria-activedescendant', options[0].id);
391
+ });
392
+
363
393
  it('toggles item with Enter key', async () => {
364
394
  const user = userEvent.setup();
365
395
  const onChange = vi.fn();
@@ -535,6 +565,65 @@ describe('MultiSelector', () => {
535
565
  expect(options).toHaveLength(1);
536
566
  });
537
567
 
568
+ it('PageDown/PageUp jump the highlight to the last/first filtered option', async () => {
569
+ const user = userEvent.setup();
570
+ render(
571
+ <MultiSelector
572
+ label="Fruit"
573
+ options={defaultOptions}
574
+ value={[]}
575
+ onChange={() => {}}
576
+ hasSearch
577
+ />,
578
+ );
579
+
580
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
581
+ const searchInput = screen.getByRole('combobox', h);
582
+ // Filter to Banana and Orange so "last" means last *visible* option.
583
+ await user.type(searchInput, 'an');
584
+ const options = screen.getAllByRole('option', h);
585
+ expect(options).toHaveLength(2);
586
+
587
+ await user.keyboard('{PageDown}');
588
+ expect(searchInput).toHaveAttribute(
589
+ 'aria-activedescendant',
590
+ options[options.length - 1].id,
591
+ );
592
+ await user.keyboard('{PageUp}');
593
+ expect(searchInput).toHaveAttribute('aria-activedescendant', options[0].id);
594
+ });
595
+
596
+ it('Home/End move the search caret, not the option highlight', async () => {
597
+ const user = userEvent.setup();
598
+ render(
599
+ <MultiSelector
600
+ label="Fruit"
601
+ options={defaultOptions}
602
+ value={[]}
603
+ onChange={() => {}}
604
+ hasSearch
605
+ />,
606
+ );
607
+
608
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
609
+ const searchInput = screen.getByRole<HTMLInputElement>('combobox', h);
610
+ await user.type(searchInput, 'an');
611
+ expect(searchInput.selectionStart).toBe(2);
612
+ const activeBefore = searchInput.getAttribute('aria-activedescendant');
613
+ // Home/End stay on the input for caret movement (APG editable combobox);
614
+ // the option highlight must not move.
615
+ await user.keyboard('{Home}');
616
+ expect(searchInput.selectionStart).toBe(0);
617
+ expect(searchInput.getAttribute('aria-activedescendant')).toBe(
618
+ activeBefore,
619
+ );
620
+ await user.keyboard('{End}');
621
+ expect(searchInput.selectionStart).toBe(2);
622
+ expect(searchInput.getAttribute('aria-activedescendant')).toBe(
623
+ activeBefore,
624
+ );
625
+ });
626
+
538
627
  it('shows empty state when search has no results', async () => {
539
628
  const user = userEvent.setup();
540
629
  render(
@@ -551,7 +640,85 @@ describe('MultiSelector', () => {
551
640
  const searchInput = screen.getByRole('combobox', h);
552
641
  await user.type(searchInput, 'xyz');
553
642
 
554
- expect(screen.getByText('No results found')).toBeInTheDocument();
643
+ expect(
644
+ within(screen.getByRole('listbox', h)).getByText('No results found'),
645
+ ).toBeInTheDocument();
646
+ });
647
+
648
+ describe('result announcements', () => {
649
+ it('announces the match count politely while searching', async () => {
650
+ const user = userEvent.setup();
651
+ render(
652
+ <MultiSelector
653
+ label="Fruit"
654
+ options={defaultOptions}
655
+ value={EMPTY_VALUE}
656
+ onChange={() => {}}
657
+ hasSearch
658
+ />,
659
+ );
660
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
661
+ // "an" matches Banana and Orange.
662
+ await user.type(screen.getByRole('combobox', h), 'an');
663
+ await waitFor(() => {
664
+ expect(politeRegion()).toHaveTextContent('2 results');
665
+ });
666
+ });
667
+
668
+ it('announces the singular form when one option matches', async () => {
669
+ const user = userEvent.setup();
670
+ render(
671
+ <MultiSelector
672
+ label="Fruit"
673
+ options={defaultOptions}
674
+ value={EMPTY_VALUE}
675
+ onChange={() => {}}
676
+ hasSearch
677
+ />,
678
+ );
679
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
680
+ // "app" matches only Apple. Anchored so it cannot pass on "1 results".
681
+ await user.type(screen.getByRole('combobox', h), 'app');
682
+ await waitFor(() => {
683
+ expect(politeRegion()).toHaveTextContent(/^1 result$/);
684
+ });
685
+ });
686
+
687
+ it('announces "No results found" when nothing matches', async () => {
688
+ const user = userEvent.setup();
689
+ render(
690
+ <MultiSelector
691
+ label="Fruit"
692
+ options={defaultOptions}
693
+ value={EMPTY_VALUE}
694
+ onChange={() => {}}
695
+ hasSearch
696
+ />,
697
+ );
698
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
699
+ await user.type(screen.getByRole('combobox', h), 'xyz');
700
+ await waitFor(() => {
701
+ expect(politeRegion()).toHaveTextContent('No results found');
702
+ });
703
+ });
704
+
705
+ it('does not announce results until the user searches', async () => {
706
+ const user = userEvent.setup();
707
+ render(
708
+ <MultiSelector
709
+ label="Fruit"
710
+ options={defaultOptions}
711
+ value={EMPTY_VALUE}
712
+ onChange={() => {}}
713
+ hasSearch
714
+ />,
715
+ );
716
+ // Popover closed: nothing announced.
717
+ expect(politeRegion()?.textContent ?? '').toBe('');
718
+ // Open with an empty query: still nothing announced.
719
+ await user.click(screen.getByRole('button', {name: 'Fruit'}));
720
+ expect(politeRegion()?.textContent ?? '').toBe('');
721
+ });
555
722
  });
556
723
 
557
724
  it('renders with description', () => {
@@ -1072,7 +1239,9 @@ describe('MultiSelector', () => {
1072
1239
  />
1073
1240
  </form>,
1074
1241
  );
1075
- expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
1242
+ expect([
1243
+ ...new FormData(container.querySelector('form')!).keys(),
1244
+ ]).toEqual([]);
1076
1245
  });
1077
1246
  });
1078
1247
  });