@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
@@ -18,6 +18,7 @@ import {
18
18
  getAllInjectedCss,
19
19
  } from '../__tests__/forcedColors';
20
20
  import * as stylex from '@stylexjs/stylex';
21
+ import {useState} from 'react';
21
22
  import {Calendar} from './Calendar';
22
23
  import type {CalendarHandle, ISODateString, DateRange} from './Calendar';
23
24
  import {calendarStyles} from './styles';
@@ -39,6 +40,24 @@ function politeRegion(): HTMLElement | null {
39
40
  return document.querySelector('[data-astryx-live-region="polite"]');
40
41
  }
41
42
 
43
+ /** A controlled Calendar whose owner follows the focus-date changes. */
44
+ function ControlledFocusCalendar({
45
+ onFocusDateChange,
46
+ }: {
47
+ onFocusDateChange: (iso: string) => void;
48
+ }) {
49
+ const [focusDate, setFocusDate] = useState<ISODateString>('2026-01-01');
50
+ return (
51
+ <Calendar
52
+ focusDate={focusDate}
53
+ onFocusDateChange={iso => {
54
+ onFocusDateChange(iso);
55
+ setFocusDate(iso);
56
+ }}
57
+ />
58
+ );
59
+ }
60
+
42
61
  /**
43
62
  * Helper to find a day button by its day number.
44
63
  * Day buttons are native <button> elements with aria-labels like
@@ -52,6 +71,20 @@ function getDayButton(day: number, month = 'January', year = 2026) {
52
71
  }
53
72
 
54
73
  describe('Calendar', () => {
74
+ it("with week numbers, the header row's corner cell is a named columnheader, so every row child is a grid cell", () => {
75
+ render(<Calendar focusDate="2026-01-01" hasWeekNumbers />);
76
+ const grid = screen.getByRole('grid');
77
+ const stray = grid.querySelectorAll(
78
+ '[role="row"] > :not([role="gridcell"]):not([role="columnheader"]):not([role="rowheader"])',
79
+ );
80
+ expect(stray).toHaveLength(0);
81
+ // Named from CLDR ("week" in en-US), as visually hidden text.
82
+ expect(
83
+ within(grid).getByRole('columnheader', {name: 'week'}),
84
+ ).toBeInTheDocument();
85
+ expect(within(grid).getAllByRole('rowheader').length).toBeGreaterThan(0);
86
+ });
87
+
55
88
  // ─── Basic Rendering ─────────────────────────────────────────
56
89
  it('forwards ref to the calendar root element', () => {
57
90
  let root: HTMLDivElement | null = null;
@@ -219,6 +252,31 @@ describe('Calendar', () => {
219
252
  expect(handleChange).toHaveBeenCalledWith('2026-01-15', expect.any(Date));
220
253
  });
221
254
 
255
+ it('shows years 1-99 as themselves, not as 1900-1999', async () => {
256
+ const user = userEvent.setup();
257
+ const handleChange = vi.fn();
258
+
259
+ render(
260
+ <Calendar
261
+ value="0001-01-15"
262
+ onChange={handleChange}
263
+ focusDate="0001-01-01"
264
+ />,
265
+ );
266
+
267
+ expect(screen.getByRole('grid', {name: 'January 1'})).toBeInTheDocument();
268
+ const day15 = getDayButton(15, 'January', 1);
269
+ expect(day15).toHaveAccessibleName(/^Monday, January 15, 1\b/);
270
+ expect(day15.closest('[role="gridcell"]')).toHaveAttribute(
271
+ 'aria-selected',
272
+ 'true',
273
+ );
274
+
275
+ await user.click(getDayButton(20, 'January', 1));
276
+ expect(handleChange).toHaveBeenCalledWith('0001-01-20', expect.any(Date));
277
+ expect(handleChange.mock.calls[0][1].getFullYear()).toBe(1);
278
+ });
279
+
222
280
  it('keeps null single selection empty until the parent accepts a date', async () => {
223
281
  const user = userEvent.setup();
224
282
  const onChange = vi.fn();
@@ -932,6 +990,493 @@ describe('Calendar', () => {
932
990
  });
933
991
  });
934
992
 
993
+ it('moves the focused day by a month with PageDown and PageUp, into the month now shown', async () => {
994
+ const user = userEvent.setup();
995
+ render(<Calendar focusDate="2026-01-31" />);
996
+
997
+ getDayButton(31).focus();
998
+ await user.keyboard('{PageDown}');
999
+ await waitFor(() => {
1000
+ expect(politeRegion()).toHaveTextContent('February 2026');
1001
+ });
1002
+ // A month, not a week: Jan 31 lands on the last day of February.
1003
+ await waitFor(() => {
1004
+ expect(document.activeElement).toHaveAttribute(
1005
+ 'aria-label',
1006
+ expect.stringMatching(/February 28, 2026/),
1007
+ );
1008
+ });
1009
+
1010
+ await user.keyboard('{PageUp}');
1011
+ await waitFor(() => {
1012
+ expect(politeRegion()).toHaveTextContent('January 2026');
1013
+ });
1014
+ await waitFor(() => {
1015
+ expect(document.activeElement).toHaveAttribute(
1016
+ 'aria-label',
1017
+ expect.stringMatching(/January 28, 2026/),
1018
+ );
1019
+ });
1020
+ });
1021
+
1022
+ it("pages from the first of two months onto the right day, not the second grid's fallback", async () => {
1023
+ const user = userEvent.setup();
1024
+ render(<Calendar focusDate="2026-01-01" numberOfMonths={2} />);
1025
+
1026
+ getDayButton(15).focus();
1027
+ await user.keyboard('{PageDown}');
1028
+ await waitFor(() => {
1029
+ expect(document.activeElement).toHaveAttribute(
1030
+ 'aria-label',
1031
+ expect.stringMatching(/February 15, 2026/),
1032
+ );
1033
+ });
1034
+ expect(document.activeElement).not.toHaveAttribute('aria-disabled');
1035
+
1036
+ await user.keyboard('{PageUp}');
1037
+ await waitFor(() => {
1038
+ expect(document.activeElement).toHaveAttribute(
1039
+ 'aria-label',
1040
+ expect.stringMatching(/January 15, 2026/),
1041
+ );
1042
+ });
1043
+ expect(document.activeElement).not.toHaveAttribute('aria-disabled');
1044
+ });
1045
+
1046
+ it('skips borrowed adjacent-month days and crosses into their month instead', async () => {
1047
+ const user = userEvent.setup();
1048
+ render(<Calendar focusDate="2026-01-01" />);
1049
+
1050
+ // January 1, 2026 is a Thursday: the first row borrows December 28–31.
1051
+ getDayButton(1).focus();
1052
+ await user.keyboard('{ArrowLeft}');
1053
+ await waitFor(() => {
1054
+ expect(politeRegion()).toHaveTextContent('December 2025');
1055
+ });
1056
+ await waitFor(() => {
1057
+ expect(document.activeElement).toHaveAttribute(
1058
+ 'data-date',
1059
+ '2025-12-31',
1060
+ );
1061
+ });
1062
+ expect(document.activeElement).not.toHaveAttribute('aria-disabled');
1063
+ });
1064
+
1065
+ it('in a two-month view an arrow from the first month lands on the real day, never a borrowed one', async () => {
1066
+ const user = userEvent.setup();
1067
+ render(<Calendar focusDate="2026-01-01" numberOfMonths={2} />);
1068
+
1069
+ // January 28 (Wednesday) → ArrowDown → February 4, shown in the second
1070
+ // grid: no page, and never the first day of a new month.
1071
+ getDayButton(28).focus();
1072
+ await user.keyboard('{ArrowDown}');
1073
+ await waitFor(() => {
1074
+ expect(document.activeElement).toHaveAttribute(
1075
+ 'data-date',
1076
+ '2026-02-04',
1077
+ );
1078
+ });
1079
+ expect(document.activeElement).not.toHaveAttribute('aria-disabled');
1080
+ // No page happened: January is still the first grid.
1081
+ expect(screen.getAllByRole('grid')).toHaveLength(2);
1082
+ expect(getDayButton(28)).toBeInTheDocument();
1083
+
1084
+ // February 24 (Tuesday) → ArrowDown → March 3: the view pages so March
1085
+ // is the second month, and focus lands on the real March 3.
1086
+ getDayButton(24, 'February').focus();
1087
+ await user.keyboard('{ArrowDown}');
1088
+ await waitFor(() => {
1089
+ expect(document.activeElement).toHaveAttribute(
1090
+ 'data-date',
1091
+ '2026-03-03',
1092
+ );
1093
+ });
1094
+ expect(document.activeElement).not.toHaveAttribute('aria-disabled');
1095
+ });
1096
+
1097
+ it('pages from a borrowed adjacent-month day onto the real day in the month already shown', async () => {
1098
+ const user = userEvent.setup();
1099
+ render(<Calendar focusDate="2026-01-01" />);
1100
+
1101
+ // A pointer can still land on the borrowed December 28 in the first
1102
+ // row (the keyboard skips it).
1103
+ const borrowed = document.querySelector<HTMLElement>(
1104
+ 'button[data-date="2025-12-28"]',
1105
+ )!;
1106
+ borrowed.focus();
1107
+ expect(document.activeElement).toBe(borrowed);
1108
+
1109
+ // PageDown targets January 28, which January's grid already shows: no
1110
+ // month change, focus moves there and never to a borrowed copy.
1111
+ await user.keyboard('{PageDown}');
1112
+ await waitFor(() => {
1113
+ expect(document.activeElement).toHaveAttribute(
1114
+ 'data-date',
1115
+ '2026-01-28',
1116
+ );
1117
+ });
1118
+ expect(screen.getByRole('grid')).toHaveAttribute(
1119
+ 'aria-label',
1120
+ 'January 2026',
1121
+ );
1122
+ expect(document.activeElement).not.toHaveAttribute('aria-disabled');
1123
+ });
1124
+
1125
+ it('an arrow past max changes nothing and leaves no pending target behind', async () => {
1126
+ const user = userEvent.setup();
1127
+ const {rerender} = render(
1128
+ <Calendar focusDate="2026-01-01" max="2026-01-31" />,
1129
+ );
1130
+ const last = getDayButton(31);
1131
+ last.focus();
1132
+ await user.keyboard('{ArrowRight}');
1133
+ // January stays, focus stays on the max day.
1134
+ expect(screen.getByRole('button', {name: 'Next month'})).toBeDisabled();
1135
+ expect(document.activeElement).toBe(getDayButton(31));
1136
+
1137
+ // Later the range widens and the user clicks Next: no stale target
1138
+ // pulls focus into the grid.
1139
+ rerender(<Calendar focusDate="2026-01-01" max="2026-02-28" />);
1140
+ const next = screen.getByRole('button', {name: 'Next month'});
1141
+ await user.click(next);
1142
+ await waitFor(() => {
1143
+ expect(politeRegion()).toHaveTextContent('February 2026');
1144
+ });
1145
+ expect(document.activeElement).toBe(next);
1146
+ });
1147
+
1148
+ it('a page into a month with no enabled day changes nothing and keeps focus', async () => {
1149
+ const user = userEvent.setup();
1150
+ // Only January 1–15 is enabled: February has no enabled day at all.
1151
+ const earlyJanuaryOnly = (date: Date) =>
1152
+ date.getFullYear() === 2026 &&
1153
+ date.getMonth() === 0 &&
1154
+ date.getDate() <= 15;
1155
+ render(
1156
+ <Calendar
1157
+ focusDate="2026-01-01"
1158
+ dateConstraints={[earlyJanuaryOnly]}
1159
+ />,
1160
+ );
1161
+ getDayButton(10).focus();
1162
+ await user.keyboard('{PageDown}');
1163
+ expect(screen.getByRole('grid')).toHaveAttribute(
1164
+ 'aria-label',
1165
+ 'January 2026',
1166
+ );
1167
+ expect(document.activeElement).toBe(getDayButton(10));
1168
+ });
1169
+
1170
+ it('a page onto a day disabled by a constraint moves the month and focuses its first enabled day', async () => {
1171
+ const user = userEvent.setup();
1172
+ // February 1–14 disabled; February 15 onwards enabled.
1173
+ const noEarlyFebruary = (date: Date) =>
1174
+ !(date.getMonth() === 1 && date.getDate() < 15);
1175
+ render(
1176
+ <Calendar focusDate="2026-01-01" dateConstraints={[noEarlyFebruary]} />,
1177
+ );
1178
+ getDayButton(10).focus();
1179
+ await user.keyboard('{PageDown}');
1180
+ await waitFor(() => {
1181
+ expect(document.activeElement).toHaveAttribute(
1182
+ 'data-date',
1183
+ '2026-02-15',
1184
+ );
1185
+ });
1186
+ expect(screen.getByRole('grid')).toHaveAttribute(
1187
+ 'aria-label',
1188
+ 'February 2026',
1189
+ );
1190
+ });
1191
+
1192
+ it('in a two-month view PageDown from the second month moves both months and a later Next click selects no stale day', async () => {
1193
+ const user = userEvent.setup();
1194
+ const onChange = vi.fn();
1195
+ render(
1196
+ <Calendar
1197
+ focusDate="2026-01-01"
1198
+ numberOfMonths={2}
1199
+ onChange={onChange}
1200
+ />,
1201
+ );
1202
+ getDayButton(10, 'February').focus();
1203
+ await user.keyboard('{PageDown}');
1204
+ // March 10 is in no shown grid, so the months move (Feb + Mar) and
1205
+ // focus lands on March 10.
1206
+ await waitFor(() => {
1207
+ expect(document.activeElement).toHaveAttribute(
1208
+ 'data-date',
1209
+ '2026-03-10',
1210
+ );
1211
+ });
1212
+ expect(
1213
+ screen.getAllByRole('grid').map(g => g.getAttribute('aria-label')),
1214
+ ).toEqual(['February 2026', 'March 2026']);
1215
+ // A later Next click pages again without selecting anything.
1216
+ await user.click(getButton('Next month'));
1217
+ await waitFor(() => {
1218
+ expect(
1219
+ screen.getAllByRole('grid').map(g => g.getAttribute('aria-label')),
1220
+ ).toEqual(['March 2026', 'April 2026']);
1221
+ });
1222
+ expect(onChange).not.toHaveBeenCalled();
1223
+ });
1224
+
1225
+ it('an arrow toward a direction where every remaining day is disabled changes nothing', async () => {
1226
+ const user = userEvent.setup();
1227
+ // Only January 2026 is enabled: nothing after January 31 can take focus
1228
+ // (the walk looks a year ahead, so the constraint pins the year too).
1229
+ const januaryOnly = (date: Date) =>
1230
+ date.getFullYear() === 2026 && date.getMonth() === 0;
1231
+ render(
1232
+ <Calendar focusDate="2026-01-01" dateConstraints={[januaryOnly]} />,
1233
+ );
1234
+ getDayButton(28).focus();
1235
+ await user.keyboard('{ArrowDown}');
1236
+ // No month change, no focus drop: still January 28.
1237
+ expect(screen.getByRole('grid')).toHaveAttribute(
1238
+ 'aria-label',
1239
+ 'January 2026',
1240
+ );
1241
+ expect(document.activeElement).toBe(getDayButton(28));
1242
+ });
1243
+
1244
+ it('an arrow resolves the next enabled day in its direction first: weekdays-only ArrowRight from Friday January 30 reaches Monday February 2', async () => {
1245
+ const user = userEvent.setup();
1246
+ const weekdaysOnly = (date: Date) =>
1247
+ date.getDay() !== 0 && date.getDay() !== 6;
1248
+ render(
1249
+ <Calendar focusDate="2026-01-01" dateConstraints={[weekdaysOnly]} />,
1250
+ );
1251
+ getDayButton(30).focus();
1252
+ await user.keyboard('{ArrowRight}');
1253
+ await waitFor(() => {
1254
+ expect(document.activeElement).toHaveAttribute(
1255
+ 'data-date',
1256
+ '2026-02-02',
1257
+ );
1258
+ });
1259
+ // The month paged with it: February's grid is the one shown.
1260
+ expect(screen.getByRole('grid')).toHaveAttribute(
1261
+ 'aria-label',
1262
+ 'February 2026',
1263
+ );
1264
+ });
1265
+
1266
+ it('an arrow whose next day in range does not exist changes nothing: max January 25, ArrowDown from January 21', async () => {
1267
+ const user = userEvent.setup();
1268
+ render(<Calendar focusDate="2026-01-01" max="2026-01-25" />);
1269
+ getDayButton(21).focus();
1270
+ await user.keyboard('{ArrowDown}');
1271
+ expect(politeRegion()?.textContent ?? '').not.toContain('February 2026');
1272
+ expect(document.activeElement).toBe(getDayButton(21));
1273
+ });
1274
+
1275
+ it('a controlled owner that refuses the month change leaves no stale target to steal focus or Enter later', async () => {
1276
+ const user = userEvent.setup();
1277
+ const onChange = vi.fn();
1278
+ function RefusingOwner() {
1279
+ const [focusDate, setFocusDate] = useState<ISODateString>('2026-01-01');
1280
+ const [allow, setAllow] = useState(false);
1281
+ return (
1282
+ <>
1283
+ <button type="button" onClick={() => setAllow(true)}>
1284
+ allow
1285
+ </button>
1286
+ <Calendar
1287
+ focusDate={focusDate}
1288
+ onFocusDateChange={next => {
1289
+ if (allow) {
1290
+ setFocusDate(next);
1291
+ }
1292
+ }}
1293
+ onChange={onChange}
1294
+ />
1295
+ </>
1296
+ );
1297
+ }
1298
+ render(<RefusingOwner />);
1299
+ getDayButton(15).focus();
1300
+ // PageDown asks for February; the owner refuses, so January stays and
1301
+ // focus does not move.
1302
+ await user.keyboard('{PageDown}');
1303
+ expect(screen.getByRole('grid')).toHaveAttribute(
1304
+ 'aria-label',
1305
+ 'January 2026',
1306
+ );
1307
+ expect(document.activeElement).toBe(getDayButton(15));
1308
+ // Later the owner lets February show (through the Next button): the
1309
+ // old February 15 target must not grab focus, and Enter on whatever is
1310
+ // focused must not pick it.
1311
+ await user.click(screen.getByRole('button', {name: 'allow'}));
1312
+ await user.click(getButton('Next month'));
1313
+ await waitFor(() => {
1314
+ expect(screen.getByRole('grid')).toHaveAttribute(
1315
+ 'aria-label',
1316
+ 'February 2026',
1317
+ );
1318
+ });
1319
+ expect(document.activeElement).not.toBe(getDayButton(15, 'February'));
1320
+ expect(onChange).not.toHaveBeenCalled();
1321
+ });
1322
+
1323
+ it('a refused target is dropped when focus leaves the day grid, so a later Next click plus Enter picks nothing', async () => {
1324
+ const user = userEvent.setup();
1325
+ const onChange = vi.fn();
1326
+ function Owner({allow}: {allow: boolean}) {
1327
+ const [focusDate, setFocusDate] = useState<ISODateString>('2026-01-01');
1328
+ return (
1329
+ <Calendar
1330
+ focusDate={focusDate}
1331
+ onFocusDateChange={next => {
1332
+ if (allow) {
1333
+ setFocusDate(next);
1334
+ }
1335
+ }}
1336
+ onChange={onChange}
1337
+ />
1338
+ );
1339
+ }
1340
+ const {rerender} = render(<Owner allow={false} />);
1341
+ getDayButton(15).focus();
1342
+ // Refused: January stays, focus stays on January 15.
1343
+ await user.keyboard('{PageDown}');
1344
+ expect(document.activeElement).toBe(getDayButton(15));
1345
+ // Focus moves off the day grid onto the Next button; the owner then
1346
+ // allows month changes without any focus change of its own.
1347
+ act(() => getButton('Next month').focus());
1348
+ rerender(<Owner allow />);
1349
+ await user.click(getButton('Next month'));
1350
+ await waitFor(() => {
1351
+ expect(screen.getByRole('grid')).toHaveAttribute(
1352
+ 'aria-label',
1353
+ 'February 2026',
1354
+ );
1355
+ });
1356
+ // The stale February 15 target must not have taken focus; Enter on
1357
+ // whatever is focused picks nothing.
1358
+ expect(document.activeElement).not.toBe(getDayButton(15, 'February'));
1359
+ await user.keyboard('{Enter}');
1360
+ expect(onChange).not.toHaveBeenCalled();
1361
+ });
1362
+
1363
+ it('a refused page target is dropped by an arrow to another day, so showing the months later does not select it', async () => {
1364
+ const user = userEvent.setup();
1365
+ const onChange = vi.fn();
1366
+ function Owner({focusDate}: {focusDate: ISODateString}) {
1367
+ // A controlled owner that ignores every requested change: the shown
1368
+ // months move only when the test re-renders it with a new prop, with
1369
+ // no focus change of its own.
1370
+ return (
1371
+ <Calendar
1372
+ focusDate={focusDate}
1373
+ numberOfMonths={2}
1374
+ onFocusDateChange={() => {}}
1375
+ onChange={onChange}
1376
+ />
1377
+ );
1378
+ }
1379
+ const {rerender} = render(<Owner focusDate="2026-01-01" />);
1380
+ getDayButton(10, 'February').focus();
1381
+ // Declined: Jan–Feb stay; focus stays on February 10.
1382
+ await user.keyboard('{PageDown}');
1383
+ expect(document.activeElement).toBe(getDayButton(10, 'February'));
1384
+ // An arrow to February 11 is a focus move to a day other than the
1385
+ // target, so the stale March 10 target is dropped.
1386
+ await user.keyboard('{ArrowRight}');
1387
+ expect(document.activeElement).toBe(getDayButton(11, 'February'));
1388
+ // The owner now shows February–March without touching focus.
1389
+ rerender(<Owner focusDate="2026-02-01" />);
1390
+ await waitFor(() => {
1391
+ expect(
1392
+ screen.getAllByRole('grid').map(g => g.getAttribute('aria-label')),
1393
+ ).toEqual(['February 2026', 'March 2026']);
1394
+ });
1395
+ expect(document.activeElement).toBe(getDayButton(11, 'February'));
1396
+ await user.keyboard('{Enter}');
1397
+ expect(onChange).not.toHaveBeenCalledWith('2026-03-10');
1398
+ });
1399
+
1400
+ it("the pending target becomes the grid's Tab stop: after a page into the second grid and after a page that moves the months, only it has tabindex 0", async () => {
1401
+ const user = userEvent.setup();
1402
+ render(<Calendar focusDate="2026-01-01" numberOfMonths={2} />);
1403
+ // Per grid: the day buttons with tabindex 0 (each grid keeps at most one;
1404
+ // a borrowed copy in the other grid may share the date).
1405
+ const stops = (gridName: string) =>
1406
+ Array.from(
1407
+ screen
1408
+ .getByRole('grid', {name: gridName})
1409
+ .querySelectorAll<HTMLElement>('button[data-date]'),
1410
+ ).filter(button => button.tabIndex === 0);
1411
+ // PageDown from January 15 lands on February 15 in the second grid —
1412
+ // not February's default stop (its first day), so the stop must move.
1413
+ getDayButton(15).focus();
1414
+ await user.keyboard('{PageDown}');
1415
+ await waitFor(() => {
1416
+ expect(document.activeElement).toHaveAttribute(
1417
+ 'data-date',
1418
+ '2026-02-15',
1419
+ );
1420
+ });
1421
+ expect(
1422
+ stops('February 2026').map(b => b.getAttribute('data-date')),
1423
+ ).toEqual(['2026-02-15']);
1424
+ expect(stops('January 2026')).toHaveLength(1);
1425
+ // PageDown again moves both months; March 15 is March's one stop.
1426
+ await user.keyboard('{PageDown}');
1427
+ await waitFor(() => {
1428
+ expect(document.activeElement).toHaveAttribute(
1429
+ 'data-date',
1430
+ '2026-03-15',
1431
+ );
1432
+ });
1433
+ expect(stops('March 2026').map(b => b.getAttribute('data-date'))).toEqual(
1434
+ ['2026-03-15'],
1435
+ );
1436
+ expect(stops('February 2026')).toHaveLength(1);
1437
+ });
1438
+
1439
+ it('a page whose target lies past max changes nothing and keeps focus', async () => {
1440
+ const user = userEvent.setup();
1441
+ render(<Calendar focusDate="2026-01-01" max="2026-02-10" />);
1442
+ getDayButton(15).focus();
1443
+ // February 15 is past max: no page, no focus drop.
1444
+ await user.keyboard('{PageDown}');
1445
+ // No announcement was made (the live region never mounted) and January
1446
+ // is still the month shown.
1447
+ expect(politeRegion()?.textContent ?? '').not.toContain('February 2026');
1448
+ expect(getDayButton(15)).toHaveAttribute('data-date', '2026-01-15');
1449
+ expect(document.activeElement).toBe(getDayButton(15));
1450
+ });
1451
+
1452
+ it('in a two-month view a boundary blocks only out-of-range targets, not month movement', async () => {
1453
+ const user = userEvent.setup();
1454
+ render(
1455
+ <Calendar focusDate="2026-01-01" min="2026-01-01" numberOfMonths={2} />,
1456
+ );
1457
+ // ArrowUp from February 3 reaches January 27 — in range, in the first
1458
+ // grid — even though Previous is disabled at the January minimum.
1459
+ getDayButton(3, 'February').focus();
1460
+ await user.keyboard('{ArrowUp}');
1461
+ await waitFor(() => {
1462
+ expect(document.activeElement).toHaveAttribute(
1463
+ 'data-date',
1464
+ '2026-01-27',
1465
+ );
1466
+ });
1467
+ // PageDown from January 15 reaches February 15, already visible: no
1468
+ // month change, focus moves to it.
1469
+ getDayButton(15).focus();
1470
+ await user.keyboard('{PageDown}');
1471
+ await waitFor(() => {
1472
+ expect(document.activeElement).toHaveAttribute(
1473
+ 'data-date',
1474
+ '2026-02-15',
1475
+ );
1476
+ });
1477
+ expect(getDayButton(15)).toBeInTheDocument();
1478
+ });
1479
+
935
1480
  it('announces the next month when paging the grid with PageDown', async () => {
936
1481
  const user = userEvent.setup();
937
1482
  render(<Calendar focusDate="2026-01-01" />);
@@ -1106,15 +1651,6 @@ describe('Calendar', () => {
1106
1651
  });
1107
1652
 
1108
1653
  describe('aria-multiselectable', () => {
1109
- it('sets aria-multiselectable="true" on the grid in range mode', () => {
1110
- render(<Calendar mode="range" focusDate="2026-01-01" />);
1111
-
1112
- expect(screen.getByRole('grid')).toHaveAttribute(
1113
- 'aria-multiselectable',
1114
- 'true',
1115
- );
1116
- });
1117
-
1118
1654
  it('does not set aria-multiselectable in single mode', () => {
1119
1655
  render(<Calendar focusDate="2026-01-01" />);
1120
1656
 
@@ -1149,21 +1685,23 @@ describe('Calendar', () => {
1149
1685
  const user = userEvent.setup();
1150
1686
  const handleFocusChange = vi.fn();
1151
1687
 
1152
- render(
1153
- <Calendar focusDate="2026-01-01" onFocusDateChange={handleFocusChange} />,
1154
- );
1688
+ render(<ControlledFocusCalendar onFocusDateChange={handleFocusChange} />);
1155
1689
 
1156
1690
  // Focus Jan 28
1157
1691
  const day28 = getDayButton(28);
1158
1692
  await user.click(day28);
1159
1693
  day28.focus();
1160
1694
 
1161
- // Press ArrowDown — should move to Feb 4 (+7 days), not Feb 28
1695
+ // Press ArrowDown — should move to Feb 4 (+7 days), not Feb 28. The
1696
+ // borrowed February row in January's grid is skipped; the grid navigates
1697
+ // to February and focuses the real day there.
1162
1698
  await user.keyboard('{ArrowDown}');
1163
1699
 
1164
- // After navigation, Feb 4 should be focused
1165
- const focusedElement = document.activeElement;
1166
- expect(focusedElement).toHaveAttribute('data-date', '2026-02-04');
1700
+ expect(handleFocusChange).toHaveBeenCalledWith('2026-02-01');
1701
+ await waitFor(() => {
1702
+ expect(document.activeElement).toHaveAttribute('data-date', '2026-02-04');
1703
+ });
1704
+ expect(document.activeElement).not.toHaveAttribute('aria-disabled');
1167
1705
  });
1168
1706
 
1169
1707
  it('ArrowDown lands on the same weekday +7 days even when earlier days are disabled (complex-2)', async () => {
@@ -1222,9 +1760,7 @@ describe('Calendar', () => {
1222
1760
  const user = userEvent.setup();
1223
1761
  const handleFocusChange = vi.fn();
1224
1762
 
1225
- render(
1226
- <Calendar focusDate="2026-01-01" onFocusDateChange={handleFocusChange} />,
1227
- );
1763
+ render(<ControlledFocusCalendar onFocusDateChange={handleFocusChange} />);
1228
1764
 
1229
1765
  const day28 = getDayButton(28);
1230
1766
  await user.click(day28);
@@ -1235,7 +1771,10 @@ describe('Calendar', () => {
1235
1771
  await user.keyboard('{ArrowDown}');
1236
1772
 
1237
1773
  // Feb 4 (+7 days) — resolved via data-date despite the unparseable label.
1238
- expect(document.activeElement).toHaveAttribute('data-date', '2026-02-04');
1774
+ expect(handleFocusChange).toHaveBeenCalledWith('2026-02-01');
1775
+ await waitFor(() => {
1776
+ expect(document.activeElement).toHaveAttribute('data-date', '2026-02-04');
1777
+ });
1239
1778
  });
1240
1779
 
1241
1780
  it('prev button is disabled when focusDate month contains min', () => {