@astryxdesign/core 0.7.1-canary.ff40b78 → 0.7.1

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 (348) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/dist/Button/Button.d.ts +1 -1
  3. package/dist/Button/Button.d.ts.map +1 -1
  4. package/dist/Button/Button.js +5 -2
  5. package/dist/Calendar/Calendar.d.ts.map +1 -1
  6. package/dist/Calendar/Calendar.js +191 -32
  7. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  8. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  9. package/dist/Chat/ChatComposerInput.js +5 -0
  10. package/dist/Chat/ChatMessage.d.ts +11 -0
  11. package/dist/Chat/ChatMessage.d.ts.map +1 -1
  12. package/dist/Chat/ChatMessage.js +18 -9
  13. package/dist/Chat/useTriggerMenu.d.ts +9 -2
  14. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  15. package/dist/Chat/useTriggerMenu.js +222 -50
  16. package/dist/CodeBlock/CodeBlock.d.ts +10 -1
  17. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  18. package/dist/CodeBlock/CodeBlock.js +158 -38
  19. package/dist/CodeBlock/tokenizer.d.ts +39 -0
  20. package/dist/CodeBlock/tokenizer.d.ts.map +1 -1
  21. package/dist/CodeBlock/tokenizer.js +108 -0
  22. package/dist/CommandPalette/CommandPalette.d.ts +9 -5
  23. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  24. package/dist/CommandPalette/CommandPalette.js +38 -20
  25. package/dist/CommandPalette/CommandPaletteEmpty.d.ts +4 -3
  26. package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
  27. package/dist/CommandPalette/CommandPaletteEmpty.js +4 -3
  28. package/dist/CommandPalette/CommandPaletteInput.d.ts +5 -4
  29. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  30. package/dist/CommandPalette/CommandPaletteInput.js +35 -18
  31. package/dist/CommandPalette/CommandPaletteItem.d.ts +11 -6
  32. package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
  33. package/dist/CommandPalette/CommandPaletteItem.js +8 -9
  34. package/dist/CommandPalette/CommandPaletteList.d.ts +4 -2
  35. package/dist/CommandPalette/CommandPaletteList.d.ts.map +1 -1
  36. package/dist/CommandPalette/CommandPaletteList.js +10 -5
  37. package/dist/ComplexSelector/ComplexSelector.d.ts +8 -5
  38. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  39. package/dist/ComplexSelector/ComplexSelector.js +12 -3
  40. package/dist/ContextMenu/ContextMenu.d.ts +3 -3
  41. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  42. package/dist/ContextMenu/ContextMenu.js +62 -48
  43. package/dist/DateInput/DateInput.js +1 -1
  44. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  45. package/dist/DateInput/MonthScroller.js +12 -4
  46. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  47. package/dist/DateRangeInput/DateRangeInput.js +12 -2
  48. package/dist/Dialog/Dialog.d.ts +1 -1
  49. package/dist/Dialog/Dialog.d.ts.map +1 -1
  50. package/dist/Dialog/Dialog.js +16 -5
  51. package/dist/Dialog/DialogHeader.d.ts +1 -1
  52. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  53. package/dist/Dialog/DialogHeader.js +7 -7
  54. package/dist/Divider/Divider.d.ts +1 -0
  55. package/dist/Divider/Divider.d.ts.map +1 -1
  56. package/dist/Divider/Divider.js +5 -4
  57. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  58. package/dist/DropdownMenu/DropdownMenu.js +161 -65
  59. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +7 -5
  60. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  61. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +12 -5
  62. package/dist/Field/InputClearButton.d.ts +8 -4
  63. package/dist/Field/InputClearButton.d.ts.map +1 -1
  64. package/dist/Field/InputClearButton.js +23 -7
  65. package/dist/FormLayout/FormLayout.d.ts.map +1 -1
  66. package/dist/FormLayout/FormLayout.js +1 -0
  67. package/dist/Grid/Grid.d.ts +13 -4
  68. package/dist/Grid/Grid.d.ts.map +1 -1
  69. package/dist/Grid/Grid.js +7 -1
  70. package/dist/HoverCard/useHoverCard.d.ts +1 -1
  71. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  72. package/dist/HoverCard/useHoverCard.js +67 -49
  73. package/dist/InputGroup/InputGroup.d.ts +2 -4
  74. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  75. package/dist/InputGroup/InputGroup.js +2 -4
  76. package/dist/InputGroup/groupStyles.d.ts +10 -2
  77. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  78. package/dist/InputGroup/groupStyles.js +19 -2
  79. package/dist/Layer/useLayerDismissal.d.ts +9 -9
  80. package/dist/Layer/useLayerDismissal.js +9 -9
  81. package/dist/Markdown/Markdown.d.ts.map +1 -1
  82. package/dist/Markdown/Markdown.js +25 -4
  83. package/dist/MobileNav/MobileNav.d.ts +7 -0
  84. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  85. package/dist/MobileNav/MobileNav.js +45 -2
  86. package/dist/MultiSelector/MultiSelector.d.ts +7 -4
  87. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  88. package/dist/MultiSelector/MultiSelector.js +24 -3
  89. package/dist/NumberInput/NumberInput.js +1 -1
  90. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  91. package/dist/PowerSearch/PowerSearchEditPopover.js +6 -0
  92. package/dist/Resizable/ResizeHandle.d.ts +1 -1
  93. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  94. package/dist/Resizable/ResizeHandle.js +83 -15
  95. package/dist/Resizable/useResizable.d.ts +23 -0
  96. package/dist/Resizable/useResizable.d.ts.map +1 -1
  97. package/dist/Resizable/useResizable.js +78 -0
  98. package/dist/Selector/Selector.d.ts +6 -3
  99. package/dist/Selector/Selector.d.ts.map +1 -1
  100. package/dist/Selector/Selector.js +28 -6
  101. package/dist/SideNav/SideNav.d.ts.map +1 -1
  102. package/dist/SideNav/SideNav.js +10 -2
  103. package/dist/Slider/Slider.d.ts.map +1 -1
  104. package/dist/Slider/Slider.js +18 -7
  105. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  106. package/dist/Tokenizer/Tokenizer.js +16 -4
  107. package/dist/Toolbar/Toolbar.d.ts +2 -1
  108. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  109. package/dist/Toolbar/Toolbar.js +3 -1
  110. package/dist/TreeList/TreeList.d.ts.map +1 -1
  111. package/dist/TreeList/TreeList.js +6 -3
  112. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  113. package/dist/TreeList/TreeListItem.js +7 -1
  114. package/dist/astryx.css +22 -0
  115. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  116. package/dist/hooks/useScrollOverflow.js +55 -12
  117. package/dist/hooks/useTreeFocus.d.ts +21 -0
  118. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  119. package/dist/hooks/useTreeFocus.js +40 -2
  120. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  121. package/dist/i18n/generated-locales/en.generated.js +3 -0
  122. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  123. package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
  124. package/dist/reset.a11y.chromium.spec.js +6 -1
  125. package/dist/reset.until-found.a11y.browser.spec.d.ts +2 -0
  126. package/dist/reset.until-found.a11y.browser.spec.d.ts.map +1 -0
  127. package/dist/reset.until-found.a11y.browser.spec.js +71 -0
  128. package/dist/theme/syntax/SyntaxTheme.d.ts.map +1 -1
  129. package/dist/theme/syntax/SyntaxTheme.js +43 -2
  130. package/dist/theme/syntax/defineSyntaxTheme.d.ts +18 -5
  131. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  132. package/dist/theme/syntax/defineSyntaxTheme.js +38 -9
  133. package/dist/theme/syntax/index.d.ts +1 -1
  134. package/dist/theme/syntax/index.d.ts.map +1 -1
  135. package/dist/theme/syntax/tokens.d.ts +12 -2
  136. package/dist/theme/syntax/tokens.d.ts.map +1 -1
  137. package/dist/theme/syntax/tokens.js +25 -4
  138. package/dist/utils/plainDate.d.ts +12 -0
  139. package/dist/utils/plainDate.d.ts.map +1 -1
  140. package/dist/utils/plainDate.js +63 -10
  141. package/locales/en.json +13 -1
  142. package/locales/pseudo.json +9 -0
  143. package/package.json +4 -4
  144. package/src/AlertDialog/AlertDialog.doc.mjs +6 -0
  145. package/src/AlertDialog/AlertDialog.spec.md +205 -0
  146. package/src/AlertDialog/AlertDialog.test.tsx +3 -14
  147. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +115 -0
  148. package/src/AlertDialog/__tests__/AlertDialog.a11y.renders.tsx +52 -0
  149. package/src/AlertDialog/__tests__/AlertDialog.a11y.states.ts +68 -0
  150. package/src/AlertDialog/__tests__/AlertDialog.a11y.test.tsx +144 -0
  151. package/src/Button/Button.test.tsx +37 -0
  152. package/src/Button/Button.tsx +5 -1
  153. package/src/Calendar/Calendar.doc.mjs +234 -60
  154. package/src/Calendar/Calendar.spec.md +209 -0
  155. package/src/Calendar/Calendar.test.tsx +559 -20
  156. package/src/Calendar/Calendar.tsx +258 -41
  157. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +126 -0
  158. package/src/Calendar/__tests__/Calendar.a11y.known-failures.ts +12 -0
  159. package/src/Calendar/__tests__/Calendar.a11y.renders.tsx +57 -0
  160. package/src/Calendar/__tests__/Calendar.a11y.states.ts +139 -0
  161. package/src/Calendar/__tests__/Calendar.a11y.test.tsx +137 -0
  162. package/src/Carousel/Carousel.test.tsx +54 -71
  163. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +192 -0
  164. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +27 -0
  165. package/src/Carousel/__tests__/Carousel.a11y.renders.tsx +39 -0
  166. package/src/Carousel/__tests__/Carousel.a11y.states.ts +139 -0
  167. package/src/Carousel/__tests__/Carousel.a11y.test.tsx +127 -0
  168. package/src/Chat/ChatComposerInput.doc.mjs +1 -1
  169. package/src/Chat/ChatComposerInput.spec.md +94 -55
  170. package/src/Chat/ChatComposerInput.test.tsx +202 -1
  171. package/src/Chat/ChatComposerInput.tsx +13 -0
  172. package/src/Chat/ChatMessage.doc.mjs +31 -16
  173. package/src/Chat/ChatMessage.spec.md +265 -0
  174. package/src/Chat/ChatMessage.test.tsx +246 -0
  175. package/src/Chat/ChatMessage.tsx +30 -7
  176. package/src/Chat/ChatSystemMessage.spec.md +36 -37
  177. package/src/Chat/ChatSystemMessage.test.tsx +4 -4
  178. package/src/Chat/useTriggerMenu.test.tsx +994 -0
  179. package/src/Chat/useTriggerMenu.tsx +278 -46
  180. package/src/CodeBlock/CodeBlock.doc.mjs +7 -0
  181. package/src/CodeBlock/CodeBlock.spec.md +40 -41
  182. package/src/CodeBlock/CodeBlock.test.tsx +89 -9
  183. package/src/CodeBlock/CodeBlock.tsx +205 -25
  184. package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
  185. package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +93 -0
  186. package/src/CodeBlock/tokenizer.test.ts +112 -0
  187. package/src/CodeBlock/tokenizer.ts +106 -0
  188. package/src/Collapsible/CollapsibleGroup.spec.md +17 -8
  189. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.renders.tsx +45 -0
  190. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.states.ts +139 -0
  191. package/src/Collapsible/__tests__/CollapsibleGroup.a11y.test.tsx +135 -0
  192. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +128 -0
  193. package/src/CommandPalette/CommandPalette.doc.mjs +126 -23
  194. package/src/CommandPalette/CommandPalette.spec.md +100 -49
  195. package/src/CommandPalette/CommandPalette.test.tsx +241 -2
  196. package/src/CommandPalette/CommandPalette.tsx +47 -21
  197. package/src/CommandPalette/CommandPaletteEmpty.tsx +4 -3
  198. package/src/CommandPalette/CommandPaletteInput.doc.mjs +61 -4
  199. package/src/CommandPalette/CommandPaletteInput.spec.md +215 -0
  200. package/src/CommandPalette/CommandPaletteInput.test.tsx +375 -51
  201. package/src/CommandPalette/CommandPaletteInput.tsx +74 -21
  202. package/src/CommandPalette/CommandPaletteItem.doc.mjs +73 -22
  203. package/src/CommandPalette/CommandPaletteItem.spec.md +223 -0
  204. package/src/CommandPalette/CommandPaletteItem.test.tsx +200 -5
  205. package/src/CommandPalette/CommandPaletteItem.tsx +16 -11
  206. package/src/CommandPalette/CommandPaletteList.doc.mjs +3 -3
  207. package/src/CommandPalette/CommandPaletteList.spec.md +41 -36
  208. package/src/CommandPalette/CommandPaletteList.test.tsx +80 -0
  209. package/src/CommandPalette/CommandPaletteList.tsx +10 -5
  210. package/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts +599 -0
  211. package/src/ComplexSelector/ComplexSelector.doc.mjs +10 -8
  212. package/src/ComplexSelector/ComplexSelector.spec.md +14 -9
  213. package/src/ComplexSelector/ComplexSelector.test.tsx +64 -16
  214. package/src/ComplexSelector/ComplexSelector.tsx +37 -8
  215. package/src/ContextMenu/ContextMenu.doc.mjs +6 -0
  216. package/src/ContextMenu/ContextMenu.spec.md +51 -53
  217. package/src/ContextMenu/ContextMenu.test.tsx +132 -1
  218. package/src/ContextMenu/ContextMenu.tsx +78 -48
  219. package/src/DateInput/DateInput.doc.mjs +52 -0
  220. package/src/DateInput/DateInput.spec.md +27 -15
  221. package/src/DateInput/DateInput.test.tsx +38 -0
  222. package/src/DateInput/DateInput.tsx +1 -1
  223. package/src/DateInput/DateInputTouch.test.tsx +22 -0
  224. package/src/DateInput/MonthScroller.tsx +12 -4
  225. package/src/DateRangeInput/DateRangeInput.test.tsx +49 -0
  226. package/src/DateRangeInput/DateRangeInput.tsx +20 -2
  227. package/src/Dialog/Dialog.test.tsx +27 -1
  228. package/src/Dialog/Dialog.tsx +16 -5
  229. package/src/Dialog/DialogHeader.test.tsx +27 -2
  230. package/src/Dialog/DialogHeader.tsx +7 -8
  231. package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
  232. package/src/Divider/Divider.doc.mjs +5 -3
  233. package/src/Divider/Divider.spec.md +140 -54
  234. package/src/Divider/Divider.test.tsx +15 -0
  235. package/src/Divider/Divider.tsx +6 -4
  236. package/src/DropdownMenu/DropdownMenu.doc.mjs +2 -2
  237. package/src/DropdownMenu/DropdownMenu.spec.md +121 -45
  238. package/src/DropdownMenu/DropdownMenu.test.tsx +288 -23
  239. package/src/DropdownMenu/DropdownMenu.tsx +212 -81
  240. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +38 -2
  241. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +32 -18
  242. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +146 -1
  243. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +136 -0
  244. package/src/DropdownMenu/__tests__/MenuButton.a11y.known-failures.ts +12 -0
  245. package/src/DropdownMenu/__tests__/MenuButton.a11y.renders.tsx +67 -0
  246. package/src/DropdownMenu/__tests__/MenuButton.a11y.states.ts +141 -0
  247. package/src/DropdownMenu/__tests__/MenuButton.a11y.test.tsx +138 -0
  248. package/src/DropdownMenu/__tests__/MenuHeaderFooter.a11y.chromium.spec.ts +40 -5
  249. package/src/Field/InputClearButton.test.tsx +29 -4
  250. package/src/Field/InputClearButton.tsx +26 -8
  251. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
  252. package/src/FormLayout/FormLayout.tsx +1 -0
  253. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +1 -1
  254. package/src/Grid/Grid.doc.mjs +14 -11
  255. package/src/Grid/Grid.spec.md +50 -32
  256. package/src/Grid/Grid.tsx +13 -4
  257. package/src/HoverCard/HoverCard.test.tsx +46 -1
  258. package/src/HoverCard/useHoverCard.tsx +68 -48
  259. package/src/InputGroup/InputGroup.doc.mjs +11 -1
  260. package/src/InputGroup/InputGroup.tsx +2 -4
  261. package/src/InputGroup/groupStyles.ts +40 -2
  262. package/src/Layer/useLayerDismissal.ts +9 -9
  263. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +142 -0
  264. package/src/Link/__tests__/Link.a11y.renders.tsx +97 -0
  265. package/src/Link/__tests__/Link.a11y.states.ts +104 -0
  266. package/src/Link/__tests__/Link.a11y.test.tsx +127 -0
  267. package/src/Markdown/Markdown.doc.mjs +1 -1
  268. package/src/Markdown/Markdown.fr23.perf.test.ts +161 -40
  269. package/src/Markdown/Markdown.spec.md +45 -23
  270. package/src/Markdown/Markdown.test.tsx +38 -0
  271. package/src/Markdown/Markdown.tsx +24 -2
  272. package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
  273. package/src/MobileNav/MobileNav.tsx +50 -2
  274. package/src/MobileNav/MobileNavFocus.test.tsx +553 -0
  275. package/src/MoreMenu/MoreMenu.spec.md +18 -7
  276. package/src/MoreMenu/MoreMenu.test.tsx +38 -0
  277. package/src/MultiSelector/MultiSelector.doc.mjs +8 -8
  278. package/src/MultiSelector/MultiSelector.spec.md +1 -1
  279. package/src/MultiSelector/MultiSelector.test.tsx +188 -21
  280. package/src/MultiSelector/MultiSelector.tsx +50 -11
  281. package/src/NumberInput/NumberInput.test.tsx +24 -0
  282. package/src/NumberInput/NumberInput.tsx +1 -1
  283. package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
  284. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +78 -0
  285. package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -0
  286. package/src/Resizable/Resizable.spec.md +41 -9
  287. package/src/Resizable/ResizeHandle.test.tsx +565 -2
  288. package/src/Resizable/ResizeHandle.tsx +126 -21
  289. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +119 -0
  290. package/src/Resizable/__tests__/ResizeHandle.a11y.known-failures.ts +39 -0
  291. package/src/Resizable/__tests__/ResizeHandle.a11y.renders.tsx +80 -0
  292. package/src/Resizable/__tests__/ResizeHandle.a11y.states.ts +197 -0
  293. package/src/Resizable/__tests__/ResizeHandle.a11y.test.tsx +123 -0
  294. package/src/Resizable/useResizable.ts +95 -0
  295. package/src/Selector/Selector.doc.mjs +7 -6
  296. package/src/Selector/Selector.spec.md +37 -24
  297. package/src/Selector/Selector.test.tsx +176 -10
  298. package/src/Selector/Selector.tsx +59 -14
  299. package/src/Selector/__tests__/Combobox.a11y.known-failures.ts +27 -0
  300. package/src/Selector/__tests__/Combobox.a11y.states.ts +145 -0
  301. package/src/Selector/__tests__/Selector.combobox.a11y.chromium.spec.ts +108 -0
  302. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +126 -0
  303. package/src/SideNav/SideNav.test.tsx +35 -0
  304. package/src/SideNav/SideNav.tsx +16 -2
  305. package/src/Slider/Slider.spec.md +43 -17
  306. package/src/Slider/Slider.test.tsx +134 -0
  307. package/src/Slider/Slider.tsx +29 -6
  308. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +112 -0
  309. package/src/Slider/__tests__/Slider.a11y.known-failures.ts +12 -0
  310. package/src/Slider/__tests__/Slider.a11y.renders.tsx +42 -0
  311. package/src/Slider/__tests__/Slider.a11y.states.ts +106 -0
  312. package/src/Slider/__tests__/Slider.a11y.test.tsx +105 -0
  313. package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
  314. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
  315. package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
  316. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
  317. package/src/Tokenizer/Tokenizer.test.tsx +87 -0
  318. package/src/Tokenizer/Tokenizer.tsx +31 -3
  319. package/src/Toolbar/Toolbar.test.tsx +131 -4
  320. package/src/Toolbar/Toolbar.tsx +3 -1
  321. package/src/TreeList/TreeList.spec.md +33 -8
  322. package/src/TreeList/TreeList.test.tsx +72 -0
  323. package/src/TreeList/TreeList.tsx +10 -6
  324. package/src/TreeList/TreeListItem.tsx +5 -0
  325. package/src/TreeList/__tests__/TreeList.a11y.chromium.spec.ts +125 -0
  326. package/src/TreeList/__tests__/TreeList.a11y.renders.tsx +44 -0
  327. package/src/TreeList/__tests__/TreeList.a11y.states.ts +164 -0
  328. package/src/TreeList/__tests__/TreeList.a11y.test.tsx +138 -0
  329. package/src/TreeList/__tests__/TreeListRowNames.a11y.chromium.spec.ts +82 -0
  330. package/src/hooks/useScrollOverflow.test.tsx +222 -0
  331. package/src/hooks/useScrollOverflow.ts +55 -13
  332. package/src/hooks/useTreeFocus.test.tsx +37 -0
  333. package/src/hooks/useTreeFocus.ts +55 -2
  334. package/src/i18n/generated-locales/en.generated.ts +3 -0
  335. package/src/i18n/generated-locales/pseudo.generated.ts +3 -0
  336. package/src/reset.a11y.chromium.spec.ts +9 -1
  337. package/src/reset.css +8 -3
  338. package/src/reset.until-found.a11y.browser.spec.ts +75 -0
  339. package/src/reset.until-found.test.ts +32 -0
  340. package/src/stylexValidation.source-build.test.mjs +42 -0
  341. package/src/theme/syntax/SyntaxTheme.tsx +46 -5
  342. package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +91 -0
  343. package/src/theme/syntax/defineSyntaxTheme.ts +75 -15
  344. package/src/theme/syntax/index.ts +5 -1
  345. package/src/theme/syntax/tokens.ts +32 -2
  346. package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +40 -0
  347. package/src/utils/plainDate.test.ts +147 -0
  348. package/src/utils/plainDate.ts +75 -10
