@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
@@ -0,0 +1,108 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** Chromium accessibility-tree and relationship proof for Selector's Combobox binding. */
4
+
5
+ import {expect, test, type Locator, type Page} from '@playwright/test';
6
+ import {
7
+ blockingResults,
8
+ checkAccessibilitySpec,
9
+ formatFailures,
10
+ unmatchedKnownFailures,
11
+ type BindingResult,
12
+ } from '@astryxdesign/a11y-spec';
13
+ import {createChromiumHarness} from '@astryxdesign/a11y-spec/chromium';
14
+ import {
15
+ DEFAULT_STORYBOOK_DIR,
16
+ serveStorybook,
17
+ type StaticServer,
18
+ } from '@astryxdesign/a11y-spec/storybook';
19
+ import {COMBOBOX_PATTERN} from '@astryxdesign/a11y-spec';
20
+ import {
21
+ SELECTOR_COMBOBOX_SCENARIOS,
22
+ type SelectorComboboxScenario,
23
+ } from './Combobox.a11y.states';
24
+ import {SELECTOR_COMBOBOX_KNOWN_FAILURES} from './Combobox.a11y.known-failures';
25
+
26
+ let storybook: StaticServer;
27
+ const observedResults: BindingResult[] = [];
28
+
29
+ test.beforeAll(async () => {
30
+ storybook = await serveStorybook(
31
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
32
+ );
33
+ });
34
+
35
+ test.afterAll(async () => {
36
+ await storybook?.close();
37
+ expect(
38
+ unmatchedKnownFailures(SELECTOR_COMBOBOX_KNOWN_FAILURES, observedResults),
39
+ ).toEqual([]);
40
+ });
41
+
42
+ async function awaitScenario(
43
+ page: Page,
44
+ scenario: SelectorComboboxScenario,
45
+ ): Promise<void> {
46
+ if (!scenario.open) {
47
+ return;
48
+ }
49
+ await expect(page.getByRole('listbox')).toBeVisible();
50
+ if (scenario.highlightFirst) {
51
+ await expect(
52
+ page.getByRole('combobox', {name: scenario.facts.name}),
53
+ ).toHaveAttribute('aria-activedescendant', /.+/);
54
+ }
55
+ }
56
+
57
+ for (const scenario of SELECTOR_COMBOBOX_SCENARIOS) {
58
+ test(`Selector Combobox contract — ${scenario.id}`, async ({page}) => {
59
+ await page.goto(
60
+ `${storybook.origin}/iframe.html?id=${scenario.storyId}&viewMode=story`,
61
+ {waitUntil: 'load'},
62
+ );
63
+ await awaitScenario(page, scenario);
64
+
65
+ // A modal sheet makes the trigger inert, so find it among hidden nodes too.
66
+ const subject = page.getByRole('combobox', {
67
+ name: scenario.facts.name,
68
+ includeHidden: true,
69
+ });
70
+ if (
71
+ scenario.binding === 'Selector.trigger' &&
72
+ scenario.facts.popupVisible
73
+ ) {
74
+ // AR1: the trigger names the popup kind its presentation renders.
75
+ await expect(subject).toHaveAttribute(
76
+ 'aria-haspopup',
77
+ scenario.facts.popupRole,
78
+ );
79
+ }
80
+ const related: Record<string, Locator> = {};
81
+ if (scenario.facts.popupVisible) {
82
+ related.popup =
83
+ scenario.facts.popupRole === 'dialog'
84
+ ? page.getByRole('dialog', {name: 'Fruit'})
85
+ : page.getByRole('listbox');
86
+ }
87
+ if (scenario.facts.activeDescendant) {
88
+ related['active-descendant'] = page.getByRole('option', {name: 'Apple'});
89
+ }
90
+ const cdp = await page.context().newCDPSession(page);
91
+ const run = await checkAccessibilitySpec({
92
+ spec: COMBOBOX_PATTERN,
93
+ binding: scenario.binding,
94
+ state: scenario.id,
95
+ facts: scenario.facts,
96
+ knownFailures: SELECTOR_COMBOBOX_KNOWN_FAILURES,
97
+ mount: async () => createChromiumHarness({page, cdp, subject, related}),
98
+ });
99
+ observedResults.push(run);
100
+
101
+ expect(formatFailures(blockingResults([run]))).toBe('');
102
+ expect(
103
+ run.results
104
+ .filter(result => result.status === 'unrun')
105
+ .map(result => `${result.expectation}: ${result.detail ?? ''}`),
106
+ ).toEqual([]);
107
+ });
108
+ }
@@ -0,0 +1,126 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** Selector's first binding to the reusable Combobox semantic contract. */
4
+
5
+ import {cleanup, render, screen, waitFor, within} from '@testing-library/react';
6
+ import userEvent from '@testing-library/user-event';
7
+ import {describe, expect, it} from 'vitest';
8
+ import {
9
+ COMBOBOX_PATTERN,
10
+ expectAccessibilitySpec,
11
+ } from '@astryxdesign/a11y-spec';
12
+ import {Selector} from '../Selector';
13
+ import {installListboxDialogStubs} from './Listbox.a11y.dom';
14
+ import {
15
+ SELECTOR_COMBOBOX_EXCLUSIONS,
16
+ SELECTOR_COMBOBOX_SCENARIOS,
17
+ type SelectorComboboxScenario,
18
+ } from './Combobox.a11y.states';
19
+ import {SELECTOR_COMBOBOX_KNOWN_FAILURES} from './Combobox.a11y.known-failures';
20
+
21
+ installListboxDialogStubs();
22
+
23
+ const hidden = {hidden: true} as const;
24
+
25
+ function subjectFor(scenario: SelectorComboboxScenario): HTMLElement {
26
+ return screen.getByRole('combobox', {...hidden, name: scenario.facts.name});
27
+ }
28
+
29
+ describe('Selector Combobox semantic binding', () => {
30
+ it.each(SELECTOR_COMBOBOX_SCENARIOS)('$id', async scenario => {
31
+ await expectAccessibilitySpec({
32
+ spec: COMBOBOX_PATTERN,
33
+ binding: scenario.binding,
34
+ state: scenario.id,
35
+ facts: scenario.facts,
36
+ knownFailures: SELECTOR_COMBOBOX_KNOWN_FAILURES,
37
+ render: async () => {
38
+ render(
39
+ <Selector
40
+ label="Fruit"
41
+ options={[
42
+ {value: 'apple', label: 'Apple'},
43
+ {value: 'banana', label: 'Banana'},
44
+ ]}
45
+ onChange={() => {}}
46
+ {...scenario.props}
47
+ />,
48
+ );
49
+ if (!scenario.open) {
50
+ return;
51
+ }
52
+ const user = userEvent.setup();
53
+ await user.click(
54
+ scenario.props.hasSearch
55
+ ? screen.getByRole('button', {name: /Fruit/})
56
+ : screen.getByRole('combobox', {name: 'Fruit'}),
57
+ );
58
+ await screen.findByRole('listbox', hidden);
59
+ if (scenario.highlightFirst) {
60
+ const search = subjectFor(scenario);
61
+ await waitFor(() => expect(search).toHaveFocus());
62
+ await user.keyboard('{ArrowDown}');
63
+ }
64
+ },
65
+ subject: () => subjectFor(scenario),
66
+ related: () => {
67
+ const related: Record<string, Element> = {};
68
+ if (!scenario.facts.popupVisible) {
69
+ return related;
70
+ }
71
+ const listbox = screen.getByRole('listbox', hidden);
72
+ if (scenario.binding === 'Selector.trigger') {
73
+ // AR1: the trigger names the popup kind its presentation renders.
74
+ expect(subjectFor(scenario)).toHaveAttribute(
75
+ 'aria-haspopup',
76
+ scenario.facts.popupRole,
77
+ );
78
+ }
79
+ related.popup =
80
+ scenario.facts.popupRole === 'dialog'
81
+ ? screen.getByRole('dialog', {...hidden, name: 'Fruit'})
82
+ : listbox;
83
+ if (scenario.facts.activeDescendant) {
84
+ expect(
85
+ subjectFor(scenario).getAttribute('aria-activedescendant'),
86
+ ).not.toBeNull();
87
+ related['active-descendant'] = within(listbox).getByRole('option', {
88
+ ...hidden,
89
+ name: 'Apple',
90
+ });
91
+ }
92
+ return related;
93
+ },
94
+ cleanup,
95
+ });
96
+ });
97
+
98
+ it('keeps the bounded first-adopter state matrix explicit', () => {
99
+ expect(
100
+ SELECTOR_COMBOBOX_SCENARIOS.map(scenario => [
101
+ scenario.binding,
102
+ scenario.id,
103
+ ]),
104
+ ).toEqual([
105
+ ['Selector.trigger', 'closed'],
106
+ ['Selector.trigger', 'open'],
107
+ ['Selector.trigger', 'read-only'],
108
+ ['Selector.trigger', 'busy'],
109
+ ['Selector.trigger', 'sheet-open'],
110
+ ['Selector.search', 'search-active'],
111
+ ]);
112
+ expect(SELECTOR_COMBOBOX_EXCLUSIONS).toHaveLength(5);
113
+ });
114
+
115
+ it('pins every known failure to exactly one bound state', () => {
116
+ expect(SELECTOR_COMBOBOX_KNOWN_FAILURES).toHaveLength(1);
117
+ for (const record of SELECTOR_COMBOBOX_KNOWN_FAILURES) {
118
+ expect(
119
+ SELECTOR_COMBOBOX_SCENARIOS.filter(
120
+ scenario =>
121
+ scenario.binding === record.binding && scenario.id === record.state,
122
+ ),
123
+ ).toHaveLength(1);
124
+ }
125
+ });
126
+ });
@@ -989,6 +989,41 @@ describe('SideNav resizable', () => {
989
989
  ).toBeInTheDocument();
990
990
  });
