@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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  schema_version: 3
3
- template_version: 3
3
+ template_version: 7
4
4
  kind: component
5
5
  id: component:CommandPalette
6
6
  authority: draft
@@ -18,6 +18,7 @@ verified_by:
18
18
  packages/core/src/CommandPalette/CommandPaletteItem.test.tsx,
19
19
  packages/core/src/CommandPalette/CommandPaletteGroup.test.tsx,
20
20
  packages/core/src/CommandPalette/CommandPaletteFooter.test.tsx,
21
+ packages/core/src/CommandPalette/__tests__/CommandPalette.a11y.chromium.spec.ts,
21
22
  packages/core/src/CommandPalette/__tests__/CommandPaletteFooter.a11y.chromium.spec.ts,
22
23
  packages/core/src/theme/themingTargets.test.ts,
23
24
  scripts/check-knowledge.mjs,
@@ -32,24 +33,43 @@ architecture:
32
33
  architecture:public-component-api,
33
34
  ]
34
35
  contributing: []
35
- system_specs: []
36
+ system_specs: [spec:AST-056]
36
37
  ---
37
38
 
38
39
  # CommandPalette component contract
39
40
 
41
+ ## Contract at a glance
42
+
43
+ | Area | Contract |
44
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | Public contract | Prop and visual APIs are unchanged. The result surface exposes listbox semantics only with selectable results; the query input has a real default name; rich Empty content remains ordinary. |
46
+ | Behavior | Modal Escape delegates to Dialog; query-field Home/End and IME remain native; result changes reset stale highlight; Empty announcements mirror rendered content. |
47
+ | End-user impact | Nested dismissal and native text entry are preserved; assistive technology receives the visible empty message, an independent query-field name, and only existing active options. |
48
+ | Builder impact | None; no migration or new caller choice. |
49
+ | Compatibility/readiness | Visual output is preserved. Empty-result ARIA, query-field accessible naming, empty-state announcement, and highlight reset behavior are corrected and verified. |
50
+ | Review checks | Reject local modal Escape handling, IME routing, Home/End result navigation, empty listboxes, stale active descendants, placeholder-only names, mismatched Empty announcements, or gaps. |
51
+ | Governing rules | `family:overlay-dismissal/FR1`, `FR2`, `FR4`, `FR5`; `spec:AST-056/FR3`, `AR1`, `AR2`; existing public component and delegated-owner contracts. |
52
+
53
+ This table is a review projection; the body below is authoritative.
54
+
40
55
  ## Intent
41
56
 
42
- CommandPalette presents searchable commands inside a Dialog. This draft records
43
- its current aggregate consumer anatomy, target ownership, and delegated stable
44
- parts without changing runtime behavior, styling, targets, or public API.
57
+ CommandPalette presents searchable commands inside a Dialog. It owns search and
58
+ selection orchestration plus the aggregate consumer anatomy while delegating its
59
+ surface and modal dismissal ordering to Dialog.
45
60
 
46
61
  ## Compatibility and migration
47
62
 
48
- - Released default preserved: `yes`
49
- - Compatibility class: additive documentation only; runtime, DOM, styling,
50
- targets, aliases, and public API remain unchanged
63
+ - Released default preserved: `yes` for props and visual output
64
+ - Compatibility class: behavioral and accessibility correction; empty states
65
+ conditionally omit listbox, expanded, controls, and active-descendant ARIA
66
+ attributes until selectable results exist; the default query accessible name
67
+ is independent of its placeholder; empty-search announcements mirror the
68
+ rendered content; public props, styling, targets, and visible defaults remain
69
+ unchanged
51
70
  - Controlled/uncontrolled behavior: unchanged
52
- - Migration decision: none
71
+ - Migration decision: adopts `family:overlay-dismissal/FR1`, `FR2`, `FR4`, and
72
+ `FR5` through Dialog; no caller migration
53
73
 
54
74
  Consumer migration instructions belong in consumer docs and release notes.
55
75
 
