@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
@@ -16,7 +16,14 @@
16
16
  * - /packages/cli/templates/blocks/components/Dialog/ (showcase blocks)
17
17
  */
18
18
 
19
- import {useEffect, useMemo, useRef, type ReactNode} from 'react';
19
+ import {
20
+ useCallback,
21
+ useEffect,
22
+ useId,
23
+ useMemo,
24
+ useRef,
25
+ type ReactNode,
26
+ } from 'react';
20
27
  import type {BaseProps} from '../BaseProps';
21
28
  import * as stylex from '@stylexjs/stylex';
22
29
  import {useScrollLock} from '../hooks/useScrollLock';
@@ -39,6 +46,7 @@ import {
39
46
  } from '../Layout/padding.stylex';
40
47
  import type {SpacingStep} from '../utils/types';
41
48
  import {mergeProps, mergeRefs} from '../utils';
49
+ import {devWarn} from '../utils/devWarning';
42
50
  import {DialogContext} from './DialogContext';
43
51
  import {themeProps} from '../utils/themeProps';
44
52
 
@@ -152,7 +160,13 @@ const styles = stylex.create({
152
160
  open: {
153
161
  display: 'flex',
154
162
  opacity: 1,
155
- animationName: enterDirectional,
163
+ // Disable the entry keyframe animation under
164
+ // `prefers-reduced-motion: reduce` so the dialog appears instantly
165
+ // instead of translating/scaling in (same pattern as layerAnimations).
166
+ animationName: {
167
+ default: enterDirectional,
168
+ '@media (prefers-reduced-motion: reduce)': 'none',
169
+ },
156
170
  },
157
171
  // Backdrop using ::backdrop pseudo-element
158
172
  backdrop: {
@@ -312,6 +326,9 @@ export interface DialogProps extends BaseProps<HTMLDialogElement> {
312
326
  *
313
327
  * Designed to be used with Layout as its child for structured content.
314
328
  * Uses the browser's built-in modal behavior for optimal accessibility.
329
+ * When a DialogHeader is rendered inside, its title automatically names the
330
+ * dialog via aria-labelledby; pass `aria-label` or `aria-labelledby` to
331
+ * override.
315
332
  *
316
333
  * @example
317
334
  * ```
@@ -348,10 +365,47 @@ export function Dialog({
348
365
  const paddingToken = spacingStepToToken[effectivePadding] as SpacingToken;
349
366
 
350
367
  const isFullscreen = variant === 'fullscreen';
351
- const dialogContextValue = useMemo(() => ({isInline}), [isInline]);
368
+
369
+ // Default accessible name: publish a title id through DialogContext so a
370
+ // DialogHeader applies it to its heading (mirrors AlertDialog's explicit
371
+ // useId wiring). Whether to emit the default aria-labelledby is decided
372
+ // imperatively in the callback ref below by checking whether the title
373
+ // element actually rendered — so a dialog with no header never points at a
374
+ // missing id, and we avoid the extra render a registration-via-state effect
375
+ // would cost at the dialog level.
376
+ const titleId = useId();
377
+
378
+ const dialogContextValue = useMemo(
379
+ () => ({isInline, titleId}),
380
+ [isInline, titleId],
381
+ );
382
+
383
+ // Consumer-provided labels always win over the DialogHeader default.
384
+ const hasConsumerName =
385
+ props['aria-label'] != null || props['aria-labelledby'] != null;
352
386
 
353
387
  const dialogRef = useRef<HTMLDialogElement>(null);
354
388
 
389
+ // Callback ref on the <dialog>: sync the default aria-labelledby from the
390
+ // DialogHeader title (if one rendered) in the same commit, with no second
391
+ // render. Consumer aria-label/aria-labelledby win — when present we leave
392
+ // the attribute to the {...safeProps} spread and never touch it here.
393
+ const attachDialog = useCallback(
394
+ (node: HTMLDialogElement | null) => {
395
+ dialogRef.current = node;
396
+ if (!node || hasConsumerName) {
397
+ return;
398
+ }
399
+ const hasTitle = node.querySelector(`#${CSS.escape(titleId)}`) != null;
400
+ if (hasTitle) {
401
+ node.setAttribute('aria-labelledby', titleId);
402
+ } else {
403
+ node.removeAttribute('aria-labelledby');
404
+ }
405
+ },
406
+ [titleId, hasConsumerName],
407
+ );
408
+
355
409
  // Capture the element that was focused when the dialog opened,
356
410
  // for directional animation origin and focus restoration on close.
357
411
  const triggerElementRef = useRef<HTMLElement | null>(null);
@@ -440,6 +494,29 @@ export function Dialog({
440
494
  return () => dialog.removeEventListener('keydown', handleKeyDown);
441
495
  }, [isOpen, isInline, allowEscape, onOpenChange]);
442
496
 
497
+ // Dev-time guardrail: an open modal should always have an accessible name.
498
+ // The header-title check reads the DOM, so this stays in an effect; the ref
499
+ // keeps it to one warning per component instance.
500
+ const warnedUnnamedDialogRef = useRef(false);
501
+ useEffect(() => {
502
+ const hasHeaderTitle =
503
+ dialogRef.current?.querySelector(`#${CSS.escape(titleId)}`) != null;
504
+ if (
505
+ isOpen &&
506
+ !isInline &&
507
+ !hasConsumerName &&
508
+ !hasHeaderTitle &&
509
+ !warnedUnnamedDialogRef.current
510
+ ) {
511
+ warnedUnnamedDialogRef.current = true;
512
+ devWarn(
513
+ 'Dialog',
514
+ 'open dialog has no accessible name. Add a DialogHeader ' +
515
+ 'with a `title`, or pass `aria-label`/`aria-labelledby`.',
516
+ );
517
+ }
518
+ }, [isOpen, isInline, hasConsumerName, titleId]);
519
+
443
520
  // Handle backdrop click — when the user clicks the ::backdrop pseudo-element,
444
521
  // the event target is the <dialog> element itself; clicks on child content
445
522
  // always target the child. This avoids false positives from native browser
@@ -531,7 +608,11 @@ export function Dialog({
531
608
  ),
532
609
  className,
533
610
  style,
534
- )}>
611
+ )}
612
+ data-testid={
613
+ (props as Record<string, unknown>)['data-testid'] as
614
+ string | undefined
615
+ }>
535
616
  {innerContent}