991
991
 
992
+ it('names the nav it resizes with aria-controls (APG window splitter)', () => {
993
+ render(<SideNav resizable>Content</SideNav>);
994
+ const nav = screen.getByRole('navigation');
995
+ const handle = screen.getByRole('separator');
996
+ expect(nav.id).not.toBe('');
997
+ expect(handle).toHaveAttribute('aria-controls', nav.id);
998
+ });
999
+
1000
+ it("keeps a consumer's nav id and points the handle at it", () => {
1001
+ render(
1002
+ <SideNav resizable id="primary-nav">
1003
+ Content
1004
+ </SideNav>,
1005
+ );
1006
+ expect(screen.getByRole('navigation')).toHaveAttribute('id', 'primary-nav');
1007
+ expect(screen.getByRole('separator')).toHaveAttribute(
1008
+ 'aria-controls',
1009
+ 'primary-nav',
1010
+ );
1011
+ });
1012
+
1013
+ it('keeps the generated nav id when a wrapper forwards id={undefined}', () => {
1014
+ render(
1015
+ <SideNav resizable id={undefined}>
1016
+ Content
1017
+ </SideNav>,
1018
+ );
1019
+ const nav = screen.getByRole('navigation');
1020
+ expect(nav.id).not.toBe('');
1021
+ expect(screen.getByRole('separator')).toHaveAttribute(
1022
+ 'aria-controls',
1023
+ nav.id,
1024
+ );
1025
+ });
1026
+
992
1027
  it('clips the overlay drag handle within the resizable nav bounds', () => {
993
1028
  render(<SideNav resizable>Content</SideNav>);
994
1029
  const nav = screen.getByRole('navigation');
@@ -22,7 +22,13 @@
22
22
  * - /packages/cli/assets/templates/blocks/components/SideNav/ (showcase blocks)
23
23
  */
24
24
 
25
- import {useCallback, useImperativeHandle, useRef, type ReactNode} from 'react';
25
+ import {
26
+ useCallback,
27
+ useId,
28
+ useImperativeHandle,
29
+ useRef,
30
+ type ReactNode,
31
+ } from 'react';
26
32
  import type {BaseProps} from '../BaseProps';
27
33
  import * as stylex from '@stylexjs/stylex';
28
34
  import type {StyleXStyles} from '@stylexjs/stylex';
@@ -440,6 +446,10 @@ export function SideNav({
440
446
  );
441
447
 
442
448
  const navRef = useRef<HTMLElement>(null);
449
+ // The resize handle names the pane it resizes (APG window splitter), so the
450
+ // nav needs an id; a consumer's own id wins.
451
+ const generatedNavId = useId();
452
+ const navId = props.id ?? generatedNavId;
443
453
  const mergedNavRef = useMergedRefs(ref, navRef);
444
454
  const collapseStateRef = useRef<SideNavCollapseState>({
445
455
  isCollapsed: false,
@@ -611,7 +621,10 @@ export function SideNav({
611
621
  className,
612
622
  resizableNavStyle,
613
623
  )}
614
- {...props}>
624
+ {...props}
625
+ // After the spread: a wrapper forwarding `id={undefined}` must not erase
626
+ // the id the resize handle's aria-controls points at.
627
+ id={navId}>
615
628
  {hasStickyTop && (
616
629
  <div
617
630
  {...stylex.props(
@@ -672,6 +685,7 @@ export function SideNav({
672
685
  xstyle={styles.resizableHandle}
673
686
  resizable={resizableHook.props}
674
687
  label={t('@astryx.sideNav.resizeSidebar')}
688
+ aria-controls={navId}
675
689
  />
676
690
  </div>
677
691
  ) : (
@@ -35,8 +35,10 @@ an additive target for the interactive control surface.
35
35
  ## Compatibility and migration
36
36
 
37
37
  - Released default preserved: `yes`
38
- - Compatibility class: additive target and state reflection only; runtime,
39
- default styling, DOM semantics, and public props remain unchanged
38
+ - Compatibility class: additive target and state reflection; default styling,
39
+ DOM semantics, and public props remain unchanged, and the one keyboard change
40
+ AR1 names — the horizontal arrows follow the track's direction under RTL —
41
+ ships as a behavior fix
40
42
  - Controlled/uncontrolled behavior: unchanged; Slider remains controlled
41
43
  - Migration decision: none
42
44
 
@@ -62,9 +64,9 @@ Consumer migration instructions belong in consumer docs and release notes.
62
64
 
63
65
  ## Public concepts
64
66
 
65
- This adds one public theming target without adding or changing a component prop,
66
- value domain, or behavior. Consumer props, modes, states, and usage remain
67
- documented in `Slider.doc.mjs`.
67
+ The record adds one public theming target and names the keyboard model (AR1)
68
+ without adding or changing a component prop or value domain. Consumer props,
69
+ modes, states, and usage remain documented in `Slider.doc.mjs`.
68
70
 
69
71
  ## Behavioral and layout contract
70
72
 
@@ -102,8 +104,10 @@ either component's remaining target shape.
102
104
 
103
105
  ### Transformation and precedence order
104
106
 
105
- - No new value, snapping, geometry, interaction, or styling precedence rule is
106
- introduced.
107
+ - ORD1: an arrow or Page key result snaps to the nearest step, then clamps to
108
+ the bounds; Home and End go to `min` and `max` (AR1).
109
+ - The anatomy decision (DEC-1) introduces no value, snapping, geometry,
110
+ interaction, or styling precedence rule of its own.
107
111
 
108
112
  ### Performance and resources
109
113
 
@@ -111,9 +115,21 @@ either component's remaining target shape.
111
115
 
112
116
  ## Accessibility contract
113
117
 
114
- This contract does not change or extend Slider's existing accessible name, value,
115
- range-thumb naming, description/status association, keyboard behavior, disabled
116
- behavior, or value-tooltip behavior.
118
+ The anatomy decision (DEC-1) does not change Slider's accessible name, value,
119
+ range-thumb naming, description/status association, disabled behavior, or
120
+ value-tooltip behavior; AR1 records the keyboard model and its one correction.
121
+
122
+ - **AR1 — Slider adopts the APG Slider pattern** (`spec:AST-020/FR15`), each
123
+ thumb as one composite control: every thumb is `role="slider"` with an
124
+ accessible name (a range names each thumb), `aria-valuemin`, `aria-valuemax`,
125
+ `aria-valuenow`, `aria-valuetext` when a formatter changes the spoken value,
126
+ `aria-orientation`, and `aria-disabled` when disabled; an enabled thumb, and a
127
+ disabled thumb that carries a disabled message, is a Tab stop.
128
+ ArrowUp and ArrowDown step by `step`; on a horizontal track the horizontal
129
+ arrows follow the track's direction — ArrowRight increases and ArrowLeft
130
+ decreases under LTR, and under RTL ArrowLeft increases and ArrowRight
131
+ decreases; PageUp and PageDown step by ten steps; Home moves to `min` and End to `max`. In a range, a thumb never crosses the other past the minimum
132
+ separation.
117
133
 
118
134
  ## Design relationships
119
135
 
@@ -198,13 +214,14 @@ separately listed value tooltip retains Tooltip's target.
198
214
 
199
215
  ## Verification map
200
216
 
201
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
202
- | ------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------- |
203
- | FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks | Removing or misaligning stable parts breaks existing role, position, or mark assertions. | `audit:Slider/anatomy` |
204
- | FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb | A target class/state is missing, undocumented, or placed outside its owning anatomy. | `audit:Slider/theming` |
205
- | FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value | A missing target is inaccurately documented as present or intentionally permanent. | `audit:Slider/theming` |
206
- | FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` |
207
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` |
217
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
218
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
219
+ | FR1 | `Slider.test.tsx` structure, range, and marks suites | Single, range, horizontal, vertical, marks | Removing or misaligning stable parts breaks existing role, position, or mark assertions. | `audit:Slider/anatomy` |
220
+ | AR1 | Slider accessibility contract (`internal/a11y-spec`, pending) bound to Slider, in jsdom and Chromium; `Slider.test.tsx` keyboard suites, including "follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged" and "keeps ArrowLeft decreasing on a vertical track under RTL"; the Page, Home/End and Up/Down cases under the contract | Single and range; horizontal LTR and RTL; vertical; disabled; formatted value | A thumb loses its role, name or value properties; an arrow, Page, Home or End key stops stepping or steps against the mirrored track; a range thumb crosses the other | `audit:Slider/accessibility` |
221
+ | FR2 | `Slider.test.tsx`, `themingTargets.test.ts`, generated probe theme, and Chromium | Root, horizontal/vertical control, disabled control, track, and thumb | A target class/state is missing, undocumented, or placed outside its owning anatomy. | `audit:Slider/theming` |
222
+ | FR3 | Source and consumer-doc review | Filled range, marks, labels, adjacent text value | A missing target is inaccurately documented as present or intentionally permanent. | `audit:Slider/theming` |
223
+ | FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` |
224
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` |
208
225
 
209
226
  The focused Slider suite asserts the `slider-control` target's orientation and
210
227
  disabled state. Source/metadata guards cover all four target declarations. The
@@ -224,6 +241,15 @@ consumer-recognizable part, so it carries `slider-control`. `Thumb` remains
224
241
  separate anatomy and continues to carry `slider-thumb`. This adds no default
225
242
  behavior or styling change.
226
243
 
244
+ ### DEC-2 — Each thumb is an APG slider, and horizontal arrows follow the track
245
+
246
+ **Reference:** `component:Slider/DEC-2`
247
+ **Decider:** `cixzhang`, `2026-10-13`
248
+
249
+ A thumb already plays the slider role as one composite control, so it carries the pattern's value properties and its whole keyboard model (AR1). The horizontal arrows follow the track's painted direction because a key that moves the thumb against the way it travels on screen is a defect, not a convention; the vertical arrows and Up/Down are direction-free.
250
+
251
+ Rejected: keeping ArrowRight as "increase" under RTL; a Tab stop or arrow model per range thumb that differs from the single thumb.
252
+
227
253
  ## Open questions
228
254
 
229
255
  - **OQ1 — After a joint ProgressBar and Slider family audit, should any shared
@@ -510,6 +510,69 @@ describe('Slider', () => {
510
510
  expect(handleChangeEnd).toHaveBeenCalledWith([0.2, 0.7]);
511
511
  });
512
512
 
513
+ it('End reaches an off-grid max from a non-zero min, and Home still reaches min', async () => {
514
+ const user = userEvent.setup();
515
+ const onChange = vi.fn();
516
+ const onChangeEnd = vi.fn();
517
+ // From min 1 with step 3 the grid is 1, 4, …, 100; max 101 is off it.
518
+ render(
519
+ <Slider
520
+ label="Zoom"
521
+ min={1}
522
+ max={101}
523
+ step={3}
524
+ value={49}
525
+ onChange={onChange}
526
+ onChangeEnd={onChangeEnd}
527
+ />,
528
+ );
529
+ const thumb = screen.getByRole('slider');
530
+ thumb.focus();
531
+
532
+ await user.keyboard('{End}');
533
+ expect(onChange).toHaveBeenLastCalledWith(101);
534
+ expect(onChangeEnd).toHaveBeenLastCalledWith(101);
535
+
536
+ await user.keyboard('{Home}');
537
+ expect(onChange).toHaveBeenLastCalledWith(1);
538
+ expect(onChangeEnd).toHaveBeenLastCalledWith(1);
539
+ });
540
+
541
+ it('End lands on max when max is off the step grid, while arrows still snap', async () => {
542
+ const user = userEvent.setup();
543
+ const onChange = vi.fn();
544
+ function Owner() {
545
+ const [value, setValue] = useState(48);
546
+ return (
547
+ <Slider
548
+ label="Zoom"
549
+ min={0}
550
+ max={100}
551
+ step={3}
552
+ value={value}
553
+ onChange={(next: number) => {
554
+ onChange(next);
555
+ setValue(next);
556
+ }}
557
+ />
558
+ );
559
+ }
560
+ render(<Owner />);
561
+ const thumb = screen.getByRole('slider');
562
+ thumb.focus();
563
+
564
+ await user.keyboard('{End}');
565
+ expect(onChange).toHaveBeenLastCalledWith(100);
566
+
567
+ // ORD1 for arrows and Page keys is unchanged: from max (100, off the
568
+ // grid) ArrowLeft targets 97 and snaps to 96; from 96 PageUp targets 126
569
+ // and clamps to 100, the bound.
570
+ await user.keyboard('{ArrowLeft}');
571
+ expect(onChange).toHaveBeenLastCalledWith(96);
572
+ await user.keyboard('{PageUp}');
573
+ expect(onChange).toHaveBeenLastCalledWith(100);
574
+ });
575
+
513
576
  it('fires onChangeEnd on keyboard Home/End with correct value', async () => {
514
577
  const user = userEvent.setup();
515
578
  const handleChangeEnd = vi.fn();
@@ -708,6 +771,77 @@ describe('Slider', () => {
708
771
  gcsSpy.mockRestore();
709
772
  });
710
773
 
774
+ it('follows the mirrored track under RTL: ArrowLeft increases, ArrowRight decreases, Up/Down unchanged', () => {
775
+ const handleChange = vi.fn();
776
+ render(
777
+ <Slider
778
+ label="Volume"
779
+ value={50}
780
+ min={0}
781
+ max={100}
782
+ step={1}
783
+ valueDisplay="none"
784
+ onChange={handleChange}
785
+ />,
786
+ );
787
+ const thumb = screen.getByRole('slider');
788
+ const track = thumb.parentElement!;
789
+ const realGetComputedStyle = window.getComputedStyle;
790
+ const gcsSpy = vi
791
+ .spyOn(window, 'getComputedStyle')
792
+ .mockImplementation((el: Element, pseudo?: string | null) => {
793
+ if (el === track) {
794
+ return {direction: 'rtl'} as CSSStyleDeclaration;
795
+ }
796
+ return realGetComputedStyle(el, pseudo ?? undefined);
797
+ });
798
+
799
+ fireEvent.keyDown(thumb, {key: 'ArrowLeft'});
800
+ expect(handleChange).toHaveBeenLastCalledWith(51);
801
+ fireEvent.keyDown(thumb, {key: 'ArrowRight'});
802
+ expect(handleChange).toHaveBeenLastCalledWith(49);
803
+ fireEvent.keyDown(thumb, {key: 'ArrowUp'});
804
+ expect(handleChange).toHaveBeenLastCalledWith(51);
805
+ fireEvent.keyDown(thumb, {key: 'ArrowDown'});
806
+ expect(handleChange).toHaveBeenLastCalledWith(49);
807
+
808
+ gcsSpy.mockRestore();
809
+ });
810
+
811
+ it('keeps ArrowLeft decreasing on a vertical track under RTL', () => {
812
+ const handleChange = vi.fn();
813
+ render(
814
+ <Slider
815
+ label="Volume"
816
+ value={50}
817
+ min={0}
818
+ max={100}
819
+ step={1}
820
+ orientation="vertical"
821
+ valueDisplay="none"
822
+ onChange={handleChange}
823
+ />,
824
+ );
825
+ const thumb = screen.getByRole('slider');
826
+ const track = thumb.parentElement!;
827
+ const realGetComputedStyle = window.getComputedStyle;
828
+ const gcsSpy = vi
829
+ .spyOn(window, 'getComputedStyle')
830
+ .mockImplementation((el: Element, pseudo?: string | null) => {
831
+ if (el === track) {
832
+ return {direction: 'rtl'} as CSSStyleDeclaration;
833
+ }
834
+ return realGetComputedStyle(el, pseudo ?? undefined);
835
+ });
836
+
837
+ fireEvent.keyDown(thumb, {key: 'ArrowLeft'});
838
+ expect(handleChange).toHaveBeenLastCalledWith(49);
839
+ fireEvent.keyDown(thumb, {key: 'ArrowRight'});
840
+ expect(handleChange).toHaveBeenLastCalledWith(51);
841
+
842
+ gcsSpy.mockRestore();
843
+ });
844
+
711
845
  // --- Mark label click snapping ---
712
846
 
713
847
  it('clicking a mark label snaps to that mark value, not pointer position', () => {
@@ -668,11 +668,18 @@ export function Slider({ref, ...props}: SliderProps) {
668
668
  );
669
669
 
670
670
  const updateValue = useCallback(
671
- (thumbIndex: number, newVal: number) => {
671
+ (thumbIndex: number, newVal: number, snap = true) => {
672
672
  if (isDisabled) {
673
673
  return;
674
674
  }
675
- const clamped = clamp(snapToStep(newVal, min, step), min, max);
675
+ // Home and End pass the bound itself (`snap: false`). The step grid is
676
+ // anchored at `min`, so Home always landed there; `max` may lie off the
677
+ // grid (max 100 with step 3 snapped End to 99, not 100).
678
+ const clamped = clamp(
679
+ snap ? snapToStep(newVal, min, step) : newVal,
680
+ min,
681
+ max,
682
+ );
676
683
 
677
684
  if (isRange) {
678
685
  const currentValues = [...values] as [number, number];
@@ -827,12 +834,22 @@ export function Slider({ref, ...props}: SliderProps) {
827
834
  const currentVal = values[thumbIndex];
828
835
  let newVal: number;
829
836
 
837
+ // On a horizontal track the keyboard follows the track's direction:
838
+ // under RTL the value grows toward the left, so ArrowLeft increases and
839
+ // ArrowRight decreases (APG Slider). Up/Down and a vertical track are
840
+ // direction-free.
841
+ const towardMax =
842
+ isHorizontal && isRtlElement(trackRef.current)
843
+ ? 'ArrowLeft'
844
+ : 'ArrowRight';
845
+ const towardMin = towardMax === 'ArrowLeft' ? 'ArrowRight' : 'ArrowLeft';
846
+
830
847
  switch (e.key) {
831
- case 'ArrowRight':
848
+ case towardMax:
832
849
  case 'ArrowUp':
833
850
  newVal = currentVal + step;
834
851
  break;
835
- case 'ArrowLeft':
852
+ case towardMin:
836
853
  case 'ArrowDown':
837
854
  newVal = currentVal - step;
838
855
  break;
@@ -853,8 +870,13 @@ export function Slider({ref, ...props}: SliderProps) {
853
870
  }
854
871
 
855
872
  e.preventDefault();
856
- const clamped = clamp(snapToStep(newVal, min, step), min, max);
857
- updateValue(thumbIndex, newVal);
873
+ const toBound = e.key === 'Home' || e.key === 'End';
874
+ const clamped = clamp(
875
+ toBound ? newVal : snapToStep(newVal, min, step),
876
+ min,
877
+ max,
878
+ );
879
+ updateValue(thumbIndex, newVal, !toBound);
858
880
 
859
881
  // Compute exact post-update values so onChangeEnd reports the correct
860
882
  // value even before React batches the state update.
@@ -876,6 +898,7 @@ export function Slider({ref, ...props}: SliderProps) {
876
898
  },
877
899
  [
878
900
  isDisabled,
901
+ isHorizontal,
879
902
  isRange,
880
903
  values,
881
904
  step,