@@ -57,9 +77,9 @@ Consumer migration instructions belong in consumer docs and release notes.
57
77
 
58
78
  **Owns**
59
79
 
60
- - Search orchestration and the current Input, List, Item, Group, Group heading,
61
- Empty, and Footer parts and targets.
62
- - The native Query field rendered inside the default Input.
80
+ - Search orchestration and composition of the current Input, List, Item, Group,
81
+ Group heading, Empty, and Footer parts and targets.
82
+ - Composition of the native Query field rendered inside the default Input; the field's semantics and native passthrough are owned by `component:CommandPaletteInput`.
63
83
 
64
84
  **Does not own / non-goals**
65
85
 
@@ -67,6 +87,8 @@ Consumer migration instructions belong in consumer docs and release notes.
67
87
  badges — owned by their respective components.
68
88
  - Caller-provided replacement Input, Footer, item content, or trailing input
69
89
  content — owned by the product callsite.
90
+ - The Item option's passthrough, pointer, disabled, selected, highlighted, and
91
+ standalone-scroll semantics — owned by `component:CommandPaletteItem`.
70
92
  - A `command-palette` root target — no such current target exists, and this
71
93
  factual backfill does not invent one.
72
94
  - Shared layer hosting, positioning, or lifecycle rules beyond the current
@@ -81,12 +103,16 @@ documented in `CommandPalette.doc.mjs` and its subcomponent docs.
81
103
 
82
104
  ## Behavioral and layout contract
83
105
 
84
- | ID | Candidate invariant | Basis | Draft review state |
85
- | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------- |
86
- | FR1 | The current default render places Input and Footer around a List inside a delegated Dialog; the List contains Items, optional Groups and Group headings, or Empty according to the current results. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
87
- | FR2 | Input, List, Item, Group, Group heading, Empty, and Footer carry the seven current `command-palette-*` targets documented below; no `command-palette` root target exists. | Current source and target docs | Verified current inventory; focused placement covers Group, Group heading, and Footer |
88
- | FR3 | The default Input delegates its Search glyph to Icon and pending Loading spinner to Spinner; the default Footer delegates keyboard shortcuts to Kbd; the containing surface delegates to Dialog. | Current source and component docs | Footer-to-Kbd composition is focused-test verified; no ownership change |
89
- | FR4 | Query field is a distinct native text field inside Input and currently has no separate public target. The `command-palette-input` target is on the surrounding search region, not the native field. | Current source and target docs | Verified current reachability; long-term theming intent remains unsettled |
106
+ | ID | Candidate invariant | Basis | Draft review state |
107
+ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
108
+ | FR1 | The current default render places Input and Footer around a result surface inside a delegated Dialog. That surface is a List listbox while Items exist and an ordinary Empty container otherwise. | Current source, docs, tests, and ARIA required-child rule | Verified current anatomy and valid rich Empty semantics |
109
+ | FR2 | Input, List, Item, Group, Group heading, Empty, and Footer carry the seven current `command-palette-*` targets documented below; no `command-palette` root target exists. | Current source and target docs | Verified current inventory and focused placement coverage |
110
+ | FR3 | The default Input delegates its Search glyph to Icon and pending Loading spinner to Spinner; the default Footer delegates keyboard shortcuts to Kbd; the containing surface delegates to Dialog. | Current source and component docs | Verified current composition; no ownership change |
111
+ | FR4 | Query field is a distinct native text field inside Input and currently has no separate public target. The `command-palette-input` target is on the surrounding search region, not the native field. | Current source and target docs | Verified current reachability; long-term theming intent remains unsettled |
112
+ | FR5 | A modal palette leaves Escape unclaimed for Dialog's shared dismissal stack so a nested member handles the first press; inline previews close locally because they do not register as layers. | `family:overlay-dismissal/FR1`, `FR2`, `FR4`, `FR5` | Verified by nested and inline integration tests |
113
+ | FR6 | The editable query field keeps Home and End for native caret movement, uses PageUp and PageDown for first/last result navigation, and does not route active IME composition keys to commands. | WAI-ARIA editable combobox convention; objective IME integrity | Verified by user-event navigation and composition-key tests |
114
+ | FR7 | A committed result set resets highlight before exposure; the query input emits active-descendant only for an index that exists in the current selectable set. | WAI-ARIA active-descendant integrity | Verified across populated, empty, and repopulated results |
115
+ | FR8 | Empty search announcements mirror rendered `emptySearchText`, including rich caller content; the query input's default accessible name is independent of its placeholder. | `spec:AST-056/FR3`, `AR1`, `AR2` | Verified by live-region and accessible-name tests |
90
116
 