536
617
  </div>
537
618
  );
@@ -541,7 +622,7 @@ export function Dialog({
541
622
 
542
623
  return (
543
624
  <dialog
544
- ref={mergeRefs(ref, dialogRef)}
625
+ ref={mergeRefs(ref, attachDialog)}
545
626
  {...safeProps}
546
627
  {...mergeProps(
547
628
  themeProps('dialog', {variant}),
@@ -566,6 +647,9 @@ export function Dialog({
566
647
  onClick={handleClick}
567
648
  onCancel={handleCancel}
568
649
  aria-modal="true"
650
+ // The default aria-labelledby (from a DialogHeader title) is set
651
+ // imperatively in `attachDialog`; a consumer-provided aria-labelledby
652
+ // flows through {...safeProps} above and wins.
569
653
  {...(purpose === 'required' ? {role: 'alertdialog'} : undefined)}>
570
654
  {innerContent}
571
655
  </dialog>
@@ -5,8 +5,8 @@
5
5
  /**
6
6
  * @file DialogContext.ts
7
7
  * @input React context
8
- * @output Internal dialog context for child focus behavior
9
- * @position Dialog internals; consumed by focus-managing children
8
+ * @output Internal dialog context for child focus behavior and default labelling
9
+ * @position Dialog internals; consumed by focus-managing children and DialogHeader
10
10
  */
11
11
 
12
12
  import {createContext, use} from 'react';
@@ -14,6 +14,13 @@ import {createContext, use} from 'react';
14
14
  export interface DialogContextValue {
15
15
  /** Whether the dialog is rendered inline for docs/showcases. */
16
16
  isInline: boolean;
17
+ /**
18
+ * Id the DialogHeader title should render with so the dialog can name
19
+ * itself via aria-labelledby. The dialog detects the title element's
20
+ * presence directly (via a callback ref), so it only emits aria-labelledby
21
+ * when the title actually rendered — never pointing at a nonexistent id.
22
+ */
23
+ titleId?: string;
17
24
  }
18
25
 
19
26
  export const DialogContext = createContext<DialogContextValue | null>(null);
@@ -12,7 +12,7 @@ export const docs = {
12
12
  {
13
13
  name: 'title',
14
14
  type: 'string',
15
- description: 'Dialog title (receives focus on open).',
15
+ description: 'Dialog title (receives focus on open and labels the dialog via aria-labelledby).',
16
16
  },
17
17
  {
18
18
  name: 'subtitle',
@@ -158,7 +158,7 @@ export const docsDense = {
158
158
  displayName: 'Dialog Header',
159
159
  description: 'dialog header w/ title, optional subtitle, close button, start/end content slots',
160
160
  propDescriptions: {
161
- title: 'dialog title (receives focus on open)',
161
+ title: 'dialog title (focused on open; labels dialog via aria-labelledby)',
162
162
  subtitle: 'subtitle below title',
163
163
  onOpenChange: 'close button callback (omit=no button)',
164
164
  startContent: 'content before title (e.g. back button)',
@@ -65,7 +65,9 @@ export interface DialogHeaderProps extends BaseProps<HTMLDivElement> {
65
65
  ref?: React.Ref<HTMLDivElement>;
66
66
  /**
67
67
  * The title of the dialog.
68
- * This title receives focus when the dialog opens for screen reader accessibility.
68
+ * This title receives focus when the dialog opens for screen reader
69
+ * accessibility, and names the parent Dialog via aria-labelledby unless the
70
+ * consumer passes an explicit aria-label/aria-labelledby to the Dialog.
69
71
  */
70
72
  title: string;
71
73
 
@@ -105,7 +107,8 @@ export interface DialogHeaderProps extends BaseProps<HTMLDivElement> {
105
107
  * Renders a title that receives focus when a modal dialog opens (for screen reader accessibility)
106
108
  * and an optional close button. Inline documentation previews suppress this autofocus.
107
109
  * The title is an h2 element with tabIndex={-1} so it can be programmatically focused but
108
- * doesn't appear in the tab order.
110
+ * doesn't appear in the tab order. The title also names the parent Dialog via
111
+ * aria-labelledby (unless the Dialog receives an explicit aria-label/aria-labelledby).
109
112
  *
110
113
  * Uses LayoutHeader internally for consistent styling with other layout headers.
111
114
  *
@@ -137,10 +140,13 @@ export function DialogHeader({
137
140
  const titleRef = useRef<HTMLHeadingElement>(null);
138
141
  const dialogContext = useDialogContext();
139
142
  const shouldAutoFocus = dialogContext?.isInline !== true;
143
+ const titleId = dialogContext?.titleId;
140
144
 
141
145
  // Auto-focus the title when mounted for screen reader accessibility.
142
146
  // Inline dialogs are documentation/showcase previews, so suppress focus to
143
147
  // avoid stealing scroll position from the surrounding page.
148
+ // The parent Dialog detects this title (by `titleId`) via a callback ref to
149
+ // set its default aria-labelledby — no registration handshake needed here.
144
150
  useEffect(() => {
145
151
  if (shouldAutoFocus && titleRef.current) {
146
152
  titleRef.current.focus();
@@ -162,6 +168,7 @@ export function DialogHeader({
162
168
  <div {...stylex.props(styles.titleWrapper)}>
163
169
  <Heading
164
170
  ref={titleRef}
171
+ id={titleId}
165
172
  level={2}
166
173
  tabIndex={-1}
167
174
  xstyle={styles.titleFocusable}>
@@ -172,6 +172,7 @@ export function Divider({
172
172
  return (
173
173
  <div
174
174
  ref={ref}
175
+ {...props}
175
176
  role="separator"
176
177
  aria-orientation={orientation}
177
178
  {...mergeProps(
@@ -186,8 +187,7 @@ export function Divider({
186
187
  ),
187
188
  className,
188
189
  style,
189
- )}
190
- {...props}>
190
+ )}>
191
191
  <div
192
192
  {...stylex.props(
193
193
  isHorizontal ? lineStyles.horizontalLine : lineStyles.verticalLine,
@@ -613,3 +613,172 @@ describe('DropdownMenu compound mode', () => {
613
613
  ).toBeInTheDocument();
614
614
  });
615
615
  });
616
+
617
+ describe('DropdownMenu keyboard access for menuitemradio/menuitemcheckbox (#3829)', () => {
618
+ it('arrow navigation reaches consumer-rendered menuitemradio and menuitemcheckbox items', async () => {
619
+ const user = userEvent.setup();
620
+ render(
621
+ <DropdownMenu button={{label: 'Sort'}}>
622
+ <DropdownMenuItem label="Edit" onClick={() => {}} />
623
+ <div role="menuitemradio" tabIndex={-1} aria-checked="false">
624
+ Newest
625
+ </div>
626
+ <div role="menuitemcheckbox" tabIndex={-1} aria-checked="false">
627
+ Archived
628
+ </div>
629
+ </DropdownMenu>,
630
+ );
631
+
632
+ await user.click(screen.getByRole('button', {name: /Sort/}));
633
+ const menu = screen.getByRole('menu', {hidden: true});
634
+ screen.getByRole('menuitem', {name: 'Edit', hidden: true}).focus();
635
+
636
+ fireEvent.keyDown(menu, {key: 'ArrowDown'});
637
+ expect(
638
+ screen.getByRole('menuitemradio', {name: 'Newest', hidden: true}),
639
+ ).toHaveFocus();
640
+
641
+ fireEvent.keyDown(menu, {key: 'ArrowDown'});
642
+ expect(
643
+ screen.getByRole('menuitemcheckbox', {name: 'Archived', hidden: true}),
644
+ ).toHaveFocus();
645
+ });
646
+
647
+ it('activates a focused menuitemradio with Enter and a menuitemcheckbox with Space', async () => {
648
+ const user = userEvent.setup();
649
+ const onSelect = vi.fn();
650
+ const onToggle = vi.fn();
651
+ render(
652
+ <DropdownMenu button={{label: 'Sort'}}>
653
+ <DropdownMenuItem label="Edit" onClick={() => {}} />
654
+ <div
655
+ role="menuitemradio"
656
+ tabIndex={-1}
657
+ aria-checked="false"
658
+ onClick={onSelect}>
659
+ Newest
660
+ </div>
661
+ <div
662
+ role="menuitemcheckbox"
663
+ tabIndex={-1}
664
+ aria-checked="false"
665
+ onClick={onToggle}>
666
+ Archived
667
+ </div>
668
+ </DropdownMenu>,
669
+ );
670
+
671
+ await user.click(screen.getByRole('button', {name: /Sort/}));
672
+ const menu = screen.getByRole('menu', {hidden: true});
673
+
674
+ screen.getByRole('menuitemradio', {name: 'Newest', hidden: true}).focus();
675
+ fireEvent.keyDown(menu, {key: 'Enter'});
676
+ expect(onSelect).toHaveBeenCalledTimes(1);
677
+
678
+ screen
679
+ .getByRole('menuitemcheckbox', {name: 'Archived', hidden: true})
680
+ .focus();
681
+ fireEvent.keyDown(menu, {key: ' '});
682
+ expect(onToggle).toHaveBeenCalledTimes(1);
683
+ });
684
+
685
+ it('typeahead matches a menuitemradio label', async () => {
686
+ const user = userEvent.setup();
687
+ render(
688
+ <DropdownMenu button={{label: 'Sort'}}>
689
+ <DropdownMenuItem label="Edit" onClick={() => {}} />
690
+ <div role="menuitemradio" tabIndex={-1} aria-checked="false">
691
+ Newest
692
+ </div>
693
+ <div role="menuitemcheckbox" tabIndex={-1} aria-checked="false">
694
+ Archived
695
+ </div>
696
+ </DropdownMenu>,
697
+ );
698
+
699
+ await user.click(screen.getByRole('button', {name: /Sort/}));
700
+ fireEvent.keyDown(screen.getByRole('menu', {hidden: true}), {key: 'n'});
701
+ expect(
702
+ screen.getByRole('menuitemradio', {name: 'Newest', hidden: true}),
703
+ ).toHaveFocus();
704
+ });
705
+
706
+ it('typeahead matches a menuitemcheckbox label', async () => {
707
+ const user = userEvent.setup();
708
+ render(
709
+ <DropdownMenu button={{label: 'Sort'}}>
710
+ <DropdownMenuItem label="Edit" onClick={() => {}} />
711
+ <div role="menuitemradio" tabIndex={-1} aria-checked="false">
712
+ Newest
713
+ </div>
714
+ <div role="menuitemcheckbox" tabIndex={-1} aria-checked="false">
715
+ Archived
716
+ </div>
717
+ </DropdownMenu>,
718
+ );
719
+
720
+ await user.click(screen.getByRole('button', {name: /Sort/}));
721
+ fireEvent.keyDown(screen.getByRole('menu', {hidden: true}), {key: 'a'});
722
+ expect(
723
+ screen.getByRole('menuitemcheckbox', {name: 'Archived', hidden: true}),
724
+ ).toHaveFocus();
725
+ });
726
+
727
+ it('typeahead skips an aria-disabled item and matches the next enabled label', async () => {
728
+ const user = userEvent.setup();
729
+ render(
730
+ <DropdownMenu button={{label: 'Sort'}}>
731
+ <DropdownMenuItem label="Edit" onClick={() => {}} />
732
+ <div role="menuitemradio" tabIndex={-1} aria-disabled="true">
733
+ Newest
734
+ </div>
735
+ <div role="menuitemcheckbox" tabIndex={-1} aria-checked="false">
736
+ Nightly
737
+ </div>
738
+ </DropdownMenu>,
739
+ );
740
+
741
+ await user.click(screen.getByRole('button', {name: /Sort/}));
742
+ // Anchor the search on 'Edit' so typeahead scans forward and meets the
743
+ // disabled 'Newest' (also an 'n' match) before the enabled 'Nightly'.
744
+ // This pins the `:not([aria-disabled="true"])` in MENU_ITEM_SELECTOR: the
745
+ // menus never pass useTypeahead's `isDisabled` option, so that clause is
746
+ // the only thing keeping disabled rows out of the typeahead list. An
747
+ // arrow-key test cannot cover it — useListFocus re-filters disabled items
748
+ // independently, so arrow navigation is guarded twice over.
749
+ // The disabled row keeps tabIndex={-1} on purpose: it stays focusable, so
750
+ // the selector clause is the sole reason focus skips it.
751
+ screen.getByRole('menuitem', {name: 'Edit', hidden: true}).focus();
752
+ fireEvent.keyDown(screen.getByRole('menu', {hidden: true}), {key: 'n'});
753
+ expect(
754
+ screen.getByRole('menuitemcheckbox', {name: 'Nightly', hidden: true}),
755
+ ).toHaveFocus();
756
+ });
757
+
758
+ it('skips aria-disabled menuitemradio and menuitemcheckbox items during arrow navigation', async () => {
759
+ const user = userEvent.setup();
760
+ render(
761
+ <DropdownMenu button={{label: 'Sort'}}>
762
+ <DropdownMenuItem label="Edit" onClick={() => {}} />
763
+ <div role="menuitemradio" tabIndex={-1} aria-disabled="true">
764
+ Newest
765
+ </div>
766
+ <div role="menuitemcheckbox" tabIndex={-1} aria-disabled="true">
767
+ Archived
768
+ </div>
769
+ <div role="menuitemradio" tabIndex={-1} aria-checked="false">
770
+ Oldest
771
+ </div>
772
+ </DropdownMenu>,
773
+ );
774
+
775
+ await user.click(screen.getByRole('button', {name: /Sort/}));
776
+ const menu = screen.getByRole('menu', {hidden: true});
777
+ screen.getByRole('menuitem', {name: 'Edit', hidden: true}).focus();
778
+
779
+ fireEvent.keyDown(menu, {key: 'ArrowDown'});
780
+ expect(
781
+ screen.getByRole('menuitemradio', {name: 'Oldest', hidden: true}),
782
+ ).toHaveFocus();
783
+ });
784
+ });
@@ -152,8 +152,7 @@ export interface DropdownMenuCheckboxItemProps extends Omit<
152
152
  *
153
153
  * @example
154
154
  * ```
155
- * * import {DropdownMenuCheckboxItem} from '@astryxdesign/core/DropdownMenu';
156
- *
155
+ * import {DropdownMenuCheckboxItem} from '@astryxdesign/core/DropdownMenu';
157
156
  * <DropdownMenu button={{label: 'View'}}>
158
157
  * <DropdownMenuCheckboxItem
159
158
  * label="Show archived"
@@ -77,11 +77,10 @@ export interface DropdownMenuRadioGroupProps extends Omit<
77
77
  *
78
78
  * @example
79
79
  * ```
80
- * * import {
80
+ * import {
81
81
  * DropdownMenuRadioGroup,
82
82
  * DropdownMenuRadioItem,
83
83
  * } from '@astryxdesign/core/DropdownMenu';
84
- *
85
84
  * <DropdownMenu button={{label: 'Sort'}}>
86
85
  * <DropdownMenuRadioGroup value={sort} onChange={setSort} aria-label="Sort by">
87
86
  * <DropdownMenuRadioItem value="newest" label="Newest" />
@@ -9,10 +9,22 @@
9
9
  * SYNC: When Field.tsx changes, update tests to match new behavior
10
10
  */
11
11
 
12
- import {describe, it, expect, vi} from 'vitest';
13
- import {render, screen} from '@testing-library/react';
12
+ import {describe, it, expect, vi, afterEach} from 'vitest';
13
+ import {render, screen, waitFor} from '@testing-library/react';
14
14
  import {Field} from './Field';
15
15
  import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
16
+ import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
17
+
18
+ function politeRegion(): HTMLElement | null {
19
+ return document.querySelector('[data-astryx-live-region="polite"]');
20
+ }
21
+ function assertiveRegion(): HTMLElement | null {
22
+ return document.querySelector('[data-astryx-live-region="assertive"]');
23
+ }
24
+
25
+ afterEach(() => {
26
+ __resetLiveRegionsForTest();
27
+ });
16
28
 
17
29
  describe('Field', () => {
18
30
  it('renders with label', () => {
@@ -228,7 +240,11 @@ describe('Field', () => {
228
240
  expect(document.querySelector('svg')).not.toBeInTheDocument();
229
241
  });
230
242
 
231
- it('status has role="alert" and aria-live="assertive" for error type', () => {
243
+ // The status message is announced through the persistent useAnnounce live
244
+ // regions rather than role/aria-live on the (conditionally mounted)
245
+ // FieldStatus element itself — regions born with their content are not
246
+ // reliably announced by assistive technology.
247
+ it('announces error status assertively via the persistent live region', async () => {
232
248
  render(
233
249
  <Field
234
250
  label="Email"
@@ -237,12 +253,13 @@ describe('Field', () => {
237
253
  <input id="email-input" />
238
254
  </Field>,
239
255
  );
240
- const status = screen.getByRole('alert');
241
- expect(status).toHaveTextContent('Invalid email');
242
- expect(status).toHaveAttribute('aria-live', 'assertive');
256
+ expect(screen.getByText('Invalid email')).not.toHaveAttribute('role');
257
+ await waitFor(() => {
258
+ expect(assertiveRegion()).toHaveTextContent('Invalid email');
259
+ });
243
260
  });
244
261
 
245
- it('status has role="status" and aria-live="polite" for warning type', () => {
262
+ it('announces warning status politely via the persistent live region', async () => {
246
263
  render(
247
264
  <Field
248
265
  label="Email"
@@ -251,9 +268,34 @@ describe('Field', () => {
251
268
  <input id="email-input" />
252
269
  </Field>,
253
270
  );
254
- const status = screen.getByRole('status');
255
- expect(status).toHaveTextContent('Check this');
256
- expect(status).toHaveAttribute('aria-live', 'polite');
271
+ expect(screen.getByText('Check this')).not.toHaveAttribute('aria-live');
272
+ await waitFor(() => {
273
+ expect(politeRegion()).toHaveTextContent('Check this');
274
+ });
275
+ });
276
+
277
+ // Regression: the status live region must NOT be born together with its
278
+ // content. A message that appears after mount (the common validation flow)
279
+ // has to land in the persistent announce region.
280
+ it('announces a status message that appears after mount', async () => {
281
+ const {rerender} = render(
282
+ <Field label="Email" inputID="email-input">
283
+ <input id="email-input" />
284
+ </Field>,
285
+ );
286
+ expect(assertiveRegion()).toBeNull();
287
+
288
+ rerender(
289
+ <Field
290
+ label="Email"
291
+ inputID="email-input"
292
+ status={{type: 'error', message: 'Email is required'}}>
293
+ <input id="email-input" />
294
+ </Field>,
295
+ );
296
+ await waitFor(() => {
297
+ expect(assertiveRegion()).toHaveTextContent('Email is required');
298
+ });
257
299
  });
258
300
 
259
301
  it('auto-generates description ID as {inputID}-desc when descriptionID is not provided', () => {
@@ -277,7 +319,10 @@ describe('Field', () => {
277
319
  <input id="my-input" />
278
320
  </Field>,
279
321
  );
280
- expect(screen.getByRole('alert')).toHaveAttribute('id', 'my-input-status');
322
+ expect(screen.getByText('Required')).toHaveAttribute(
323
+ 'id',
324
+ 'my-input-status',
325
+ );
281
326
  });
282
327
 
283
328
  it('warns when isOptional and isRequired are both set', () => {
@@ -421,7 +466,7 @@ describe('Field', () => {
421
466
  </Field>,
422
467
  {wrapper: horizontalLabelsWrapper},
423
468
  );
424
- const statusEl = screen.getByRole('alert');
469
+ const statusEl = screen.getByText('Required');
425
470
  const inputEl = screen.getByTestId('email');
426
471
  // Both status and input should be inside the same wrapper div (column 2)
427
472
  expect(statusEl.parentElement).toBe(inputEl.parentElement);
@@ -25,6 +25,7 @@ import {FieldStatus} from '../FieldStatus/FieldStatus';
25
25
  import {spacingVars, borderVars} from '../theme/tokens.stylex';
26
26
  import type {IconType} from '../Icon';
27
27
  import {mergeProps} from '../utils';
28
+ import {useDevWarning} from '../hooks/useDevWarning';
28
29
  import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
29
30
  import {Text} from '../Text';
30
31
  import {themeProps} from '../utils/themeProps';
@@ -215,11 +216,11 @@ export function Field({
215
216
  const resolvedMessageID =
216
217
  status?.messageID ?? (status?.message ? `${inputID}-status` : undefined);
217
218
 
218
- if (isOptional && isRequired) {
219
- console.warn(
220
- 'Field: isOptional and isRequired are mutually exclusive. isOptional takes precedence.',
221
- );
222
- }
219
+ useDevWarning(
220
+ 'Field',
221
+ 'isOptional and isRequired are mutually exclusive. isOptional takes precedence.',
222
+ isOptional && isRequired,
223
+ );
223
224
 
224
225
  const labelNode = (
225
226
  <FieldLabel
@@ -171,7 +171,11 @@ export function FieldLabel({
171
171
  labelTooltip,
172
172
  description,
173
173
  descriptionID,
174
+ className,
175
+ style,
176
+ xstyle,
174
177
  ref,
178
+ ...rest
175
179
  }: FieldLabelProps) {
176
180
  const statusText = isOptional ? 'Optional' : isRequired ? 'Required' : null;
177
181
 
@@ -207,13 +211,17 @@ export function FieldLabel({
207
211
  // `htmlFor` only applies to a real `<label>` associating with a single
208
212
  // control; a group label (span) has no `htmlFor`.
209
213
  htmlFor={isGroupLabel ? undefined : inputID}
214
+ {...rest}
210
215
  {...mergeProps(
211
216
  themeProps('field-label'),
212
217
  stylex.props(
213
218
  styles.label,
214
219
  isDisabled && styles.labelDisabled,
215
220
  isLabelHidden && styles.srOnly,
221
+ xstyle,
216
222
  ),
223
+ className,
224
+ style,
217
225
  )}>
218
226
  {labelContent}
219
227
  </LabelElement>
@@ -8,7 +8,7 @@ export const docs = {
8
8
  category: 'Data Input',
9
9
  isHiddenFromOverview: true,
10
10
  description:
11
- 'Status message component for form field validation feedback.',
11
+ 'Status message component for form field validation feedback. Messages are announced to screen readers through persistent live regions (assertive for errors, polite otherwise), so conditional mounting is safe.',
12
12
  theming: {
13
13
  targets: [
14
14
  {className: 'astryx-field-status', visualProps: ['type', 'variant']},
@@ -66,12 +66,24 @@ export const docs = {
66
66
  /** @type {import('../docs-types').TranslationDoc} */
67
67
  export const docsDense = {
68
68
  description:
69
- 'Validation feedback message for fields/custom controls. Supports error, warning, success and attached/detached variants.',
69
+ 'Validation feedback message for fields/custom controls. Supports error, warning, success and attached/detached variants. Announced via persistent live regions (assertive for errors, polite otherwise).',
70
70
  usage: {
71
71
  bestPractices: [
72
- {guidance: true, description: 'Use attached status below bordered inputs when message belongs to that input.'},
73
- {guidance: true, description: 'Use detached status for checkboxes, switches, and custom controls where overlap is visually awkward.'},
74
- {guidance: false, description: 'Use FieldStatus for general alerts or page-level notices; use Banner or Toast instead.'},
72
+ {
73
+ guidance: true,
74
+ description:
75
+ 'Use attached status below bordered inputs when message belongs to that input.',
76
+ },
77
+ {
78
+ guidance: true,
79
+ description:
80
+ 'Use detached status for checkboxes, switches, and custom controls where overlap is visually awkward.',
81
+ },
82
+ {
83
+ guidance: false,
84
+ description:
85
+ 'Use FieldStatus for general alerts or page-level notices; use Banner or Toast instead.',
86
+ },
75
87
  ],
76
88
  },
77
89
  propDescriptions: {