@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
@@ -99,6 +99,38 @@ describe('Timestamp', () => {
99
99
  expect(el.textContent).not.toContain(':');
100
100
  });
101
101
 
102
+ it('renders date_weekday format with a weekday prefix', () => {
103
+ render(
104
+ <Timestamp
105
+ value="2026-02-19T17:00:00Z"
106
+ format="date_weekday"
107
+ data-testid="ts"
108
+ />,
109
+ );
110
+ const el = screen.getByTestId('ts');
111
+ // Includes the year and a leading weekday abbreviation, no time portion.
112
+ expect(el.textContent).toContain('2026');
113
+ expect(el.textContent).not.toContain(':');
114
+ // en-US short weekday for 2026-02-19 is "Thu"; assert a weekday word is
115
+ // present without over-fitting the exact locale punctuation.
116
+ expect(el.textContent).toMatch(/Mon|Tue|Wed|Thu|Fri|Sat|Sun/);
117
+ });
118
+
119
+ it('renders date_long format with a full month name', () => {
120
+ render(
121
+ <Timestamp
122
+ value="2026-02-19T17:00:00Z"
123
+ format="date_long"
124
+ data-testid="ts"
125
+ />,
126
+ );
127
+ const el = screen.getByTestId('ts');
128
+ // Long-month shape: full month name, year, no time portion.
129
+ expect(el.textContent).toContain('February');
130
+ expect(el.textContent).toContain('2026');
131
+ expect(el.textContent).not.toContain(':');
132
+ });
133
+
102
134
  it('renders date_time format', () => {
103
135
  render(
104
136
  <Timestamp
@@ -21,9 +21,11 @@ import * as stylex from '@stylexjs/stylex';
21
21
  import {Text} from '../Text';
22
22
  import type {TextType, TextSize, TextColor, TextWeight} from '../theme/types';
23
23
  import {mergeProps, mergeRefs} from '../utils';
24
+ import {useDevWarning} from '../hooks/useDevWarning';
24
25
  import type {BaseProps} from '../BaseProps';
25
26
  import {themeProps} from '../utils/themeProps';
26
27
  import {colorVars} from '../theme/tokens.stylex';
28
+ import {SHARED_DATE_FORMAT_OPTIONS} from '../utils/plainDate';
27
29
 
28
30
  const LazyXDSTooltip = lazy(async () =>
29
31
  import('../Tooltip/Tooltip').then(mod => ({default: mod.Tooltip})),
@@ -37,6 +39,8 @@ export type TimestampFormat =
37
39
  | 'relative'
38
40
  | 'auto'
39
41
  | 'date'
42
+ | 'date_long'
43
+ | 'date_weekday'
40
44
  | 'date_time'
41
45
  | 'time'
42
46
  | 'system_date'
@@ -53,6 +57,8 @@ export interface TimestampProps extends BaseProps<HTMLTimeElement> {
53
57
  * - `'relative'`: "2 hours ago", "yesterday", "now"
54
58
  * - `'auto'`: Relative for recent times, `date_time` for older
55
59
  * - `'date'`: "Mar 21, 2025"
60
+ * - `'date_long'`: "March 21, 2025"
61
+ * - `'date_weekday'`: "Wed, Mar 21, 2025"
56
62
  * - `'date_time'`: "Mar 21, 2025, 2:51 PM"
57
63
  * - `'time'`: "2:51 PM"
58
64
  * - `'system_date'`: "2025-03-21"
@@ -249,11 +255,22 @@ function formatTimestamp(
249
255
  ): string {
250
256
  switch (format) {
251
257
  case 'date':
252
- return new Intl.DateTimeFormat(undefined, {
253
- year: 'numeric',
254
- month: 'short',
255
- day: 'numeric',
256
- }).format(date);
258
+ return new Intl.DateTimeFormat(
259
+ undefined,
260
+ SHARED_DATE_FORMAT_OPTIONS.date,
261
+ ).format(date);
262
+
263
+ case 'date_long':
264
+ return new Intl.DateTimeFormat(
265
+ undefined,
266
+ SHARED_DATE_FORMAT_OPTIONS.date_long,
267
+ ).format(date);
268
+
269
+ case 'date_weekday':
270
+ return new Intl.DateTimeFormat(
271
+ undefined,
272
+ SHARED_DATE_FORMAT_OPTIONS.date_weekday,
273
+ ).format(date);
257
274
 
258
275
  case 'date_time':
259
276
  return new Intl.DateTimeFormat(undefined, {
@@ -415,11 +432,14 @@ export function Timestamp({
415
432
  return () => clearInterval(timer);
416
433
  }, [isLive, isValidDate, effectiveFormat, diffSeconds]);
417
434
 
435
+ useDevWarning(
436
+ 'Timestamp',
437
+ `could not parse value ${JSON.stringify(value)} as a date. Rendering nothing.`,
438
+ !isValidDate,
439
+ );
440
+
418
441
  // Placed after all hooks so the hook order stays stable across renders.
419
442
  if (!isValidDate) {
420
- console.warn(
421
- `Timestamp: could not parse value ${JSON.stringify(value)} as a date. Rendering nothing.`,
422
- );
423
443
  return null;
424
444
  }
425
445
 
@@ -7,6 +7,7 @@ import {createRoot} from 'react-dom/client';
7
7
  import {dataAttr} from '../naming';
8
8
  import {ToastContext, type ToastContextValue} from './ToastContext';
9
9
  import {ToastViewport} from './ToastViewport';
10
+ import {warnOnce} from '../utils/devWarning';
10
11
  import type {
11
12
  ToastOptions,
12
13
  ToastDismissFn,
@@ -17,7 +18,6 @@ import type {
17
18
  // Fallback singleton
18
19
  let fallbackContext: ToastContextValue | null = null;
19
20
  let fallbackRoot: ReturnType<typeof createRoot> | null = null;
20
- let fallbackWarned = false;
21
21
 
22
22
  const ROOT_THEME_ATTRS = ['data-theme', dataAttr('theme')] as const;
23
23
 
@@ -70,13 +70,12 @@ function getFallbackContext(): ToastContextValue {
70
70
  );
71
71
  }
72
72
 
73
- if (!fallbackWarned) {
74
- fallbackWarned = true;
75
- console.warn(
76
- 'useToast: No LayerProvider found. Using fallback viewport. ' +
77
- 'Wrap your app with <LayerProvider> or <AppShell> for full control.',
78
- );
79
- }
73
+ warnOnce(
74
+ 'toast-fallback-viewport',
75
+ 'useToast',
76
+ 'No LayerProvider found. Using fallback viewport. ' +
77
+ 'Wrap your app with <LayerProvider> or <AppShell> for full control.',
78
+ );
80
79
 
81
80
  const container = document.createElement('div');
82
81
  container.setAttribute('data-astryx-toast-fallback', '');
@@ -41,7 +41,7 @@ interface ToggleButtonGroupContextValue {
41
41
  isDisabled?: boolean;
42
42
  }
43
43
 
44
- export const ToggleButtonGroupContext =
44
+ const ToggleButtonGroupContext =
45
45
  createContext<ToggleButtonGroupContextValue | null>(null);
46
46
  ToggleButtonGroupContext.displayName = 'ToggleButtonGroupContext';
47
47
 
@@ -31,7 +31,7 @@ export const docs = {
31
31
  name: 'onChange',
32
32
  type: '(items: T[], change: TokenizerChange<T>) => void',
33
33
  description:
34
- "Called when selection changes. The change argument includes the affected item and type ('add' | 'create' | 'remove' | 'reorder').",
34
+ "Called when selection changes. The change argument includes the affected item and type ('add' | 'create' | 'remove' | 'reorder'). Additions and removals (including Backspace on an empty input) are announced to screen readers via a polite live region.",
35
35
  required: true,
36
36
  },
37
37
  {
@@ -165,6 +165,13 @@ export const docs = {
165
165
  type: '(query: string) => void',
166
166
  description: 'Callback fired when the search query text changes.',
167
167
  },
168
+ {
169
+ name: 'startIcon',
170
+ type: 'ReactNode | IconType',
171
+ description:
172
+ 'Icon to display at the start of the input, before any tokens. Accepts a semantic icon name, an SVG icon component, or a ReactNode directly.',
173
+ slotElements: [{__element: 'Icon', props: {icon: 'search', size: 'sm'}}],
174
+ },
168
175
  {
169
176
  name: 'endContent',
170
177
  type: 'ReactNode',
@@ -370,6 +377,12 @@ export const docsZh = {
370
377
  type: '(query: string) => void',
371
378
  description: '\u641c\u7d22\u67e5\u8be2\u6587\u672c\u53d8\u66f4\u65f6\u89e6\u53d1\u7684\u56de\u8c03\u3002',
372
379
  },
380
+ {
381
+ name: 'startIcon',
382
+ type: 'ReactNode | IconType',
383
+ description:
384
+ '\u5728\u8f93\u5165\u6846\u5f00\u5934\uff08token \u4e4b\u524d\uff09\u663e\u793a\u7684\u56fe\u6807\u3002\u63a5\u53d7\u8bed\u4e49\u56fe\u6807\u540d\u79f0\u3001SVG \u56fe\u6807\u7ec4\u4ef6\u6216\u76f4\u63a5\u4f20\u5165 ReactNode\u3002',
385
+ },
373
386
  {
374
387
  name: 'endContent',
375
388
  type: 'ReactNode',
@@ -460,6 +473,7 @@ export const docsDense = {
460
473
  size: 'Input+token size.',
461
474
  debounceMs: 'Search debounce delay ms. 0 for sync sources.',
462
475
  onChangeQuery: 'Fired on search query text change.',
476
+ startIcon: 'Icon at input start, before tokens. Icon name, SVG component, or ReactNode.',
463
477
  endContent: 'Content at input row end. For buttons, counts, controls.',
464
478
  handleRef: 'Imperative handle for focus() and blur() control.',
465
479
  xstyle: 'StyleX layout styles (margins, positioning). Must be stylex.create() value.',
@@ -9,11 +9,17 @@
9
9
  * SYNC: When Tokenizer.tsx changes, update tests to match
10
10
  */
11
11
 
12
- import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
12
+ import {describe, it, expect, vi, beforeAll, afterAll, afterEach} from 'vitest';
13
13
  import {render, screen, fireEvent, act, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {Tokenizer} from './Tokenizer';
16
+ import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
16
17
  import type {SearchSource, SearchableItem} from '../Typeahead/types';
18
+ import {TestIcon} from '../__tests__/TestIcon';
19
+
20
+ function politeRegion(): HTMLElement | null {
21
+ return document.querySelector('[data-astryx-live-region="polite"]');
22
+ }
17
23
 
18
24
  // Store original matches to restore later
19
25
  const originalMatches = HTMLElement.prototype.matches;
@@ -60,6 +66,10 @@ afterAll(() => {
60
66
  (HTMLElement.prototype as any).matches = originalMatches;
61
67
  });
62
68
 
69
+ afterEach(() => {
70
+ __resetLiveRegionsForTest();
71
+ });
72
+
63
73
  // Test data
64
74
  const users: SearchableItem[] = [
65
75
  {id: '1', label: 'Alice'},
@@ -818,6 +828,51 @@ describe('Tokenizer', () => {
818
828
  });
819
829
  });
820
830
 
831
+ describe('startIcon', () => {
832
+ it('does not render a start icon when omitted', () => {
833
+ render(
834
+ <Tokenizer
835
+ label="Members"
836
+ searchSource={userSource}
837
+ value={[]}
838
+ onChange={() => {}}
839
+ />,
840
+ );
841
+ expect(document.querySelector('svg')).not.toBeInTheDocument();
842
+ });
843
+
844
+ it('renders a ReactNode start icon before the tokens', () => {
845
+ render(
846
+ <Tokenizer
847
+ label="Members"
848
+ searchSource={userSource}
849
+ value={[{id: '1', label: 'Alice'}]}
850
+ onChange={() => {}}
851
+ startIcon={<TestIcon data-testid="start-icon" />}
852
+ />,
853
+ );
854
+ const icon = screen.getByTestId('start-icon');
855
+ const token = screen.getByText('Alice');
856
+ expect(icon).toBeInTheDocument();
857
+ expect(
858
+ icon.compareDocumentPosition(token) & Node.DOCUMENT_POSITION_FOLLOWING,
859
+ ).toBeTruthy();
860
+ });
861
+
862
+ it('renders an IconType (SVG component) start icon', () => {
863
+ render(
864
+ <Tokenizer
865
+ label="Members"
866
+ searchSource={userSource}
867
+ value={[]}
868
+ onChange={() => {}}
869
+ startIcon={TestIcon}
870
+ />,
871
+ );
872
+ expect(document.querySelector('svg')).toBeInTheDocument();
873
+ });
874
+ });
875
+
821
876
  describe('disabledMessage', () => {
822
877
  const h = {hidden: true} as const;
823
878
  const isOpen = (el: Element) => el.matches(':popover-open');
@@ -921,6 +976,130 @@ describe('Tokenizer', () => {
921
976
  expect(screen.getByRole('combobox')).toBeDisabled();
922
977
  });
923
978
  });
979
+ describe('announcements', () => {
980
+ it('announces removal politely on Backspace with an empty input', async () => {
981
+ const onChange = vi.fn();
982
+ render(
983
+ <Tokenizer
984
+ label="Members"
985
+ searchSource={userSource}
986
+ value={[users[0], users[1]]}
987
+ onChange={onChange}
988
+ />,
989
+ );
990
+ const input = screen.getByRole('combobox');
991
+ fireEvent.keyDown(input, {key: 'Backspace'});
992
+ expect(onChange).toHaveBeenCalledWith([users[0]], {
993
+ item: users[1],
994
+ type: 'remove',
995
+ });
996
+ await waitFor(() => {
997
+ expect(politeRegion()).toHaveTextContent('Removed Bob');
998
+ });
999
+ });
1000
+
1001
+ it("announces removal politely when clicking a token's remove button", async () => {
1002
+ render(
1003
+ <Tokenizer
1004
+ label="Members"
1005
+ searchSource={userSource}
1006
+ value={[users[0], users[1]]}
1007
+ onChange={() => {}}
1008
+ />,
1009
+ );
1010
+ fireEvent.click(screen.getByRole('button', {name: 'Remove Alice'}));
1011
+ await waitFor(() => {
1012
+ expect(politeRegion()).toHaveTextContent('Removed Alice');
1013
+ });
1014
+ });
1015
+
1016
+ it('announces addition politely when selecting a search result', async () => {
1017
+ render(
1018
+ <Tokenizer
1019
+ label="Members"
1020
+ searchSource={userSource}
1021
+ value={[]}
1022
+ onChange={() => {}}
1023
+ hasEntriesOnFocus
1024
+ debounceMs={0}
1025
+ />,
1026
+ );
1027
+ const input = screen.getByRole('combobox');
1028
+ fireEvent.focus(input);
1029
+ await act(async () => {
1030
+ await new Promise(r => setTimeout(r, 50));
1031
+ });
1032
+ fireEvent.click(screen.getByText('Alice'));
1033
+ await waitFor(() => {
1034
+ expect(politeRegion()).toHaveTextContent('Added Alice');
1035
+ });
1036
+ });
1037
+
1038
+ it('announces addition politely when creating a token with hasCreate', async () => {
1039
+ const emptySource: SearchSource = {
1040
+ search: () => [],
1041
+ bootstrap: () => [],
1042
+ };
1043
+ render(
1044
+ <Tokenizer
1045
+ label="Tags"
1046
+ searchSource={emptySource}
1047
+ value={[]}
1048
+ onChange={() => {}}
1049
+ hasCreate
1050
+ debounceMs={0}
1051
+ />,
1052
+ );
1053
+ const input = screen.getByRole('combobox');
1054
+ await act(async () => {
1055
+ fireEvent.change(input, {target: {value: 'new-tag'}});
1056
+ });
1057
+ await act(async () => {
1058
+ await new Promise(r => setTimeout(r, 50));
1059
+ });
1060
+ fireEvent.click(screen.getByText('Create "new-tag"'));
1061
+ await waitFor(() => {
1062
+ expect(politeRegion()).toHaveTextContent('Added new-tag');
1063
+ });
1064
+ });
1065
+
1066
+ it('does not announce on mount', () => {
1067
+ render(
1068
+ <Tokenizer
1069
+ label="Members"
1070
+ searchSource={userSource}
1071
+ value={[users[0]]}
1072
+ onChange={() => {}}
1073
+ />,
1074
+ );
1075
+ // The live regions are created lazily on first announce, so a mount
1076
+ // with pre-selected tokens must not create (or speak through) one.
1077
+ expect(politeRegion()).toBeNull();
1078
+ });
1079
+
1080
+ it('does not announce add/remove while typing', async () => {
1081
+ render(
1082
+ <Tokenizer
1083
+ label="Members"
1084
+ searchSource={userSource}
1085
+ value={[]}
1086
+ onChange={() => {}}
1087
+ debounceMs={0}
1088
+ />,
1089
+ );
1090
+ const input = screen.getByRole('combobox');
1091
+ await act(async () => {
1092
+ fireEvent.change(input, {target: {value: 'Ali'}});
1093
+ });
1094
+ await act(async () => {
1095
+ await new Promise(r => setTimeout(r, 50));
1096
+ });
1097
+ // BaseTypeahead announces result counts while typing (existing
1098
+ // behavior); typing alone must not produce add/remove announcements.
1099
+ expect(politeRegion()?.textContent ?? '').not.toMatch(/Added|Removed/);
1100
+ });
1101
+ });
1102
+
924
1103
  describe('form participation', () => {
925
1104
  it('submits one entry per token id under htmlName', () => {
926
1105
  const {container} = render(
@@ -951,7 +1130,9 @@ describe('Tokenizer', () => {
951
1130
  />
952
1131
  </form>,
953
1132
  );
954
- expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
1133
+ expect([
1134
+ ...new FormData(container.querySelector('form')!).keys(),
1135
+ ]).toEqual([]);
955
1136
  });
956
1137
  });
957
1138
  });
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file Tokenizer.tsx
7
- * @input Uses React, BaseTypeahead, Field, Token
7
+ * @input Uses React, BaseTypeahead, Field, Token, useAnnounce
8
8
  * @output Exports Tokenizer multi-select typeahead component
9
9
  * @position Composed component; forwards DOM ref and exposes focus control via
10
10
  * handleRef
@@ -43,6 +43,7 @@ import {renderIconSlot, type IconType} from '../Icon';
43
43
  import {OverflowList} from '../OverflowList';
44
44
  import {useLayer} from '../Layer/useLayer';
45
45
  import {useTooltip} from '../Tooltip';
46
+ import {useAnnounce} from '../hooks/useAnnounce';
46
47
  import {
47
48
  colorVars,
48
49
  spacingVars,
@@ -560,6 +561,12 @@ export function Tokenizer<T extends SearchableItem>({
560
561
  [],
561
562
  );
562
563
 
564
+ // Announce token add/remove politely via the persistent live region.
565
+ // Tokens previously appeared and disappeared silently — Backspace on an
566
+ // empty input removes the trailing token, and the per-token remove buttons
567
+ // gave no audible feedback either.
568
+ const announce = useAnnounce();
569
+
563
570
  // Handle adding an item — detect creatable synthetic items
564
571
  const handleAdd = useCallback(
565
572
  (item: T | null) => {
@@ -584,6 +591,7 @@ export function Tokenizer<T extends SearchableItem>({
584
591
  const realItem = base as T;
585
592
  const newItems = [...value, realItem];
586
593
  onChange(newItems, {item: realItem, type: 'create'});
594
+ announce(`Added ${createdValue}`);
587
595
  return;
588
596
  }
589
597
 
@@ -592,18 +600,22 @@ export function Tokenizer<T extends SearchableItem>({
592
600
  }
593
601
  const newItems = [...value, item];
594
602
  onChange(newItems, {item, type: 'add'});
603
+ announce(`Added ${item.label}`);
595
604
  },
596
- [value, onChange, isAtMax, selectedIds, hasCreate],
605
+ [value, onChange, isAtMax, selectedIds, hasCreate, announce],
597
606
  );
598
607
 
599
- // Handle removing an item
608
+ // Handle removing an item. Single removal path: both Backspace on an empty
609
+ // input and the per-token remove buttons route through here, so the
610
+ // announcement covers both.
600
611
  const handleRemove = useCallback(
601
612
  (item: T) => {
602
613
  const newItems = value.filter(v => v.id !== item.id);
603
614
  onChange(newItems, {item, type: 'remove'});
615
+ announce(`Removed ${item.label}`);
604
616
  inputRef.current?.focus();
605
617
  },
606
- [value, onChange],
618
+ [value, onChange, announce],
607
619
  );
608
620
 
609
621
  // Handle clearing all items
@@ -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', () => {
@@ -444,4 +523,49 @@ describe('TopNavItem', () => {
444
523
  expect(card).toHaveAttribute('aria-label', 'Featured');
445
524
  });
446
525
  });
526
+
527
+ describe('TopNavMegaMenuFeaturedCard image alt handling', () => {
528
+ it('marks the image as explicitly decorative when imageAlt is omitted', () => {
529
+ render(
530
+ <TopNavMegaMenuFeaturedCard
531
+ title="What's new"
532
+ image="/promo.jpg"
533
+ data-testid="featured-card"
534
+ />,
535
+ );
536
+ const img = screen.getByTestId('featured-card').querySelector('img');
537
+ expect(img).toHaveAttribute('alt', '');
538
+ expect(img).toHaveAttribute('role', 'presentation');
539
+ expect(img).toHaveAttribute('aria-hidden', 'true');
540
+ // The image must not be exposed to assistive technology as a nameless img.
541
+ expect(screen.queryByRole('img')).toBeNull();
542
+ });
543
+
544
+ it('exposes the image with normal img semantics when imageAlt is provided', () => {
545
+ render(
546
+ <TopNavMegaMenuFeaturedCard
547
+ title="What's new"
548
+ image="/promo.jpg"
549
+ imageAlt="Team collaboration"
550
+ />,
551
+ );
552
+ const img = screen.getByRole('img', {name: 'Team collaboration'});
553
+ expect(img).not.toHaveAttribute('role');
554
+ expect(img).not.toHaveAttribute('aria-hidden');
555
+ });
556
+
557
+ it('keeps title and link semantics unchanged with a decorative image', () => {
558
+ render(
559
+ <TopNavMegaMenuFeaturedCard
560
+ title="What's new"
561
+ image="/promo.jpg"
562
+ linkLabel="Read the announcement"
563
+ linkHref="/blog/v4"
564
+ />,
565
+ );
566
+ expect(screen.getByText("What's new")).toBeInTheDocument();
567
+ const link = screen.getByRole('link', {name: /Read the announcement/});
568
+ expect(link).toHaveAttribute('href', '/blog/v4');
569
+ });
570
+ });
447
571
  });