91
117
  ### Allowed variation
92
118
 
@@ -99,14 +125,14 @@ documented in `CommandPalette.doc.mjs` and its subcomponent docs.
99
125
 
100
126
  ### Representative states
101
127
 
102
- | State | Required invariant | Allowed variation |
103
- | ---------------------- | ------------------------------------------------------- | ----------------------------------------------- |
104
- | Ungrouped results | List contains Item instances. | Item count and caller-rendered content. |
105
- | Grouped results | List contains Group, Group heading, and Item instances. | Group names, count, ordering, and item content. |
106
- | Empty bootstrap/search | List contains Empty instead of Items. | Caller-provided empty content. |
107
- | Pending search | Default Input may contain Loading spinner. | Spinner is absent when no search is pending. |
108
- | Default slots | Input and Footer render their documented defaults. | Footer shortcut text and translated labels. |
109
- | Caller-replaced slots | The slot content replaces the corresponding default. | Replacement structure remains caller-owned. |
128
+ | State | Required invariant | Allowed variation |
129
+ | ---------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------- |
130
+ | Ungrouped results | List contains Item instances. | Item count and caller-rendered content. |
131
+ | Grouped results | List contains Group, Group heading, and Item instances. | Group names, count, ordering, and item content. |
132
+ | Empty bootstrap/search | Empty replaces listbox semantics; search-empty text is announced as rendered. | Caller-provided rich content, including ordinary controls. |
133
+ | Pending search | Default Input may contain Loading spinner. | Spinner is absent when no search is pending. |
134
+ | Default slots | Input and Footer render their documented defaults. | Footer shortcut text and translated labels. |
135
+ | Caller-replaced slots | The slot content replaces the corresponding default. | Replacement structure remains caller-owned. |
110
136
 
111
137
  ### Transformation and precedence order
112
138
 
@@ -118,8 +144,27 @@ documented in `CommandPalette.doc.mjs` and its subcomponent docs.
118
144
 
119
145
  ## Accessibility contract
120
146
 
121
- This draft does not change or extend CommandPalette's current Dialog, combobox,
122
- listbox, option, announcement, or keyboard behavior.
147
+ - **AR1 — Named composite.** The containing Dialog, combobox, populated listbox,
148
+ groups, options, busy status, empty state, and keyboard guidance preserve their
149
+ owned accessible names and roles. When no selectable results exist, the result
150
+ surface and combobox omit listbox, expanded, controls, and active-descendant
151
+ semantics so rich Empty content stays ordinary non-nested content.
152
+ - **AR2 — Topmost Escape.** A modal palette delegates unclaimed Escape presses to
153
+ Dialog's shared dismissal stack. A nested registered surface handles the first
154
+ press; the host palette remains open until it becomes topmost.
155
+ - **AR3 — Inline preview.** Documentation-only inline rendering remains outside
156
+ the shared layer stack and preserves its local Escape close behavior.
157
+ - **AR4 — Editable query keys.** Home and End preserve native caret movement;
158
+ PageUp and PageDown navigate to the first and last results; active IME
159
+ composition keys remain owned by the input method.
160
+ - **AR5 — Rendered empty message.** A search dead end announces the text rendered
161
+ by `emptySearchText`, including text produced by caller components and links.
162
+ - **AR6 — Query-field name.** The default query field name is a localized
163
+ "Search commands" label independent of its placeholder; caller `label` and
164
+ native `aria-label` overrides remain supported.
165
+ - **AR7 — Existing active option.** Result changes reset highlight, and
166
+ `aria-activedescendant` is present only when the indexed option exists in the
167
+ current selectable set.
123
168
 