@@ -214,9 +214,9 @@ export const docs = {
214
214
  },
215
215
  {
216
216
  name: 'variant',
217
- type: "'input' | 'ghost'",
217
+ type: "'input' | 'secondary' | 'ghost'",
218
218
  description:
219
- 'Visual trigger style. input is the bordered input treatment for forms; ghost is borderless and matches ghost buttons for toolbar usage.',
219
+ 'Visual trigger style. input is the bordered input treatment for forms; secondary matches the secondary buttons of the active theme (borderless and neutral-filled by default) for navs and toolbars; ghost is borderless and transparent, matching ghost buttons for toolbar usage.',
220
220
  default: "'input'",
221
221
  },
222
222
  {
@@ -340,9 +340,9 @@ export const docs = {
340
340
  name: 'statusVariant',
341
341
  type: "'attached' | 'detached' | 'tooltip'",
342
342
  description:
343
- 'How the status message is placed relative to the input. attached overlaps directly below the bordered input and is only valid for the input variant; ghost selectors detach attached status messages by default. Use tooltip for compact toolbar controls.',
343
+ 'How the status message is placed relative to the input. attached overlaps directly below the bordered input and is only valid for the input variant; secondary and ghost selectors detach attached status messages by default. Use tooltip for compact toolbar controls.',
344
344
  default:
345
- "'attached' for input selectors; 'detached' for ghost selectors",
345
+ "'attached' for input selectors; 'detached' for secondary and ghost selectors",
346
346
  },
347
347
  {
348
348
  name: 'renderOption',
@@ -493,7 +493,7 @@ export const docs = {
493
493
  {
494
494
  guidance: true,
495
495
  description:
496
- 'Use variant="ghost" when a multi-selector sits in a toolbar with ghost buttons. If validation status is needed there, prefer statusVariant="tooltip" so the toolbar height stays compact.',
496
+ 'Use variant="secondary" when a multi-selector sits beside secondary buttons or segmented controls, and variant="ghost" when it sits beside ghost buttons. If validation status is needed there, prefer statusVariant="tooltip" so the toolbar height stays compact.',
497
497
  },
498
498
  {
499
499
  guidance: true,
@@ -661,7 +661,7 @@ export const docsDense = {
661
661
  {
662
662
  guidance: true,
663
663
  description:
664
- 'Use variant="ghost" in toolbars with ghost buttons; prefer statusVariant="tooltip" for compact validation status.',
664
+ 'Use variant="secondary" beside secondary buttons or segmented controls and variant="ghost" beside ghost buttons; prefer statusVariant="tooltip" for compact validation status.',
665
665
  },
666
666
  {
667
667
  guidance: false,
@@ -693,7 +693,7 @@ export const docsDense = {
693
693
  placeholder: 'text when nothing selected',
694
694
  size: 'size variant',
695
695
  variant:
696
- 'visual trigger style: input bordered control or ghost toolbar control',
696
+ 'visual trigger style: input bordered control, secondary control matching the theme secondary buttons, or ghost transparent control',
697
697
  triggerDisplay: 'how to show selected in trigger',
698
698
  formatValue:
699
699
  'formats count/labels trigger text; receives selected items',
@@ -719,7 +719,7 @@ export const docsDense = {
719
719
  isLoading: 'spinner in trigger',
720
720
  status: 'validation status w/ optional message',
721
721
  statusVariant:
722
- 'status message placement; ghost detaches attached by default; use tooltip for compact toolbars.',
722
+ 'status message placement; secondary and ghost detach attached by default; use tooltip for compact toolbars.',
723
723
  renderOption:
724
724
  'custom render fn per selectable option; not dividers/sections/select-all',
725
725
  renderTrigger:
@@ -31,7 +31,7 @@ architecture:
31
31
  architecture:public-component-api,
32
32
  ]
33
33
  contributing: []
34
- system_specs: [spec:AST-011/DEC-1]
34
+ system_specs: [spec:AST-011/DEC-1, spec:AST-079/DEC-1]
35
35
  ---
36
36
 
37
37
  # MultiSelector component contract
@@ -25,7 +25,7 @@ import {
25
25
  type MultiSelectorHandle,
26
26
  type MultiSelectorChange,
27
27
  } from './MultiSelector';
28
- import {useRef} from 'react';
28
+ import {useRef, useState} from 'react';
29
29
  import {Icon} from '../Icon';
30
30
  import {InternationalizationProvider} from '../i18n';
31
31
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
@@ -33,6 +33,8 @@ import {__resetInteractionModalityForTest} from '../utils/interactionModality';
33
33
  import {defineTheme} from '../theme/defineTheme';
34
34
  import {generateThemeCSS} from '../theme/generateThemeRules';
35
35
  import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
36
+ import {groupStyles} from '../InputGroup/groupStyles';
37
+ import {InputGroup, InputGroupText} from '../InputGroup';
36
38
 
37
39
  function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
38
40
  const {prose, component} = generateThemeCSS(theme);
@@ -219,6 +221,54 @@ describe('MultiSelector', () => {
219
221
  expect(screen.getByLabelText('Fruit')).toBeInTheDocument();
220
222
  });
221
223
 
224
+ it('applies distinct trigger chrome to the secondary variant', () => {
225
+ // data-testid reaches the native control; the variant is on the trigger root.
226
+ const triggerRoot = () =>
227
+ screen
228
+ .getByTestId('selector')
229
+ .closest('.astryx-multi-selector') as HTMLElement;
230
+ const {rerender} = render(
231
+ <MultiSelector
232
+ label="Fruit"
233
+ options={defaultOptions}
234
+ value={[]}
235
+ onChange={() => {}}
236
+ variant="input"
237
+ data-testid="selector"
238
+ />,
239
+ );
240
+ const inputClasses = triggerRoot().className;
241
+
242
+ rerender(
243
+ <MultiSelector
244
+ label="Fruit"
245
+ options={defaultOptions}
246
+ value={[]}
247
+ onChange={() => {}}
248
+ variant="secondary"
249
+ data-testid="selector"
250
+ />,
251
+ );
252
+ const secondary = triggerRoot();
253
+ const secondaryClasses = secondary.className;
254
+ expect(secondary).toHaveAttribute('data-variant', 'secondary');
255
+
256
+ rerender(
257
+ <MultiSelector
258
+ label="Fruit"
259
+ options={defaultOptions}
260
+ value={[]}
261
+ onChange={() => {}}
262
+ variant="ghost"
263
+ data-testid="selector"
264
+ />,
265
+ );
266
+ const ghostClasses = triggerRoot().className;
267
+
268
+ expect(secondaryClasses).not.toBe(inputClasses);
269
+ expect(secondaryClasses).not.toBe(ghostClasses);
270
+ });
271
+
222
272
  it('renders custom option content with renderOption', async () => {
223
273
  const user = userEvent.setup();
224
274
  render(
@@ -1595,6 +1645,55 @@ describe('MultiSelector', () => {
1595
1645
  expect(clear).not.toHaveAttribute('tabIndex', '-1');
1596
1646
  });
1597
1647
 
1648
+ it('returns focus to the trigger when the clear button is activated by keyboard', async () => {
1649
+ function Controlled() {
1650
+ const [value, setValue] = useState(['Apple', 'Banana']);
1651
+ return (
1652
+ <MultiSelector
1653
+ label="Fruit"
1654
+ options={defaultOptions}
1655
+ value={value}
1656
+ onChange={setValue}
1657
+ hasClear
1658
+ />
1659
+ );
1660
+ }
1661
+ const user = userEvent.setup();
1662
+ render(<Controlled />);
1663
+ screen.getByRole('button', {name: 'Clear all Fruit'}).focus();
1664
+ await user.keyboard('{Enter}');
1665
+ expect(
1666
+ screen.queryByRole('button', {name: 'Clear all Fruit'}),
1667
+ ).not.toBeInTheDocument();
1668
+ expect(screen.getByRole('combobox')).toHaveFocus();
1669
+ });
1670
+
1671
+ it('leaves focus in the search input when the clear button is clicked with the list open', async () => {
1672
+ function Controlled() {
1673
+ const [value, setValue] = useState(['Apple', 'Banana']);
1674
+ return (
1675
+ <MultiSelector
1676
+ label="Fruit"
1677
+ options={defaultOptions}
1678
+ value={value}
1679
+ onChange={setValue}
1680
+ hasClear
1681
+ hasSearch
1682
+ />
1683
+ );
1684
+ }
1685
+ const user = userEvent.setup();
1686
+ const {container} = render(<Controlled />);
1687
+ await user.click(container.querySelector('[aria-haspopup]') as Element);
1688
+ const search = screen.getByRole('combobox', h);
1689
+ search.focus();
1690
+ await user.click(screen.getByRole('button', {name: 'Clear all Fruit'}));
1691
+ expect(
1692
+ screen.queryByRole('button', {name: 'Clear all Fruit'}),
1693
+ ).not.toBeInTheDocument();
1694
+ expect(search).toHaveFocus();
1695
+ });
1696
+
1598
1697
  it('scrolls the highlighted option into view during arrow navigation', async () => {
1599
1698
  const scrollIntoView = vi.fn();
1600
1699
  Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
@@ -2396,26 +2495,29 @@ describe('MultiSelector statusVariant forwarding', () => {
2396
2495
  ).not.toBeNull();
2397
2496
  });
2398
2497
 
2399
- it('detaches attached status by default for the ghost variant', () => {
2400
- const {container} = render(
2401
- <MultiSelector
2402
- label="Fruit"
2403
- options={['Apple', 'Banana']}
2404
- value={[]}
2405
- onChange={() => {}}
2406
- variant="ghost"
2407
- status={{type: 'error', message: 'Required'}}
2408
- />,
2409
- );
2410
- expect(container.querySelector('.astryx-multi-selector')).toHaveAttribute(
2411
- 'data-variant',
2412
- 'ghost',
2413
- );
2414
- expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
2415
- 'data-variant',
2416
- 'detached',
2417
- );
2418
- });
2498
+ it.each(['secondary', 'ghost'] as const)(
2499
+ 'detaches attached status by default for the %s variant',
2500
+ variant => {
2501
+ const {container} = render(
2502
+ <MultiSelector
2503
+ label="Fruit"
2504
+ options={['Apple', 'Banana']}
2505
+ value={[]}
2506
+ onChange={() => {}}
2507
+ variant={variant}
2508
+ status={{type: 'error', message: 'Required'}}
2509
+ />,
2510
+ );
2511
+ expect(container.querySelector('.astryx-multi-selector')).toHaveAttribute(
2512
+ 'data-variant',
2513
+ variant,
2514
+ );
2515
+ expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
2516
+ 'data-variant',
2517
+ 'detached',
2518
+ );
2519
+ },
2520
+ );
2419
2521
 
2420
2522
  it('uses a status tooltip for ghost multi-selectors when requested', () => {
2421
2523
  const {container} = render(
@@ -4206,3 +4308,68 @@ describe('MultiSelector create row inside a grid', () => {
4206
4308
  });
4207
4309
  });
4208
4310
  });
4311
+
4312
+ describe('MultiSelector in an InputGroup', () => {
4313
+ const groupClasses = () =>
4314
+ stylex.props(groupStyles.inGroup).className!.split(' ');
4315
+ const triggerBox = (name: RegExp) =>
4316
+ screen
4317
+ .getByRole('combobox', {name})
4318
+ .closest('.astryx-multi-selector') as HTMLElement;
4319
+
4320
+ it.each(['input', 'secondary'] as const)(
4321
+ 'gives a grouped %s trigger the connected group geometry in every position',
4322
+ variant => {
4323
+ const selector = (label: string) => (
4324
+ <MultiSelector
4325
+ label={label}
4326
+ isLabelHidden
4327
+ variant={variant}
4328
+ options={['Apple', 'Banana', 'Cherry']}
4329
+ value={EMPTY_VALUE}
4330
+ onChange={() => {}}
4331
+ />
4332
+ );
4333
+ render(
4334
+ <>
4335
+ <InputGroup label="First">
4336
+ {selector('Fruit first')}
4337
+ <InputGroupText>kg</InputGroupText>
4338
+ </InputGroup>
4339
+ <InputGroup label="Middle">
4340
+ <InputGroupText>#</InputGroupText>
4341
+ {selector('Fruit middle')}
4342
+ <InputGroupText>kg</InputGroupText>
4343
+ </InputGroup>
4344
+ <InputGroup label="Last">
4345
+ <InputGroupText>#</InputGroupText>
4346
+ {selector('Fruit last')}
4347
+ </InputGroup>
4348
+ </>,
4349
+ );
4350
+
4351
+ // Row height, seam radii, and focus stacking come from these classes;
4352
+ // a secondary trigger keeps its fill but not its standalone edges.
4353
+ for (const position of ['first', 'middle', 'last']) {
4354
+ const classes = new Set(
4355
+ triggerBox(new RegExp(`Fruit ${position}`)).className.split(' '),
4356
+ );
4357
+ expect(groupClasses().every(c => classes.has(c))).toBe(true);
4358
+ }
4359
+ },
4360
+ );
4361
+
4362
+ it('keeps a standalone secondary trigger out of the group geometry', () => {
4363
+ render(
4364
+ <MultiSelector
4365
+ label="Fruit"
4366
+ variant="secondary"
4367
+ options={['Apple', 'Banana', 'Cherry']}
4368
+ value={EMPTY_VALUE}
4369
+ onChange={() => {}}
4370
+ />,
4371
+ );
4372
+ const classes = new Set(triggerBox(/Fruit/).className.split(' '));
4373
+ expect(groupClasses().some(c => classes.has(c))).toBe(false);
4374
+ });
4375
+ });
@@ -5,13 +5,14 @@
5
5
  /**
6
6
  * @file MultiSelector.tsx
7
7
  * @input Uses React, StyleX, usePopover, useTooltip, CheckboxInput, Field, Badge, Icon, InputGroupContext
8
- * @output Exports MultiSelector component
8
+ * @output Exports MultiSelector with input, secondary, and ghost trigger variants
9
9
  * @position Core implementation; consumed by index.ts
10
10
  *
11
11
  * SYNC: When modified, update:
12
12
  * - /packages/core/src/MultiSelector/MultiSelector.doc.mjs
13
13
  * - /packages/core/src/MultiSelector/MultiSelector.test.tsx
14
14
  * - /packages/core/src/MultiSelector/index.ts
15
+ * - /apps/storybook/stories/MultiSelector.stories.tsx
15
16
  * - /apps/storybook/stories/InputGroup.stories.tsx
16
17
  * - /packages/cli/assets/templates/blocks/components/MultiSelector/ (showcase blocks)
17
18
  */
@@ -241,6 +242,31 @@ const styles = stylex.create({
241
242
  '[data-astryx-press="on"]': 'scale(0.98)',
242
243
  },
243
244
  },
245
+ // Keep the input-family width contract while reusing ghost's borderless paint
246
+ // and the neutral resting fill used by secondary buttons.
247
+ triggerSecondary: {
248
+ borderWidth: 0,
249
+ backgroundColor: colorVars['--color-neutral'],
250
+ boxShadow: {
251
+ default: 'none',
252
+ ':hover:not(:focus-within):where(:not(:disabled,[aria-disabled="true"]))':
253
+ {
254
+ '@media (hover: hover)': 'none',
255
+ },
256
+ ':focus-within': 'none',
257
+ },
258
+ fontWeight: fontWeightVars['--font-weight-medium'],
259
+ transitionProperty:
260
+ 'background-image, background-color, color, opacity, transform',
261
+ transform: {
262
+ default: 'scale(1)',
263
+ ':active': {
264
+ default: 'scale(0.98)',
265
+ '@media (pointer: coarse)': 'scale(1)',
266
+ },
267
+ '[data-astryx-press="on"]': 'scale(0.98)',
268
+ },
269
+ },
244
270
  triggerGhostDisabled: {
245
271
  backgroundImage: 'none',
246
272
  transform: {
@@ -494,7 +520,7 @@ const STATUS_BUTTON_LABEL_KEY: Record<MultiSelectorStatusType, string> = {
494
520
 
495
521
  export type MultiSelectorSize = 'sm' | 'md' | 'lg';
496
522
 
497
- export type MultiSelectorVariant = 'input' | 'ghost';
523
+ export type MultiSelectorVariant = 'input' | 'secondary' | 'ghost';
498
524
 
499
525
  export type MultiSelectorPresentation = AdaptivePresentation;
500
526
 
@@ -736,7 +762,9 @@ export interface MultiSelectorProps<
736
762
  /**
737
763
  * Visual style of the selector trigger.
738
764
  * - 'input': bordered input-style trigger for forms
739
- * - 'ghost': borderless trigger matching ghost buttons, for toolbars
765
+ * - 'secondary': matches the theme's secondary buttons (borderless and
766
+ * neutral-filled by default)
767
+ * - 'ghost': borderless, transparent trigger matching ghost buttons
740
768
  * @default 'input'
741
769
  */
742
770
  variant?: MultiSelectorVariant;
@@ -750,7 +778,7 @@ export interface MultiSelectorProps<
750
778
  * - 'attached': message overlaps directly below the bordered input (input variant only)
751
779
  * - 'detached': message floats below as a separate element with spacing
752
780
  * - 'tooltip': message is exposed from the on-field status icon
753
- * @default 'attached' for input selectors; 'detached' for ghost selectors
781
+ * @default 'attached' for input selectors; 'detached' for secondary and ghost selectors
754
782
  */
755
783
  statusVariant?: FieldStatusVariant;
756
784
 
@@ -1114,7 +1142,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1114
1142
  emptySearchTextFromProps ?? t('@astryx.multiSelector.emptySearchResults');
1115
1143
  const size = useSize(sizeProp, 'md');
1116
1144
  const effectiveStatusVariant =
1117
- variant === 'ghost' && statusVariant === 'attached'
1145
+ variant !== 'input' && statusVariant === 'attached'
1118
1146
  ? 'detached'
1119
1147
  : statusVariant;
1120
1148
  const isEffectivelyReadOnly = isReadOnly && !isDisabled;
@@ -1569,6 +1597,14 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1569
1597
  (e: React.MouseEvent) => {
1570
1598
  e.stopPropagation(); // Don't open dropdown
1571
1599
  clearValues();
1600
+ // A keyboard activation (detail 0) leaves focus on the clear button,
1601
+ // which unmounts once the value is empty and would drop focus to <body>;
1602
+ // hand it to the trigger. A pointer press never focuses the button, so
1603
+ // there is no focus to repair, and moving it would pull focus out of an
1604
+ // open list's search input.
1605
+ if (e.detail === 0) {
1606
+ triggerRef.current?.focus();
1607
+ }
1572
1608
  },
1573
1609
  [clearValues],
1574
1610
  );
@@ -2465,25 +2501,28 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
2465
2501
  inputWrapperStyles.base,
2466
2502
  styles.triggerContainer,
2467
2503
  sizeStyles[size],
2504
+ variant === 'secondary' && styles.triggerSecondary,
2468
2505
  variant === 'ghost' && styles.triggerGhost,
2469
- variant === 'ghost' && interactionOverlayStyles.backgroundImage,
2470
- variant === 'ghost' && focusOutlineStyles.focusWithin,
2506
+ variant !== 'input' && interactionOverlayStyles.backgroundImage,
2507
+ variant !== 'input' && focusOutlineStyles.focusWithin,
2471
2508
  surface.isTriggerFocusRingSuppressed &&
2472
2509
  selectorPresentationStyles.pointerRestoredFocus,
2473
2510
  isDisabled && inputWrapperStyles.disabled,
2474
2511
  isEffectivelyReadOnly && styles.triggerReadOnly,
2475
- variant === 'ghost' && isDisabled && styles.triggerGhostDisabled,
2476
- variant === 'ghost' &&
2512
+ variant !== 'input' && isDisabled && styles.triggerGhostDisabled,
2513
+ variant !== 'input' &&
2477
2514
  isEffectivelyReadOnly &&
2478
2515
  styles.triggerGhostReadOnly,
2479
2516
  optimisticValue.length === 0 && styles.triggerPlaceholder,
2480
- variant !== 'ghost' &&
2517
+ variant === 'input' &&
2481
2518
  status &&
2482
2519
  inputStatusBorderStyles[status.type],
2483
- variant !== 'ghost' &&
2520
+ variant === 'input' &&
2484
2521
  status &&
2485
2522
  !isDisabled &&
2486
2523
  inputStatusHoverShadowStyles[status.type],
2524
+ // Grouped secondary triggers keep their neutral fill but take the
2525
+ // group's row height, connected edge radii, and focus stacking.
2487
2526
  variant !== 'ghost' && inputGroup && groupStyles.inGroup,
2488
2527
  controlXstyle,
2489
2528
  ),
@@ -1099,6 +1099,30 @@ describe('NumberInput', () => {
1099
1099
  expect(screen.getByRole('alert')).toHaveTextContent('Invalid number');
1100
1100
  });
1101
1101
 
1102
+ it('resolves the invalid-number announcement from the i18n catalog', async () => {
1103
+ const user = userEvent.setup();
1104
+ render(
1105
+ <InternationalizationProvider
1106
+ locale="ar-SA"
1107
+ overrides={{
1108
+ 'ar-SA': {'@astryx.numberInput.invalidNumber': 'رقم غير صالح'},
1109
+ }}>
1110
+ <NumberInput
1111
+ label="Count"
1112
+ value={null}
1113
+ onChange={() => {}}
1114
+ isIntegerOnly
1115
+ />
1116
+ </InternationalizationProvider>,
1117
+ );
1118
+
1119
+ const input = screen.getByRole('spinbutton');
1120
+ await user.click(input);
1121
+ await user.type(input, '3.5');
1122
+
1123
+ expect(screen.getByRole('alert')).toHaveTextContent('رقم غير صالح');
1124
+ });
1125
+
1102
1126
  it('does not announce an alert message when input is valid', async () => {
1103
1127
  const user = userEvent.setup();
1104
1128
  render(<NumberInput label="Count" value={null} onChange={() => {}} />);
@@ -977,7 +977,7 @@ export function NumberInput({
977
977
  user would get no feedback that their entry was rejected (WCAG 3.3.1).
978
978
  */}
979
979
  <VisuallyHidden as="div" role="alert" aria-live="assertive">
980
- {!isInputValid ? 'Invalid number' : ''}
980
+ {!isInputValid ? t('@astryx.numberInput.invalidNumber') : ''}
981
981
  </VisuallyHidden>
982
982
  {hasClear && value != null && !isDisabled && !isReadOnly && (
983
983
  <InputClearButton
@@ -71,21 +71,6 @@ async function runState(
71
71
  });
72
72
  }
73
73
 
74
- test('every expectation is exercised by at least one NumberInput state', async ({
75
- page,
76
- }) => {
77
- test.setTimeout(4 * 60 * 1000);
78
- const cdp = await page.context().newCDPSession(page);
79
- const results: BindingResult[] = [];
80
- for (const state of NUMBER_INPUT_A11Y_STATES) {
81
- results.push(await runState(page, cdp, state));
82
- }
83
- expect(neverExercised(results)).toEqual([]);
84
- expect(unmatchedKnownFailures(NUMBER_INPUT_KNOWN_FAILURES, results)).toEqual(
85
- [],
86
- );
87
- });
88
-
89
74
  test('the Chromium read-only omission remains exact visible debt', async ({
90
75
  page,
91
76
  }) => {
@@ -104,13 +89,35 @@ test('the Chromium read-only omission remains exact visible debt', async ({
104
89
  expect(readOnly?.knownFailure).toBe(NUMBER_INPUT_KNOWN_FAILURES[0]);
105
90
  });
106
91
 
92
+ // Each state's result, recorded by its own test below so the whole-suite check
93
+ // at the end reads it instead of measuring every state a second time. A state
94
+ // whose test did not run in this worker (a filtered run, or a worker restarted
95
+ // after a failure) is measured by the whole-suite check itself.
96
+ const recorded = new Map<string, BindingResult>();
97
+
107
98
  for (const state of NUMBER_INPUT_A11Y_STATES) {
108
99
  test(`NumberInput [${state.id}] — ${state.summary}`, async ({page}) => {
109
100
  const cdp = await page.context().newCDPSession(page);
110
101
  const result = await runState(page, cdp, state);
102
+ recorded.set(state.id, result);
111
103
  const report = summarize(SPINBUTTON_PATTERN, [result]);
112
104
  expect(formatFailures(blockingResults([result]))).toBe('');
113
105
  expect(report.unrunLayers).toEqual([]);
114
106
  expect(report.counts.unexpectedPass).toBe(0);
115
107
  });
116
108
  }
109
+
110
+ test('every expectation is exercised by at least one NumberInput state', async ({
111
+ page,
112
+ }) => {
113
+ test.setTimeout(4 * 60 * 1000);
114
+ const cdp = await page.context().newCDPSession(page);
115
+ const results: BindingResult[] = [];
116
+ for (const state of NUMBER_INPUT_A11Y_STATES) {
117
+ results.push(recorded.get(state.id) ?? (await runState(page, cdp, state)));
118
+ }
119
+ expect(neverExercised(results)).toEqual([]);
120
+ expect(unmatchedKnownFailures(NUMBER_INPUT_KNOWN_FAILURES, results)).toEqual(
121
+ [],
122
+ );
123
+ });
@@ -18,6 +18,7 @@ import {
18
18
  waitFor,
19
19
  within,
20
20
  } from '@testing-library/react';
21
+ import userEvent from '@testing-library/user-event';
21
22
  import React, {useState} from 'react';
22
23
  import * as stylex from '@stylexjs/stylex';
23
24
  import {layerViewportInset} from '../Layer/layerViewportInset.stylex';
@@ -1110,3 +1111,80 @@ describe('empty-operator token with an Editor override', () => {
1110
1111
  expect(onChange).not.toHaveBeenCalled();
1111
1112
  });
1112
1113
  });
1114
+
1115
+ describe('Enter on a focused footer button (#7437)', () => {
1116
+ const validFilter: PowerSearchFilter = {
1117
+ field: 'status',
1118
+ operator: 'is',
1119
+ value: {type: 'string', value: 'example'},
1120
+ };
1121
+
1122
+ function Harness({
1123
+ onSave,
1124
+ onCancel,
1125
+ }: {
1126
+ onSave: (filter: PowerSearchFilter | null) => void;
1127
+ onCancel: () => void;
1128
+ }) {
1129
+ const internalConfig = useInternalConfig(testConfig);
1130
+ return (
1131
+ <PowerSearchEditPopover
1132
+ config={internalConfig}
1133
+ filter={validFilter}
1134
+ mode="edit"
1135
+ onSave={onSave}
1136
+ onCancel={onCancel}
1137
+ />
1138
+ );
1139
+ }
1140
+
1141
+ it('cancels without saving when Cancel has focus', async () => {
1142
+ const onSave = vi.fn();
1143
+ const onCancel = vi.fn();
1144
+ const user = userEvent.setup();
1145
+ render(<Harness onSave={onSave} onCancel={onCancel} />);
1146
+
1147
+ screen.getByRole('button', {name: 'Cancel'}).focus();
1148
+ await user.keyboard('{Enter}');
1149
+
1150
+ expect(onCancel).toHaveBeenCalledTimes(1);
1151
+ expect(onSave).not.toHaveBeenCalled();
1152
+ });
1153
+
1154
+ it('deletes the filter when Delete has focus', async () => {
1155
+ const onSave = vi.fn();
1156
+ const user = userEvent.setup();
1157
+ render(<Harness onSave={onSave} onCancel={() => {}} />);
1158
+
1159
+ screen.getByRole('button', {name: 'Delete'}).focus();
1160
+ await user.keyboard('{Enter}');
1161
+
1162
+ expect(onSave).toHaveBeenCalledTimes(1);
1163
+ expect(onSave).toHaveBeenCalledWith(null);
1164
+ });
1165
+
1166
+ it('saves once when Apply has focus', async () => {
1167
+ const onSave = vi.fn();
1168
+ const user = userEvent.setup();
1169
+ render(<Harness onSave={onSave} onCancel={() => {}} />);
1170
+
1171
+ screen.getByRole('button', {name: 'Apply'}).focus();
1172
+ await user.keyboard('{Enter}');
1173
+
1174
+ expect(onSave).toHaveBeenCalledTimes(1);
1175
+ expect(onSave).toHaveBeenCalledWith(validFilter);
1176
+ });
1177
+
1178
+ it('still cancels on Escape when a button has focus', async () => {
1179
+ const onSave = vi.fn();
1180
+ const onCancel = vi.fn();
1181
+ const user = userEvent.setup();
1182
+ render(<Harness onSave={onSave} onCancel={onCancel} />);
1183
+
1184
+ screen.getByRole('button', {name: 'Delete'}).focus();
1185
+ await user.keyboard('{Escape}');
1186
+
1187
+ expect(onCancel).toHaveBeenCalledTimes(1);
1188
+ expect(onSave).not.toHaveBeenCalled();
1189
+ });
1190
+ });
@@ -793,6 +793,12 @@ export function PowerSearchEditPopover({
793
793
  if (isImeKeyEvent(e.nativeEvent)) {
794
794
  return;
795
795
  }
796
+ // A focused button owns its Enter: Cancel cancels, Delete deletes, and
797
+ // the date picker's calendar button opens the calendar. Enter in a
798
+ // text or number value field still saves.
799
+ if (e.key === 'Enter' && e.target instanceof HTMLButtonElement) {
800
+ return;
801
+ }
796
802
  if (e.key === 'Enter' && !isSaveDisabled && !e.defaultPrevented) {
797
803
  e.preventDefault();
798
804
  handleSave();