124
169
  ## Design relationships
125
170
 
@@ -185,27 +230,36 @@ current audit gap and does not authorize a new target.
185
230
  mapping, delegation, and factual `none` dispositions.
186
231
  - `architecture:public-component-api` owns the stable props and composition
187
232
  surface; this documentation adds no API.
188
- - `architecture:interaction-modality` owns shared keyboard and pointer modality;
189
- this draft does not redefine focus ownership or input behavior.
190
- - `family:overlay-dismissal` identifies CommandPalette as a current member and
191
- records its component-local Escape handling as an adoption gap.
192
- - No layer-runtime record is linked because no current record with that scope is
193
- present in this checkout.
233
+ - `architecture:interaction-modality/INV4` owns supported-modality path
234
+ reachability; the exact query-key details follow objective editable-combobox
235
+ and IME composition standards.
236
+ - `spec:AST-056/FR3`, `AR1`, and `AR2` own rendered empty-search announcement
237
+ parity and an accessible query-field name independent of placeholder text.
238
+ - `family:overlay-dismissal` owns topmost Escape and close-request ordering;
239
+ CommandPalette adopts it through Dialog while preserving local inline-preview
240
+ closing because inline mode does not register as a layer.
241
+ - `architecture:layer-runtime` records that the modal path delegates Escape to
242
+ Dialog's shared dismissal registration while the unregistered inline preview
243
+ preserves local closing.
194
244
 
195
245
  ## Verification map
196
246
 
197
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
198
- | ------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ |
199
- | FR1 | CommandPalette root and subcomponent render suites plus browser evidence | Default, grouped, empty, pending, and replaced slots | Removing the asserted Dialog, Query field, List, Item, Group, Group heading, Empty, or Footer content fails role, content, slot, or exact-head browser assertions; Search glyph and Loading spinner presence remain source-inspected only. | `audit:CommandPalette/anatomy` |
200
- | FR2 | `themingTargets.test.ts`, focused subcomponent suites, and browser evidence | Seven local targets; Group, Group heading, and Footer placement | Removing a current target fails the global inventory; moving Group, Group heading, or Footer fails focused class assertions and the Footer browser receipt. | `audit:CommandPalette/theming` |
201
- | FR3 | Focused Footer suite and browser evidence plus delegated component target metadata | Default surface, Input visuals, and Footer shortcuts | Removing Footer's four Kbd shortcuts or either stable target fails focused unit and browser assertions; Icon and Spinner composition remain source-inspected. | `audit:CommandPalette/theming` |
202
- | FR4 | `CommandPaletteInput.test.tsx` and source inspection | Native query field with idle and pending Input | Removing the field fails combobox tests; adding a field target requires an explicit map update. | `audit:CommandPalette/anatomy` |
203
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and seven current local targets | Missing, extra, prefixed, stale, or alias-backed mappings fail repository validation. | `audit:CommandPalette/theming` |
204
-
205
- Focused target-placement assertions cover Group, Group heading, and Footer.
206
- Input, List, Item, and Empty placement rely on source inspection plus the global
207
- target inventory. Focused Footer tests pin all four default Kbd instances; Icon
208
- and Spinner composition remain source-inspected.
247
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
248
+ | ------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
249
+ | FR1, AR1 | CommandPalette root and subcomponent suites plus exact-head Chromium evidence | Default, grouped, empty, pending, rich Empty, and replaced slots | Exposing an empty listbox, leaving empty Input popup attributes, or nesting rich Empty controls under option semantics fails focused role and hosted axe assertions. | `audit:CommandPalette/anatomy` |
250
+ | FR2 | Focused subcomponent suites and `themingTargets.test.ts` | Seven local targets at their owned elements | Removing or moving a current target fails a focused placement assertion or the global inventory. | `audit:CommandPalette/theming` |
251
+ | FR3 | `CommandPaletteInput.test.tsx`, `CommandPaletteFooter.test.tsx`, and browser evidence | Default Search glyph, pending spinner, and Footer Kbd | Replacing or removing the delegated Icon, Spinner, or Kbd instance fails a composed-owner assertion and changes the exact-head evidence inventory. | `audit:CommandPalette/theming` |
252
+ | FR4 | `CommandPaletteInput.test.tsx` and source inspection | Native query field with idle and pending Input | Removing the field fails combobox tests; adding a field target requires an explicit map update. | `audit:CommandPalette/anatomy` |
253
+ | FR5, AR2, AR3 | `CommandPalette.test.tsx` | Modal host with nested layer; inline preview | Restoring local modal Escape handling closes the host instead of the nested surface; removing the inline branch loses preview closing. | `audit:CommandPalette/interaction` |
254
+ | FR6, AR4 | `CommandPalette.test.tsx` and `CommandPaletteInput.test.tsx` | Home, End, PageDown, Enter, and active IME composition | Routing Home/End to results or forwarding a composing command key fails user-event and native composition assertions. | `audit:CommandPalette/interaction` |
255
+ | FR7, AR7 | `CommandPalette.test.tsx` and `CommandPaletteInput.test.tsx` | Populated, empty, and repopulated result sets | Preserving an out-of-range highlight or emitting its nonexistent active descendant fails transition and direct bounds assertions. | `audit:CommandPalette/a11y` |
256
+ | FR8, AR5, AR6 | `CommandPalette.test.tsx`, `CommandPaletteInput.test.tsx`, and i18n checks | Rich caller Empty content; default and caller query names | Announcing catalog copy over rendered caller content or naming the query only through placeholder text fails live-region, name, and catalog assertions. | `audit:CommandPalette/a11y` |
257
+ | Theming anatomy map | `scripts/check-knowledge.mjs` and exact-head Chromium evidence | Canonical anatomy and seven current local targets | Missing, extra, prefixed, stale, alias-backed, or visually absent mappings fail repository validation or browser sensor counts. | `audit:CommandPalette/theming` |
258
+
259
+ Focused target-placement assertions cover all seven local targets. The composed
260
+ Input and Footer tests also pin the delegated Search Icon, pending Spinner, and
261
+ four default Kbd shortcuts; exact-head browser evidence verifies the same owned
262
+ anatomy across light, dark, LTR, RTL, and narrow rendering.
209
263
 
210
264
  ## Decision log
211
265
 
@@ -216,9 +270,6 @@ layer, API, or theming decision.
216
270
 
217
271
  - **OQ1 — Should Query field gain a stable public theming target?** (`human-api`)
218
272
  Its current lack of direct reachability is an audit gap, not settled intent.
219
- - **OQ2 — Should focused tests pin the default Search glyph and pending Loading
220
- spinner?** (`checkable`) Their presence is currently source-inspected rather
221
- than asserted.
222
273
 
223
274
  ## Content boundary
224
275
 
@@ -9,8 +9,11 @@
9
9
 
10
10
  import {describe, it, expect, vi, beforeEach} from 'vitest';
11
11
  import {render, screen, waitFor, fireEvent, act} from '@testing-library/react';
12
+ import userEvent from '@testing-library/user-event';
12
13
  import {useState} from 'react';
13
14
  import {CommandPalette} from './CommandPalette';
15
+ import {CommandPaletteInput} from './CommandPaletteInput';
16
+ import {useLayerDismissal} from '../Layer/useLayerDismissal';
14
17
  import {createStaticSource} from '@astryxdesign/core/Typeahead';
15
18
  import type {SearchSource, SearchableItem} from '@astryxdesign/core/Typeahead';
16
19
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
@@ -27,6 +30,11 @@ const groupedSource = createStaticSource([
27
30
 
28
31
  const emptySource = createStaticSource([]);
29
32
 
33
+ function NestedDismissible({onDismiss}: {onDismiss: () => void}) {
34
+ useLayerDismissal({isActive: true, onDismiss});
35
+ return null;
36
+ }
37
+
30
38
  // Mock showModal and close since jsdom doesn't implement them
31
39
  beforeEach(() => {
32
40
  HTMLDialogElement.prototype.showModal = vi.fn(function (
@@ -198,6 +206,31 @@ describe('CommandPalette', () => {
198
206
  });
199
207
  });
200
208
 
209
+ it('keeps actionable empty content outside listbox semantics', async () => {
210
+ const {container} = render(
211
+ <CommandPalette
212
+ isOpen={true}
213
+ onOpenChange={() => {}}
214
+ searchSource={emptySource}
215
+ emptyBootstrapText={<a href="/commands/new">Create a command</a>}
216
+ />,
217
+ );
218
+
219
+ expect(
220
+ await screen.findByRole('link', {name: 'Create a command'}),
221
+ ).toHaveAttribute('href', '/commands/new');
222
+ expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
223
+ expect(screen.queryByRole('option')).not.toBeInTheDocument();
224
+ expect(
225
+ container.querySelectorAll('.astryx-command-palette-empty'),
226
+ ).toHaveLength(1);
227
+ expect(screen.getByRole('combobox')).toHaveAttribute(
228
+ 'aria-expanded',
229
+ 'false',
230
+ );
231
+ expect(screen.getByRole('combobox')).not.toHaveAttribute('aria-controls');
232
+ });
233
+
201
234
  it('shows default emptyBootstrapText when not provided', async () => {
202
235
  render(
203
236
  <CommandPalette
@@ -211,6 +244,40 @@ describe('CommandPalette', () => {
211
244
  });
212
245
  });
213
246
 
247
+ it('clears a stale highlight across empty and repopulated results', async () => {
248
+ render(
249
+ <CommandPalette
250
+ isOpen={true}
251
+ onOpenChange={() => {}}
252
+ searchSource={simpleSource}
253
+ />,
254
+ );
255
+ await waitFor(() =>
256
+ expect(screen.getByText('Settings')).toBeInTheDocument(),
257
+ );
258
+
259
+ const input = screen.getByRole('combobox');
260
+ fireEvent.keyDown(input, {key: 'ArrowDown'});
261
+ fireEvent.keyDown(input, {key: 'ArrowDown'});
262
+ expect(input.getAttribute('aria-activedescendant')).toMatch(/-item-1$/);
263
+
264
+ fireEvent.change(input, {target: {value: 'zzz'}});
265
+ await waitFor(() => expect(screen.queryByRole('listbox')).toBeNull());
266
+ expect(input).not.toHaveAttribute('aria-activedescendant');
267
+
268
+ fireEvent.change(input, {target: {value: 'hom'}});
269
+ await waitFor(() =>
270
+ expect(screen.getByRole('option', {name: 'Home'})).toBeInTheDocument(),
271
+ );
272
+ expect(input).not.toHaveAttribute('aria-activedescendant');
273
+
274
+ fireEvent.keyDown(input, {key: 'ArrowDown'});
275
+ expect(input).toHaveAttribute(
276
+ 'aria-activedescendant',
277
+ screen.getByRole('option', {name: 'Home'}).id,
278
+ );
279
+ });
280
+
214
281
  it('calls onOpenChange(false) when Escape is pressed', () => {
215
282
  const handleOpenChange = vi.fn();
216
283
  render(
@@ -230,6 +297,46 @@ describe('CommandPalette', () => {
230
297
  expect(handleOpenChange).toHaveBeenCalledWith(false);
231
298
  });
232
299
 
300
+ it('lets the topmost nested layer handle Escape before the palette', () => {
301
+ const handleOpenChange = vi.fn();
302
+ const handleNestedDismiss = vi.fn();
303
+ render(
304
+ <CommandPalette
305
+ isOpen={true}
306
+ onOpenChange={handleOpenChange}
307
+ searchSource={simpleSource}
308
+ input={
309
+ <>
310
+ <CommandPaletteInput />
311
+ <NestedDismissible onDismiss={handleNestedDismiss} />
312
+ </>
313
+ }
314
+ />,
315
+ );
316
+
317
+ fireEvent.keyDown(screen.getByRole('combobox'), {key: 'Escape'});
318
+
319
+ expect(handleNestedDismiss).toHaveBeenCalledOnce();
320
+ expect(handleOpenChange).not.toHaveBeenCalled();
321
+ });
322
+
323
+ it('preserves local Escape closing for inline previews', () => {
324
+ const handleOpenChange = vi.fn();
325
+ render(
326
+ <CommandPalette
327
+ isOpen={true}
328
+ isInline
329
+ onOpenChange={handleOpenChange}
330
+ searchSource={simpleSource}
331
+ />,
332
+ );
333
+
334
+ fireEvent.keyDown(screen.getByRole('combobox'), {key: 'Escape'});
335
+
336
+ expect(handleOpenChange).toHaveBeenCalledOnce();
337
+ expect(handleOpenChange).toHaveBeenCalledWith(false);
338
+ });
339
+
233
340
  it('keeps the empty state mounted while a no-result search is pending', async () => {
234
341
  // A source whose searches resolve only when we release them, so we can
235
342
  // observe the render output while a search transition is in flight.
@@ -357,6 +464,52 @@ describe('CommandPalette', () => {
357
464
  expect(input).not.toHaveAttribute('aria-activedescendant');
358
465
  });
359
466
 
467
+ it('keeps Home and End on the query field and uses Page keys for results', async () => {
468
+ const user = userEvent.setup();
469
+ const handleOpenChange = vi.fn();
470
+ const handleValueChange = vi.fn();
471
+ render(
472
+ <CommandPalette
473
+ isOpen={true}
474
+ onOpenChange={handleOpenChange}
475
+ onValueChange={handleValueChange}
476
+ searchSource={simpleSource}
477
+ />,
478
+ );
479
+
480
+ const input = screen.getByRole<HTMLInputElement>('combobox');
481
+ await waitFor(() =>
482
+ expect(screen.getByText('Settings')).toBeInTheDocument(),
483
+ );
484
+ await user.click(input);
485
+ await user.type(input, 'e');
486
+ await waitFor(() =>
487
+ expect(screen.getByText('Settings')).toBeInTheDocument(),
488
+ );
489
+ const homeOption = screen.getByText('Home').closest('[role="option"]');
490
+ const settingsOption = screen
491
+ .getByText('Settings')
492
+ .closest('[role="option"]');
493
+
494
+ expect(input).toHaveValue('e');
495
+ expect(input.selectionStart).toBe(1);
496
+ await user.keyboard('{Home}');
497
+ expect(input.selectionStart).toBe(0);
498
+ await user.keyboard('{End}');
499
+ expect(input.selectionStart).toBe(1);
500
+ expect(input).not.toHaveAttribute('aria-activedescendant');
501
+
502
+ await user.keyboard('{PageDown}');
503
+ expect(input.getAttribute('aria-activedescendant')).toBe(
504
+ settingsOption?.id,
505
+ );
506
+ await user.keyboard('{PageUp}');
507
+ expect(input.getAttribute('aria-activedescendant')).toBe(homeOption?.id);
508
+ await user.keyboard('{Enter}');
509
+ expect(handleValueChange).toHaveBeenCalledWith('home');
510
+ expect(handleOpenChange).toHaveBeenCalledWith(false);
511
+ });
512
+
360
513
  it('highlights on hover without scrolling and scrolls once per key (#6077)', async () => {
361
514
  // Hover must be handled by the root's delegated list handler (routed to
362
515
  // useCombobox's hover-aware path), and the shared useHighlightedOptionScroll effect
@@ -453,19 +606,105 @@ describe('CommandPalette', () => {
453
606
  });
454
607
  });
455
608
 
456
- it('announces the empty state with the query when nothing matches', async () => {
609
+ it('announces the rendered default empty state when nothing matches', async () => {
610
+ render(
611
+ <CommandPalette
612
+ isOpen={true}
613
+ onOpenChange={() => {}}
614
+ searchSource={simpleSource}
615
+ />,
616
+ );
617
+ await waitFor(() => expect(screen.getByText('Home')).toBeInTheDocument());
618
+ fireEvent.change(screen.getByRole('combobox'), {target: {value: 'zzz'}});
619
+ await waitFor(() => {
620
+ expect(politeRegion()).toHaveTextContent(/^No results$/);
621
+ });
622
+ });
623
+
624
+ it('announces rich emptySearchText exactly as rendered', async () => {
457
625
  render(
458
626
  <CommandPalette
459
627
  isOpen={true}
460
628
  onOpenChange={() => {}}
461
629
  searchSource={simpleSource}
630
+ emptySearchText={
631
+ <span>
632
+ Nothing like that here. <a href="/commands/new">Add a command</a>
633
+ </span>
634
+ }
462
635
  />,
463
636
  );
464
637
  await waitFor(() => expect(screen.getByText('Home')).toBeInTheDocument());
465
638
  fireEvent.change(screen.getByRole('combobox'), {target: {value: 'zzz'}});
639
+
640
+ expect(
641
+ await screen.findByRole('link', {name: 'Add a command'}),
642
+ ).toHaveAttribute('href', '/commands/new');
466
643
  await waitFor(() => {
467
- expect(politeRegion()).toHaveTextContent('No results for zzz');
644
+ expect(politeRegion()).toHaveTextContent(
645
+ /^Nothing like that here\. Add a command$/,
646
+ );
647
+ });
648
+ });
649
+
650
+ it('waits for a narrowed search to settle before announcing no results', async () => {
651
+ const resolvers: ((items: SearchableItem[]) => void)[] = [];
652
+ const source: SearchSource = {
653
+ bootstrap: () => [
654
+ {id: 'home', label: 'Home'},
655
+ {id: 'settings', label: 'Settings'},
656
+ ],
657
+ async search(): Promise<SearchableItem[]> {
658
+ return new Promise<SearchableItem[]>(resolve => {
659
+ resolvers.push(resolve);
660
+ });
661
+ },
662
+ };
663
+ const spoken: string[] = [];
664
+ const observer = new MutationObserver(() => {
665
+ const text = politeRegion()?.textContent ?? '';
666
+ if (text !== '' && spoken.at(-1) !== text) {
667
+ spoken.push(text);
668
+ }
669
+ });
670
+ observer.observe(document.body, {
671
+ childList: true,
672
+ subtree: true,
673
+ characterData: true,
468
674
  });
675
+ try {
676
+ render(
677
+ <CommandPalette
678
+ isOpen={true}
679
+ onOpenChange={() => {}}
680
+ searchSource={source}
681
+ />,
682
+ );
683
+ await waitFor(() =>
684
+ expect(screen.getByText('Home')).toBeInTheDocument(),
685
+ );
686
+ const input = screen.getByRole('combobox');
687
+
688
+ fireEvent.change(input, {target: {value: 'e'}});
689
+ await act(async () =>
690
+ resolvers.shift()?.(source.bootstrap() as SearchableItem[]),
691
+ );
692
+ await waitFor(() => expect(spoken.at(-1)).toBe('2 results'));
693
+
694
+ // Optimistic narrowing empties the list while the source is still
695
+ // answering; the source then finds one match.
696
+ fireEvent.change(input, {target: {value: 'ez'}});
697
+ await act(async () => {
698
+ await new Promise(resolve => setTimeout(resolve, 50));
699
+ });
700
+ await act(async () =>
701
+ resolvers.shift()?.([{id: 'settings', label: 'Settings'}]),
702
+ );
703
+ await waitFor(() => expect(spoken.at(-1)).toBe('1 result'));
704
+ expect(spoken).not.toContain('No results');
705
+ } finally {
706
+ observer.disconnect();
707
+ }
469
708
  });
470
709
 
471
710
  it('announces loading politely while a search is in flight', async () => {