@astryxdesign/core 0.6.2 → 0.6.3-canary.0949cb1

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 (384) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +3 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts +7 -5
  6. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.js +56 -19
  8. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  9. package/dist/Chat/ChatComposer.js +10 -5
  10. package/dist/Chat/ChatComposerDrawer.d.ts +19 -5
  11. package/dist/Chat/ChatComposerDrawer.d.ts.map +1 -1
  12. package/dist/Chat/ChatComposerDrawer.js +48 -23
  13. package/dist/Chat/ChatComposerInput.d.ts +11 -3
  14. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  15. package/dist/Chat/ChatComposerInput.js +63 -16
  16. package/dist/Chat/ChatDictationButton.d.ts +3 -2
  17. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  18. package/dist/Chat/ChatDictationButton.js +6 -4
  19. package/dist/Chat/ChatSendButton.js +1 -1
  20. package/dist/Chat/index.d.ts +1 -1
  21. package/dist/Chat/index.d.ts.map +1 -1
  22. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  23. package/dist/CheckboxInput/CheckboxInput.js +12 -2
  24. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  25. package/dist/Collapsible/Collapsible.js +6 -1
  26. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  27. package/dist/CommandPalette/CommandPaletteInput.js +1 -1
  28. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  29. package/dist/ComplexSelector/ComplexSelector.js +1 -1
  30. package/dist/DateInput/DateInput.d.ts.map +1 -1
  31. package/dist/DateInput/DateInput.js +4 -4
  32. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  33. package/dist/DateInput/NativeDateField.js +16 -16
  34. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  35. package/dist/DateInput/TouchDateField.js +2 -2
  36. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  37. package/dist/DateRangeInput/DateRangeInput.js +4 -4
  38. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  39. package/dist/DateTimeInput/DateTimeInput.js +8 -8
  40. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  41. package/dist/DateTimeInput/TouchDateTimeField.js +4 -4
  42. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts +9 -3
  43. package/dist/DateTimeInput/nativePickerSegmentStyles.d.ts.map +1 -1
  44. package/dist/DateTimeInput/nativePickerSegmentStyles.js +3 -3
  45. package/dist/Dialog/DialogHeader.d.ts +8 -1
  46. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  47. package/dist/Dialog/DialogHeader.js +10 -1
  48. package/dist/Field/InputClearButton.d.ts +4 -2
  49. package/dist/Field/InputClearButton.d.ts.map +1 -1
  50. package/dist/Field/InputClearButton.js +4 -2
  51. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  52. package/dist/Field/PanelSearchInput.js +1 -1
  53. package/dist/FileInput/FileInput.d.ts.map +1 -1
  54. package/dist/FileInput/FileInput.js +7 -7
  55. package/dist/Kbd/Kbd.d.ts +5 -3
  56. package/dist/Kbd/Kbd.d.ts.map +1 -1
  57. package/dist/Kbd/Kbd.js +36 -42
  58. package/dist/Link/Link.d.ts.map +1 -1
  59. package/dist/Link/Link.js +6 -2
  60. package/dist/Markdown/Markdown.d.ts +8 -2
  61. package/dist/Markdown/Markdown.d.ts.map +1 -1
  62. package/dist/Markdown/Markdown.fr23.bench.d.ts +2 -0
  63. package/dist/Markdown/Markdown.fr23.bench.d.ts.map +1 -0
  64. package/dist/Markdown/Markdown.fr23.bench.js +150 -0
  65. package/dist/Markdown/Markdown.fr23.sampling.d.ts +2 -0
  66. package/dist/Markdown/Markdown.fr23.sampling.d.ts.map +1 -0
  67. package/dist/Markdown/Markdown.fr23.sampling.js +43 -0
  68. package/dist/Markdown/Markdown.js +262 -109
  69. package/dist/Markdown/ast.d.ts +162 -0
  70. package/dist/Markdown/ast.d.ts.map +1 -0
  71. package/dist/Markdown/ast.js +74 -0
  72. package/dist/Markdown/index.d.ts +5 -3
  73. package/dist/Markdown/index.d.ts.map +1 -1
  74. package/dist/Markdown/index.js +3 -2
  75. package/dist/Markdown/parser/index.d.ts +10 -0
  76. package/dist/Markdown/parser/index.d.ts.map +1 -0
  77. package/dist/Markdown/parser/index.js +10 -0
  78. package/dist/Markdown/parser.d.ts +126 -58
  79. package/dist/Markdown/parser.d.ts.map +1 -1
  80. package/dist/Markdown/parser.js +716 -167
  81. package/dist/Markdown/plugins/frontmatter.d.ts +32 -0
  82. package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -0
  83. package/dist/Markdown/plugins/frontmatter.js +132 -0
  84. package/dist/Markdown/plugins/index.d.ts +16 -0
  85. package/dist/Markdown/plugins/index.d.ts.map +1 -0
  86. package/dist/Markdown/plugins/index.js +13 -0
  87. package/dist/Markdown/plugins/protocol.d.ts +191 -0
  88. package/dist/Markdown/plugins/protocol.d.ts.map +1 -0
  89. package/dist/Markdown/plugins/protocol.js +957 -0
  90. package/dist/Markdown/plugins/semanticFence.d.ts +36 -0
  91. package/dist/Markdown/plugins/semanticFence.d.ts.map +1 -0
  92. package/dist/Markdown/plugins/semanticFence.js +213 -0
  93. package/dist/Markdown/plugins/sourceDecoration.d.ts +76 -0
  94. package/dist/Markdown/plugins/sourceDecoration.d.ts.map +1 -0
  95. package/dist/Markdown/plugins/sourceDecoration.js +393 -0
  96. package/dist/Markdown/plugins/textTransform.d.ts +30 -0
  97. package/dist/Markdown/plugins/textTransform.d.ts.map +1 -0
  98. package/dist/Markdown/plugins/textTransform.js +374 -0
  99. package/dist/Markdown/remark.d.ts +196 -0
  100. package/dist/Markdown/remark.d.ts.map +1 -0
  101. package/dist/Markdown/remark.js +1062 -0
  102. package/dist/Markdown/url.d.ts +4 -0
  103. package/dist/Markdown/url.d.ts.map +1 -0
  104. package/dist/Markdown/url.js +25 -0
  105. package/dist/Markdown/utils.d.ts +4 -2
  106. package/dist/Markdown/utils.d.ts.map +1 -1
  107. package/dist/Markdown/utils.js +2 -1
  108. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  109. package/dist/NumberInput/NumberInput.js +4 -4
  110. package/dist/Outline/index.d.ts +1 -0
  111. package/dist/Outline/index.d.ts.map +1 -1
  112. package/dist/Outline/parseOutlineFromMarkdown.d.ts +7 -1
  113. package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
  114. package/dist/Outline/parseOutlineFromMarkdown.js +14 -8
  115. package/dist/Outline/useOutlineFromMarkdown.d.ts +3 -1
  116. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  117. package/dist/Outline/useOutlineFromMarkdown.js +8 -3
  118. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  119. package/dist/PowerSearch/PowerSearchEditPopover.js +46 -30
  120. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  121. package/dist/ProgressBar/ProgressBar.js +36 -31
  122. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  123. package/dist/RadioList/RadioListItem.js +13 -1
  124. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  125. package/dist/SegmentedControl/SegmentedControlItem.js +5 -5
  126. package/dist/Selector/Selector.d.ts +9 -7
  127. package/dist/Selector/Selector.d.ts.map +1 -1
  128. package/dist/Selector/Selector.js +7 -5
  129. package/dist/SideNav/SideNav.d.ts +2 -1
  130. package/dist/SideNav/SideNav.d.ts.map +1 -1
  131. package/dist/SideNav/SideNav.js +9 -3
  132. package/dist/Slider/Slider.d.ts.map +1 -1
  133. package/dist/Slider/Slider.js +28 -12
  134. package/dist/Spinner/Spinner.d.ts.map +1 -1
  135. package/dist/Spinner/Spinner.js +23 -7
  136. package/dist/Switch/Switch.d.ts.map +1 -1
  137. package/dist/Switch/Switch.js +11 -0
  138. package/dist/TabList/Tab.d.ts +1 -1
  139. package/dist/TabList/Tab.d.ts.map +1 -1
  140. package/dist/TabList/Tab.js +20 -7
  141. package/dist/Table/Table.d.ts +1 -0
  142. package/dist/Table/Table.d.ts.map +1 -1
  143. package/dist/Table/Table.js +29 -13
  144. package/dist/TextArea/TextArea.d.ts.map +1 -1
  145. package/dist/TextArea/TextArea.js +1 -1
  146. package/dist/TextInput/TextInput.d.ts.map +1 -1
  147. package/dist/TextInput/TextInput.js +2 -2
  148. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  149. package/dist/TimeInput/TimeInput.js +4 -4
  150. package/dist/ToggleButton/ToggleButton.d.ts +9 -6
  151. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  152. package/dist/ToggleButton/ToggleButton.js +15 -27
  153. package/dist/TreeList/TreeList.d.ts.map +1 -1
  154. package/dist/TreeList/TreeList.js +3 -0
  155. package/dist/TreeList/TreeListItem.d.ts +6 -1
  156. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  157. package/dist/TreeList/TreeListItem.js +12 -2
  158. package/dist/TreeList/TreeListTypes.d.ts +8 -1
  159. package/dist/TreeList/TreeListTypes.d.ts.map +1 -1
  160. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  161. package/dist/Typeahead/BaseTypeahead.js +16 -7
  162. package/dist/astryx.css +18 -7
  163. package/dist/hooks/scrollKeyboardDelegation.d.ts +3 -0
  164. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -0
  165. package/dist/hooks/scrollKeyboardDelegation.js +146 -0
  166. package/dist/hooks/useScrollableArea.d.ts +6 -2
  167. package/dist/hooks/useScrollableArea.d.ts.map +1 -1
  168. package/dist/hooks/useScrollableArea.js +17 -5
  169. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  170. package/dist/theme/generateThemeRules.js +10 -0
  171. package/dist/utils/interactionOverlay.stylex.d.ts +8 -0
  172. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  173. package/dist/utils/interactionOverlay.stylex.js +9 -0
  174. package/locales/af-ZA.json +530 -1
  175. package/locales/ar-SA.json +490 -1
  176. package/locales/ca-ES.json +514 -1
  177. package/locales/cs-CZ.json +498 -1
  178. package/locales/da-DK.json +522 -1
  179. package/locales/de-DE.json +522 -1
  180. package/locales/el-GR.json +498 -1
  181. package/locales/es-ES.json +510 -1
  182. package/locales/fi-FI.json +498 -1
  183. package/locales/fr-FR.json +534 -1
  184. package/locales/he-IL.json +498 -1
  185. package/locales/hu-HU.json +498 -1
  186. package/locales/it-IT.json +510 -1
  187. package/locales/ja-JP.json +490 -1
  188. package/locales/ko-KR.json +498 -1
  189. package/locales/nl-NL.json +534 -1
  190. package/locales/no-NO.json +510 -1
  191. package/locales/pl-PL.json +506 -1
  192. package/locales/pt-BR.json +506 -1
  193. package/locales/pt-PT.json +506 -1
  194. package/locales/ro-RO.json +506 -1
  195. package/locales/ru-RU.json +498 -1
  196. package/locales/sr-SP.json +498 -1
  197. package/locales/sv-SE.json +502 -1
  198. package/locales/tr-TR.json +498 -1
  199. package/locales/uk-UA.json +498 -1
  200. package/locales/vi-VN.json +502 -1
  201. package/locales/zh-CN.json +490 -1
  202. package/locales/zh-TW.json +490 -1
  203. package/package.json +19 -4
  204. package/src/BottomSheet/BottomSheet.doc.mjs +8 -1
  205. package/src/BottomSheet/BottomSheet.spec.md +46 -20
  206. package/src/BottomSheet/BottomSheet.test.tsx +6 -3
  207. package/src/BottomSheet/BottomSheet.tsx +3 -1
  208. package/src/BottomSheet/BottomSheetKeyboard.test.tsx +195 -0
  209. package/src/BottomSheet/BottomSheetPanel.test.tsx +11 -1
  210. package/src/BottomSheet/BottomSheetPanel.tsx +49 -16
  211. package/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts +344 -0
  212. package/src/Button/__tests__/Button.a11y.chromium.spec.ts +17 -1
  213. package/src/Button/__tests__/Button.a11y.known-failures.ts +0 -29
  214. package/src/Button/__tests__/Button.a11y.renders.tsx +9 -2
  215. package/src/Button/__tests__/Button.a11y.states.ts +9 -0
  216. package/src/Chat/ChatComposer.doc.mjs +14 -0
  217. package/src/Chat/ChatComposer.spec.md +230 -0
  218. package/src/Chat/ChatComposer.test.tsx +57 -0
  219. package/src/Chat/ChatComposer.tsx +21 -9
  220. package/src/Chat/ChatComposerDrawer.doc.mjs +63 -5
  221. package/src/Chat/ChatComposerDrawer.spec.md +267 -0
  222. package/src/Chat/ChatComposerDrawer.test.tsx +81 -12
  223. package/src/Chat/ChatComposerDrawer.tsx +38 -9
  224. package/src/Chat/ChatComposerInput.doc.mjs +114 -26
  225. package/src/Chat/ChatComposerInput.spec.md +185 -0
  226. package/src/Chat/ChatComposerInput.test.tsx +113 -3
  227. package/src/Chat/ChatComposerInput.tsx +78 -13
  228. package/src/Chat/ChatComposerTokenElement.spec.md +152 -0
  229. package/src/Chat/ChatComposerTokenElement.test.tsx +85 -0
  230. package/src/Chat/ChatDictationButton.doc.mjs +3 -3
  231. package/src/Chat/ChatDictationButton.spec.md +160 -0
  232. package/src/Chat/ChatDictationButton.test.tsx +92 -27
  233. package/src/Chat/ChatDictationButton.tsx +8 -6
  234. package/src/Chat/ChatSendButton.test.tsx +1 -4
  235. package/src/Chat/ChatSendButton.tsx +1 -1
  236. package/src/Chat/index.ts +1 -0
  237. package/src/CheckboxInput/CheckboxInput.doc.mjs +11 -0
  238. package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
  239. package/src/CheckboxInput/CheckboxInput.tsx +21 -1
  240. package/src/ClickableCard/ClickableCard.test.tsx +102 -5
  241. package/src/Collapsible/Collapsible.doc.mjs +11 -0
  242. package/src/Collapsible/Collapsible.test.tsx +21 -0
  243. package/src/Collapsible/Collapsible.tsx +5 -0
  244. package/src/CommandPalette/CommandPaletteInput.tsx +6 -1
  245. package/src/ComplexSelector/ComplexSelector.tsx +6 -1
  246. package/src/DateInput/DateInput.tsx +6 -1
  247. package/src/DateInput/NativeDateField.tsx +24 -8
  248. package/src/DateInput/TouchDateField.tsx +6 -2
  249. package/src/DateRangeInput/DateRangeInput.tsx +6 -1
  250. package/src/DateTimeInput/DateTimeInput.tsx +6 -1
  251. package/src/DateTimeInput/TouchDateTimeField.tsx +6 -1
  252. package/src/DateTimeInput/nativePickerSegmentStyles.ts +24 -8
  253. package/src/Dialog/DialogHeader.doc.mjs +12 -0
  254. package/src/Dialog/DialogHeader.test.tsx +97 -0
  255. package/src/Dialog/DialogHeader.tsx +25 -3
  256. package/src/Dialog/modules/DialogHeader.spec.md +75 -38
  257. package/src/Field/InputClearButton.test.tsx +17 -3
  258. package/src/Field/InputClearButton.tsx +5 -2
  259. package/src/Field/PanelSearchInput.tsx +5 -2
  260. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +1 -1
  261. package/src/FileInput/FileInput.tsx +11 -2
  262. package/src/Kbd/Kbd.doc.mjs +3 -3
  263. package/src/Kbd/Kbd.test.tsx +57 -1
  264. package/src/Kbd/Kbd.tsx +57 -37
  265. package/src/Layout/LayoutFooter.doc.mjs +17 -0
  266. package/src/Link/Link.doc.mjs +11 -0
  267. package/src/Link/Link.test.tsx +24 -0
  268. package/src/Link/Link.tsx +5 -0
  269. package/src/List/List.spec.md +200 -0
  270. package/src/Markdown/Markdown.doc.mjs +272 -0
  271. package/src/Markdown/Markdown.fr23.bench.ts +174 -0
  272. package/src/Markdown/Markdown.fr23.perf.test.ts +249 -0
  273. package/src/Markdown/Markdown.fr23.sampling.ts +49 -0
  274. package/src/Markdown/Markdown.helpers.perf.test.ts +489 -0
  275. package/src/Markdown/Markdown.public.test.ts +126 -3
  276. package/src/Markdown/Markdown.spec.md +197 -44
  277. package/src/Markdown/Markdown.test.tsx +11 -0
  278. package/src/Markdown/Markdown.tsx +414 -135
  279. package/src/Markdown/ast.test.ts +307 -0
  280. package/src/Markdown/ast.ts +337 -0
  281. package/src/Markdown/incremental.test.ts +12 -0
  282. package/src/Markdown/index.ts +37 -1
  283. package/src/Markdown/modules/remark.spec.md +269 -0
  284. package/src/Markdown/parser/index.ts +64 -0
  285. package/src/Markdown/parser.public.test.ts +103 -0
  286. package/src/Markdown/parser.ts +1180 -235
  287. package/src/Markdown/plugins/frontmatter.test.tsx +125 -0
  288. package/src/Markdown/plugins/frontmatter.ts +198 -0
  289. package/src/Markdown/plugins/index.ts +53 -0
  290. package/src/Markdown/plugins/protocol.test.tsx +1022 -0
  291. package/src/Markdown/plugins/protocol.ts +1618 -0
  292. package/src/Markdown/plugins/semanticFence.test.tsx +546 -0
  293. package/src/Markdown/plugins/semanticFence.ts +424 -0
  294. package/src/Markdown/plugins/sourceDecoration.test.tsx +799 -0
  295. package/src/Markdown/plugins/sourceDecoration.ts +539 -0
  296. package/src/Markdown/plugins/textTransform.test.ts +467 -0
  297. package/src/Markdown/plugins/textTransform.ts +554 -0
  298. package/src/Markdown/remark.test.tsx +1877 -0
  299. package/src/Markdown/remark.ts +1574 -0
  300. package/src/Markdown/url.ts +30 -0
  301. package/src/Markdown/utils.ts +13 -1
  302. package/src/MultiSelector/MultiSelector.test.tsx +1 -35
  303. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +128 -0
  304. package/src/NumberInput/NumberInput.tsx +6 -1
  305. package/src/Outline/Outline.doc.mjs +7 -5
  306. package/src/Outline/Outline.spec.md +1 -1
  307. package/src/Outline/index.ts +1 -0
  308. package/src/Outline/modules/parseOutlineFromMarkdown.spec.md +142 -0
  309. package/src/Outline/parseOutlineFromMarkdown.ts +40 -10
  310. package/src/Outline/useOutlineFromMarkdown.ts +15 -3
  311. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  312. package/src/PowerSearch/PowerSearchEditPopover.test.tsx +150 -1
  313. package/src/PowerSearch/PowerSearchEditPopover.tsx +51 -28
  314. package/src/ProgressBar/ProgressBar.test.tsx +29 -11
  315. package/src/ProgressBar/ProgressBar.tsx +59 -72
  316. package/src/RadioList/RadioList.doc.mjs +11 -0
  317. package/src/RadioList/RadioList.test.tsx +32 -0
  318. package/src/RadioList/RadioListItem.tsx +25 -1
  319. package/src/ScrollableArea/modules/useScrollableArea.spec.md +50 -22
  320. package/src/SegmentedControl/SegmentedControl.doc.mjs +2 -2
  321. package/src/SegmentedControl/SegmentedControl.test.tsx +31 -0
  322. package/src/SegmentedControl/SegmentedControlItem.tsx +6 -9
  323. package/src/Selector/Selector.doc.mjs +2 -2
  324. package/src/Selector/Selector.spec.md +51 -49
  325. package/src/Selector/Selector.test.tsx +61 -55
  326. package/src/Selector/Selector.tsx +23 -14
  327. package/src/Selector/__tests__/Listbox.a11y.chromium.spec.ts +130 -0
  328. package/src/Selector/__tests__/Listbox.a11y.dom.ts +38 -0
  329. package/src/Selector/__tests__/Listbox.a11y.inventory.test.ts +63 -0
  330. package/src/Selector/__tests__/Listbox.a11y.known-failures.ts +37 -0
  331. package/src/Selector/__tests__/Listbox.a11y.states.ts +232 -0
  332. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +121 -0
  333. package/src/SideNav/SideNav.doc.mjs +1 -1
  334. package/src/SideNav/SideNav.test.tsx +10 -0
  335. package/src/SideNav/SideNav.tsx +14 -2
  336. package/src/Slider/Slider.doc.mjs +17 -3
  337. package/src/Slider/Slider.test.tsx +221 -0
  338. package/src/Slider/Slider.tsx +40 -9
  339. package/src/Spinner/Spinner.test.tsx +4 -0
  340. package/src/Spinner/Spinner.tsx +31 -18
  341. package/src/Switch/Switch.doc.mjs +11 -0
  342. package/src/Switch/Switch.test.tsx +16 -0
  343. package/src/Switch/Switch.tsx +28 -0
  344. package/src/TabList/Tab.tsx +35 -7
  345. package/src/TabList/TabList.doc.mjs +11 -0
  346. package/src/TabList/TabList.spec.md +204 -0
  347. package/src/TabList/TabList.test.tsx +66 -0
  348. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +1 -34
  349. package/src/Table/Table.doc.mjs +1 -1
  350. package/src/Table/Table.spec.md +54 -40
  351. package/src/Table/Table.test.tsx +83 -10
  352. package/src/Table/Table.tsx +30 -22
  353. package/src/Table/__tests__/TableScroll.a11y.chromium.spec.ts +137 -0
  354. package/src/TextArea/TextArea.tsx +6 -1
  355. package/src/TextInput/TextInput.test.tsx +20 -1
  356. package/src/TextInput/TextInput.tsx +6 -1
  357. package/src/TimeInput/TimeInput.tsx +6 -1
  358. package/src/ToggleButton/ToggleButton.doc.mjs +3 -3
  359. package/src/ToggleButton/ToggleButton.test.tsx +299 -4
  360. package/src/ToggleButton/ToggleButton.tsx +26 -33
  361. package/src/ToggleButton/ToggleButtonGroup.doc.mjs +3 -3
  362. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +209 -0
  363. package/src/Tokenizer/Tokenizer.spec.md +142 -75
  364. package/src/TreeList/TreeList.doc.mjs +3 -3
  365. package/src/TreeList/TreeList.spec.md +123 -75
  366. package/src/TreeList/TreeList.test.tsx +29 -0
  367. package/src/TreeList/TreeList.tsx +3 -0
  368. package/src/TreeList/TreeListItem.tsx +15 -1
  369. package/src/TreeList/TreeListTypes.ts +11 -1
  370. package/src/Typeahead/BaseTypeahead.spec.md +4 -3
  371. package/src/Typeahead/BaseTypeahead.tsx +21 -7
  372. package/src/Typeahead/Typeahead.test.tsx +53 -0
  373. package/src/__tests__/PressedState.a11y.chromium.spec.ts +813 -0
  374. package/src/__tests__/pressState.ts +93 -0
  375. package/src/hooks/scrollKeyboardDelegation.test.ts +155 -0
  376. package/src/hooks/scrollKeyboardDelegation.ts +233 -0
  377. package/src/hooks/useScrollableArea.doc.mjs +15 -3
  378. package/src/hooks/useScrollableArea.test.tsx +59 -1
  379. package/src/hooks/useScrollableArea.ts +34 -10
  380. package/src/inputFontFloor.test.ts +106 -0
  381. package/src/tailwind-theme.css +0 -6
  382. package/src/theme/generateThemeRules.test.ts +30 -0
  383. package/src/theme/generateThemeRules.ts +10 -0
  384. package/src/utils/interactionOverlay.stylex.ts +20 -0
@@ -0,0 +1,121 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Selector.listbox.a11y.test.tsx
5
+ * @input Uses Selector, its explicit part/state inventory, and the Listbox contract
6
+ * @output DOM evidence for real single-select listbox, group, and option parts
7
+ * @position Component-facing binding; keyboard, callbacks, forms, and styles stay local.
8
+ */
9
+
10
+ import {cleanup, render, screen, within} from '@testing-library/react';
11
+ import userEvent from '@testing-library/user-event';
12
+ import {describe, expect, it} from 'vitest';
13
+ import {
14
+ expectAccessibilitySpec,
15
+ LISTBOX_PATTERN,
16
+ } from '@astryxdesign/a11y-spec';
17
+ import {Selector} from '../Selector';
18
+ import {LISTBOX_SCENARIOS, listboxParts} from './Listbox.a11y.states';
19
+ import {installListboxDialogStubs} from './Listbox.a11y.dom';
20
+
21
+ installListboxDialogStubs();
22
+
23
+ const scenarios = LISTBOX_SCENARIOS.filter(
24
+ scenario => scenario.component === 'Selector',
25
+ );
26
+
27
+ describe('Selector Listbox semantic binding', () => {
28
+ it.each(scenarios)('$id', async scenario => {
29
+ for (const part of listboxParts(scenario)) {
30
+ await expectAccessibilitySpec({
31
+ spec: LISTBOX_PATTERN,
32
+ binding: `Selector.${part.role}`,
33
+ state: part.state,
34
+ facts: part.facts,
35
+ render: async () => {
36
+ render(
37
+ <div dir={scenario.direction ?? 'ltr'}>
38
+ <Selector
39
+ label="Fruit"
40
+ options={scenario.options}
41
+ value={scenario.values[0]}
42
+ onChange={() => {}}
43
+ hasSearch={scenario.hasSearch}
44
+ presentation={scenario.presentation}
45
+ isLoading={scenario.isLoading}
46
+ isLabelHidden={scenario.hiddenLabel}
47
+ renderOption={
48
+ scenario.customContent
49
+ ? option => <span>{option.label} details</span>
50
+ : undefined
51
+ }
52
+ isDefaultOpen
53
+ />
54
+ </div>,
55
+ );
56
+ await screen.findByRole('listbox', {hidden: true});
57
+ if (scenario.query != null) {
58
+ await userEvent
59
+ .setup()
60
+ .type(
61
+ screen.getByRole('combobox', {hidden: true}),
62
+ scenario.query,
63
+ );
64
+ }
65
+ },
66
+ subject: () =>
67
+ part.role === 'listbox'
68
+ ? screen.getByRole('listbox', {hidden: true})
69
+ : part.role === 'group'
70
+ ? within(
71
+ screen.getByRole('listbox', {hidden: true}),
72
+ ).getAllByRole('group', {hidden: true})[
73
+ scenario.groups.indexOf(part.name ?? '')
74
+ ]
75
+ : within(screen.getByRole('listbox', {hidden: true})).getByRole(
76
+ 'option',
77
+ {name: part.name, hidden: true},
78
+ ),
79
+ related: () => {
80
+ const listbox = screen.getByRole('listbox', {hidden: true});
81
+ expect(
82
+ within(listbox).getAllByRole('option', {hidden: true}),
83
+ ).toHaveLength(scenario.expectedOptions.length);
84
+ return {
85
+ listbox,
86
+ ...Object.fromEntries(
87
+ scenario.expectedOptions.map(option => [
88
+ `option:${option.value}`,
89
+ within(listbox).getByRole('option', {
90
+ name: option.name,
91
+ hidden: true,
92
+ }),
93
+ ]),
94
+ ),
95
+ ...Object.fromEntries(
96
+ scenario.groups.map((name, index) => [
97
+ `group:${name}`,
98
+ within(listbox).getAllByRole('group', {hidden: true})[index],
99
+ ]),
100
+ ),
101
+ };
102
+ },
103
+ cleanup,
104
+ });
105
+ }
106
+ });
107
+
108
+ it('keeps the bounded state matrix explicit', () => {
109
+ expect(scenarios.map(scenario => scenario.id)).toEqual([
110
+ 'single-unset',
111
+ 'single-selected',
112
+ 'single-disabled-selected',
113
+ 'single-grouped',
114
+ 'single-filtered',
115
+ 'single-custom-rtl',
116
+ 'single-sheet',
117
+ 'single-sheet-search',
118
+ 'single-loading',
119
+ ]);
120
+ });
121
+ });
@@ -25,7 +25,7 @@ export const docs = {
25
25
  {className: 'astryx-side-nav-section'},
26
26
  ],
27
27
  },
28
- description: 'Container with five zones: header, topContent, children (scrollable), footer, and footerIcons. Supports collapsible mode.',
28
+ description: 'Container with five zones: header, topContent, children (scrollable), footer, and footerIcons. Supports collapsible and resizable modes.',
29
29
  props: [
30
30
  {
31
31
  name: 'header',
@@ -989,6 +989,16 @@ describe('SideNav resizable', () => {
989
989
  ).toBeInTheDocument();
990
990
  });
991
991
 
992
+ it('clips the overlay drag handle within the resizable nav bounds', () => {
993
+ render(<SideNav resizable>Content</SideNav>);
994
+ const nav = screen.getByRole('navigation');
995
+ const container = nav.parentElement;
996
+ const handle = screen.getByTestId('astryx-sidenav-resize-handle');
997
+
998
+ expect(container).toContainElement(handle);
999
+ expect(getComputedStyle(container!).overflow).toBe('clip');
1000
+ });
1001
+
992
1002
  it('does not render drag handle without resizable', () => {
993
1003
  render(<SideNav>Content</SideNav>);
994
1004
  expect(
@@ -11,7 +11,8 @@
11
11
  * Sidebar navigation container with five zones: header + topContent (sticky together),
12
12
  * children (scrollable), footer, and footerIcons (sticky bottom).
13
13
  *
14
- * Supports optional resize via drag handle at the inline-end edge.
14
+ * Supports optional resize via a drag handle at the inline-end edge. The
15
+ * resizable wrapper clips handle overflow while keeping its focus ring inside.
15
16
  *
16
17
  * SYNC: When modified, update these files to stay in sync:
17
18
  * - /packages/core/src/SideNav/SideNav.doc.mjs
@@ -25,7 +26,7 @@ import {useCallback, useImperativeHandle, useRef, type ReactNode} from 'react';
25
26
  import type {BaseProps} from '../BaseProps';
26
27
  import * as stylex from '@stylexjs/stylex';
27
28
  import type {StyleXStyles} from '@stylexjs/stylex';
28
- import {spacingVars} from '../theme/tokens.stylex';
29
+ import {focusVars, spacingVars} from '../theme/tokens.stylex';
29
30
  import {mergeProps} from '../utils';
30
31
  import {
31
32
  SideNavCollapseContext,
@@ -241,6 +242,16 @@ const styles = stylex.create({
241
242
  display: 'flex',
242
243
  flexShrink: 0,
243
244
  height: '100%',
245
+ overflow: 'clip',
246
+ },
247
+ resizableHandle: {
248
+ // Keep the shared ring inside the clipping boundary. The wrapper cannot
249
+ // use overflow-clip-margin: that paint allowance also contributes the
250
+ // overlay's half-pixel spill to AppShell's scrollable overflow.
251
+ outlineOffset: {
252
+ default: '0',
253
+ ':focus-visible': `calc(0px - ${focusVars['--focus-outline-width']})`,
254
+ },
244
255
  },
245
256
  // Topbar mode — horizontal layout for mobile top bar
246
257
  topbar: {
@@ -658,6 +669,7 @@ export function SideNav({
658
669
  position="overlay"
659
670
  pillPlacement="end"
660
671
  isAlwaysVisible={false}
672
+ xstyle={styles.resizableHandle}
661
673
  resizable={resizableHook.props}
662
674
  label={t('@astryx.sideNav.resizeSidebar')}
663
675
  />
@@ -160,7 +160,8 @@ export const docs = {
160
160
  {
161
161
  name: 'marks',
162
162
  type: 'Array<{ value: number; label?: string }>',
163
- description: 'Tick marks at specified positions with optional labels.',
163
+ description:
164
+ 'Tick marks at specified positions with optional labels. Unfilled marks use the track color; marks inside the filled region (at or behind the thumb, or between the thumbs in range mode) use the fill color.',
164
165
  },
165
166
  {
166
167
  name: 'minStepsBetweenThumbs',
@@ -255,6 +256,17 @@ export const docs = {
255
256
  ],
256
257
  },
257
258
  usage: {
259
+ accessibility: [
260
+ {
261
+ name: 'Thumb',
262
+ category: 'Color contrast',
263
+ criterion: '1.4.11 Non-text Contrast',
264
+ requirement: '3:1',
265
+ states: ['Rest', 'Hover', 'Pointer down'],
266
+ description:
267
+ 'The thumb must have at least 3:1 contrast with the track and the surface behind it. Pointer down is the whole drag: measure the thumb with the pressed overlay applied.',
268
+ },
269
+ ],
258
270
  anatomy,
259
271
  description:
260
272
  'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage.',
@@ -354,7 +366,8 @@ export const docsZh = {
354
366
  {
355
367
  name: 'marks',
356
368
  type: 'Array<{ value: number; label?: string }>',
357
- description: '在指定位置的刻度标记,带可选标签。',
369
+ description:
370
+ '在指定位置的刻度标记,带可选标签。未填充区域的标记使用轨道颜色;填充区域内的标记使用强调色。',
358
371
  },
359
372
  {
360
373
  name: 'minStepsBetweenThumbs',
@@ -520,7 +533,8 @@ export const docsDense = {
520
533
  orientation: 'Slider orientation.',
521
534
  formatValue: 'Custom value formatting fn for display + aria-valuetext.',
522
535
  valueDisplay: 'How current value is displayed.',
523
- marks: 'Tick marks at specified positions w/ optional labels.',
536
+ marks:
537
+ 'Tick marks at specified positions w/ optional labels. Unfilled marks use the track color; filled marks use the accent color.',
524
538
  minStepsBetweenThumbs:
525
539
  'Min steps between thumbs in range mode; prevents overlap.',
526
540
  isDisabled: 'Whether slider is disabled.',
@@ -13,6 +13,10 @@ import {useState} from 'react';
13
13
  import {describe, it, expect, vi, beforeEach} from 'vitest';
14
14
  import {render, screen, act, fireEvent, waitFor} from '@testing-library/react';
15
15
  import userEvent from '@testing-library/user-event';
16
+ import {
17
+ declaresPressedOverlay,
18
+ rulesDeclaredFor,
19
+ } from '../__tests__/pressState';
16
20
  import * as stylex from '@stylexjs/stylex';
17
21
  import {Slider} from './Slider';
18
22
  import {focusOutlineStyles} from '../utils/focusOutline.stylex';
@@ -727,6 +731,96 @@ describe('Slider', () => {
727
731
  expect(handleChange).toHaveBeenCalledWith(100);
728
732
  });
729
733
 
734
+ // --- Marks color ---
735
+
736
+ /** Whether the element has an unconditional rule painting a token. */
737
+ function declaresColorToken(el: Element, token: string): boolean {
738
+ return rulesDeclaredFor(el).some(rule => {
739
+ const selector = rule
740
+ .slice(0, rule.indexOf('{'))
741
+ .replaceAll(':not(#\\#)', '');
742
+ return !selector.includes(':') && rule.includes(`var(${token})`);
743
+ });
744
+ }
745
+
746
+ it('uses the track color for unfilled marks and the accent for filled marks', () => {
747
+ render(
748
+ <Slider
749
+ label="Volume"
750
+ value={50}
751
+ min={0}
752
+ max={100}
753
+ marks={[{value: 25}, {value: 75}]}
754
+ />,
755
+ );
756
+ const marks = screen.getAllByTestId('slider-mark');
757
+
758
+ expect(marks).toHaveLength(2);
759
+ expect(declaresColorToken(marks[0], '--color-track')).toBe(false);
760
+ expect(declaresColorToken(marks[0], '--color-accent')).toBe(true);
761
+ expect(declaresColorToken(marks[1], '--color-track')).toBe(true);
762
+ expect(declaresColorToken(marks[1], '--color-accent')).toBe(false);
763
+ });
764
+
765
+ function declaresFillColor(el: Element): boolean {
766
+ return declaresColorToken(el, '--color-accent');
767
+ }
768
+
769
+ it('colors marks at or behind the thumb with the fill color', () => {
770
+ render(
771
+ <Slider
772
+ label="Volume"
773
+ value={50}
774
+ min={0}
775
+ max={100}
776
+ marks={[
777
+ {value: 0},
778
+ {value: 25},
779
+ {value: 50},
780
+ {value: 75},
781
+ {value: 100},
782
+ ]}
783
+ />,
784
+ );
785
+ const marks = screen.getAllByTestId('slider-mark');
786
+ expect(marks).toHaveLength(5);
787
+ // Marks at 0, 25, and 50 sit at or behind the thumb: fill color.
788
+ expect(declaresFillColor(marks[0])).toBe(true);
789
+ expect(declaresFillColor(marks[1])).toBe(true);
790
+ expect(declaresFillColor(marks[2])).toBe(true);
791
+ // Marks at 75 and 100 sit ahead of the thumb: default mark color.
792
+ expect(declaresFillColor(marks[3])).toBe(false);
793
+ expect(declaresFillColor(marks[4])).toBe(false);
794
+ });
795
+
796
+ it('colors marks between the thumbs with the fill color in range mode', () => {
797
+ render(
798
+ <Slider
799
+ label="Price range"
800
+ value={[20, 80] as [number, number]}
801
+ min={0}
802
+ max={100}
803
+ marks={[
804
+ {value: 0},
805
+ {value: 20},
806
+ {value: 50},
807
+ {value: 80},
808
+ {value: 100},
809
+ ]}
810
+ />,
811
+ );
812
+ const marks = screen.getAllByTestId('slider-mark');
813
+ expect(marks).toHaveLength(5);
814
+ // The mark at 0 sits before the range: default mark color.
815
+ expect(declaresFillColor(marks[0])).toBe(false);
816
+ // Marks at 20, 50, and 80 sit between (or at) the thumbs: fill color.
817
+ expect(declaresFillColor(marks[1])).toBe(true);
818
+ expect(declaresFillColor(marks[2])).toBe(true);
819
+ expect(declaresFillColor(marks[3])).toBe(true);
820
+ // The mark at 100 sits after the range: default mark color.
821
+ expect(declaresFillColor(marks[4])).toBe(false);
822
+ });
823
+
730
824
  // --- Boundary clamping ---
731
825
 
732
826
  it('clamps value at max boundary', async () => {
@@ -1101,3 +1195,130 @@ describe('Slider', () => {
1101
1195
  });
1102
1196
  });
1103
1197
  });
1198
+
1199
+ describe('pressed state', () => {
1200
+ it('paints the pressed overlay on the thumb for as long as it is dragged, from wherever the press lands', () => {
1201
+ render(
1202
+ <Slider label="Volume" value={50} min={0} max={100} onChange={vi.fn()} />,
1203
+ );
1204
+ const thumb = screen.getByRole('slider');
1205
+ const trackContainer = thumb.parentElement!;
1206
+ trackContainer.getBoundingClientRect = () => ({
1207
+ left: 0,
1208
+ top: 0,
1209
+ right: 200,
1210
+ bottom: 20,
1211
+ width: 200,
1212
+ height: 20,
1213
+ x: 0,
1214
+ y: 0,
1215
+ toJSON: () => {},
1216
+ });
1217
+
1218
+ expect(declaresPressedOverlay(thumb)).toBe(false);
1219
+ // A slider is a drag: the press lands on the track, not the thumb, and
1220
+ // the thumb follows it. The pressed paint follows the drag.
1221
+ fireEvent.pointerDown(trackContainer, {
1222
+ clientX: 20,
1223
+ clientY: 10,
1224
+ pointerId: 1,
1225
+ });
1226
+ expect(declaresPressedOverlay(thumb)).toBe(true);
1227
+ fireEvent.pointerMove(trackContainer, {
1228
+ clientX: 120,
1229
+ clientY: 10,
1230
+ pointerId: 1,
1231
+ });
1232
+ expect(declaresPressedOverlay(thumb)).toBe(true);
1233
+ fireEvent.pointerUp(trackContainer, {
1234
+ clientX: 120,
1235
+ clientY: 10,
1236
+ pointerId: 1,
1237
+ });
1238
+ expect(declaresPressedOverlay(thumb)).toBe(false);
1239
+ });
1240
+
1241
+ it('paints only the thumb being dragged in a range slider', () => {
1242
+ render(
1243
+ <Slider
1244
+ label="Price"
1245
+ value={[20, 80] as [number, number]}
1246
+ min={0}
1247
+ max={100}
1248
+ onChange={vi.fn()}
1249
+ />,
1250
+ );
1251
+ const [start, end] = screen.getAllByRole('slider');
1252
+ // In range mode the control is the group; each thumb sits in its own
1253
+ // value-tooltip wrapper.
1254
+ const trackContainer = screen.getByRole('group');
1255
+ trackContainer.getBoundingClientRect = () => ({
1256
+ left: 0,
1257
+ top: 0,
1258
+ right: 200,
1259
+ bottom: 20,
1260
+ width: 200,
1261
+ height: 20,
1262
+ x: 0,
1263
+ y: 0,
1264
+ toJSON: () => {},
1265
+ });
1266
+ fireEvent.pointerDown(trackContainer, {
1267
+ clientX: 170,
1268
+ clientY: 10,
1269
+ pointerId: 1,
1270
+ });
1271
+ expect(declaresPressedOverlay(end)).toBe(true);
1272
+ expect(declaresPressedOverlay(start)).toBe(false);
1273
+ fireEvent.pointerUp(trackContainer, {
1274
+ clientX: 170,
1275
+ clientY: 10,
1276
+ pointerId: 1,
1277
+ });
1278
+ expect(declaresPressedOverlay(end)).toBe(false);
1279
+ });
1280
+
1281
+ it('keeps a direct press on the visible upper thumb when range values coincide', () => {
1282
+ render(
1283
+ <Slider
1284
+ label="Price"
1285
+ value={[50, 50] as [number, number]}
1286
+ min={0}
1287
+ max={100}
1288
+ onChange={vi.fn()}
1289
+ />,
1290
+ );
1291
+ const [start, end] = screen.getAllByRole('slider');
1292
+ const trackContainer = screen.getByRole('group');
1293
+ trackContainer.getBoundingClientRect = () => ({
1294
+ left: 0,
1295
+ top: 0,
1296
+ right: 200,
1297
+ bottom: 20,
1298
+ width: 200,
1299
+ height: 20,
1300
+ x: 0,
1301
+ y: 0,
1302
+ toJSON: () => {},
1303
+ });
1304
+
1305
+ fireEvent.pointerDown(end, {
1306
+ clientX: 100,
1307
+ clientY: 10,
1308
+ pointerId: 1,
1309
+ });
1310
+ expect(declaresPressedOverlay(end)).toBe(true);
1311
+ expect(declaresPressedOverlay(start)).toBe(false);
1312
+ });
1313
+
1314
+ it('does not press a disabled slider', () => {
1315
+ render(<Slider label="Volume" value={50} isDisabled onChange={vi.fn()} />);
1316
+ const thumb = screen.getByRole('slider');
1317
+ fireEvent.pointerDown(thumb.parentElement!, {
1318
+ clientX: 20,
1319
+ clientY: 10,
1320
+ pointerId: 1,
1321
+ });
1322
+ expect(declaresPressedOverlay(thumb)).toBe(false);
1323
+ });
1324
+ });
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @file Slider.tsx
7
7
  * @input Uses React, useId, useRef, useCallback, Field, Tooltip, useTooltip, VisuallyHidden
8
- * @output Exports Slider and its props; modifier-only key presses do not restore the thumb focus ring
8
+ * @output Exports Slider and its props; unfilled marks share the track token; modifier-only key presses do not restore the thumb focus ring
9
9
  * @position Core implementation; consumed by index.ts, tested by Slider.test.tsx
10
10
  *
11
11
  * SYNC: When modified, update these files to stay in sync:
@@ -288,6 +288,15 @@ const styles = stylex.create({
288
288
  },
289
289
  },
290
290
  },
291
+ // Pressed: the system's pressed overlay over the thumb's fill for as long
292
+ // as the thumb is being dragged. A slider is a drag, not a tap — the finger
293
+ // lands anywhere on the track and the thumb follows it — so the pressed
294
+ // paint follows the drag state the pointer handlers already keep, on a
295
+ // mouse and on a finger alike, rather than `:active` on the thumb itself
296
+ // (which a press on the track never activates).
297
+ thumbPressed: {
298
+ backgroundImage: `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']})`,
299
+ },
291
300
  thumbDisabled: {
292
301
  backgroundColor: colorVars['--color-background-muted'],
293
302
  cursor: 'default',
@@ -314,9 +323,15 @@ const styles = stylex.create({
314
323
  },
315
324
  mark: {
316
325
  position: 'absolute',
317
- backgroundColor: colorVars['--color-border-emphasized'],
326
+ backgroundColor: colorVars['--color-track'],
318
327
  borderRadius: radiusVars['--radius-full'],
319
328
  },
329
+ // Marks over the filled region (at or behind the thumb in single mode,
330
+ // between the thumbs in range mode) take the fill color so they read as
331
+ // part of the filled track rather than the unfilled rail.
332
+ markFilled: {
333
+ backgroundColor: colorVars['--color-accent'],
334
+ },
320
335
  markHorizontal: {
321
336
  width: 2,
322
337
  height: 8,
@@ -701,17 +716,25 @@ export function Slider({ref, ...props}: SliderProps) {
701
716
  const newVal = markEl
702
717
  ? Number(markEl.dataset.markValue)
703
718
  : getValueFromPosition(e.clientX, e.clientY);
704
- const thumbIndex = getClosestThumb(newVal);
719
+ const track = trackRef.current;
720
+ const thumbs = track?.querySelectorAll<HTMLElement>('[role="slider"]');
721
+ const pressedThumb = (e.target as Element).closest<HTMLElement>(
722
+ '[role="slider"]',
723
+ );
724
+ const pressedThumbIndex =
725
+ pressedThumb == null || thumbs == null
726
+ ? -1
727
+ : Array.from(thumbs).indexOf(pressedThumb);
728
+ // A direct thumb press owns that thumb even when range values coincide.
729
+ // Track and mark presses still choose the nearest value.
730
+ const thumbIndex =
731
+ pressedThumbIndex >= 0 ? pressedThumbIndex : getClosestThumb(newVal);
705
732
  draggingThumbRef.current = thumbIndex;
706
733
  setDraggingThumb(thumbIndex);
707
734
  updateValue(thumbIndex, newVal);
708
735
 
709
- // Focus the closest thumb
710
- const track = trackRef.current;
711
- if (track) {
712
- const thumbs = track.querySelectorAll<HTMLElement>('[role="slider"]');
713
- thumbs[thumbIndex]?.focus();
714
- }
736
+ // Focus the thumb that owns this drag.
737
+ thumbs?.[thumbIndex]?.focus();
715
738
  // Also clear it explicitly: focusing an already-focused thumb fires no
716
739
  // focus event, so a thumb the user had tabbed to would keep its ring
717
740
  // through the drag.
@@ -908,6 +931,7 @@ export function Slider({ref, ...props}: SliderProps) {
908
931
  ? styles.thumbHorizontal
909
932
  : rtlStyles.centerInline('50%'),
910
933
  !isDisabled && styles.thumbHover,
934
+ !isDisabled && draggingThumb === thumbIndex && styles.thumbPressed,
911
935
  !isDisabled &&
912
936
  keyboardFocusThumb === thumbIndex &&
913
937
  focusOutlineStyles.focusVisible,
@@ -1085,6 +1109,12 @@ export function Slider({ref, ...props}: SliderProps) {
1085
1109
  const markPos = isHorizontal
1086
1110
  ? {insetInlineStart: insetPosition(percent)}
1087
1111
  : {bottom: insetPosition(percent)};
1112
+ // Marks at or inside the filled region take the fill color:
1113
+ // at or behind the thumb in single mode, between the thumbs
1114
+ // in range mode.
1115
+ const isFilled = isRange
1116
+ ? mark.value >= values[0] && mark.value <= values[1]
1117
+ : mark.value <= values[0];
1088
1118
  return (
1089
1119
  <div key={mark.value}>
1090
1120
  <div
@@ -1096,6 +1126,7 @@ export function Slider({ref, ...props}: SliderProps) {
1096
1126
  isHorizontal
1097
1127
  ? styles.markHorizontal
1098
1128
  : styles.markVertical,
1129
+ isFilled && styles.markFilled,
1099
1130
  ),
1100
1131
  {style: markPos},
1101
1132
  )}
@@ -404,6 +404,10 @@ describe('Spinner ring', () => {
404
404
  frames.forEach(cb => cb(0));
405
405
  expect(animations).toHaveLength(5);
406
406
  expect(animations.every(a => a.startTime === 0)).toBe(true);
407
+ // The dash animation lives on the arc <circle>, a descendant of the
408
+ // <svg> this ref sits on, not the <svg> itself (#6253) — subtree:true
409
+ // is what lets getAnimations() find it from here.
410
+ expect(getAnimations).toHaveBeenCalledWith({subtree: true});
407
411
  // The pin runs on the branch the other shade cases cannot reach, so the
408
412
  // no-read assertion is made here too.
409
413
  expect(
@@ -147,13 +147,14 @@ function registerSpinnerVars(): void {
147
147
  registerSpinnerVars();
148
148
 
149
149
  /**
150
- * Pin every ring's rotation to the document timeline's origin instead of its
151
- * own start time, so spinners mounted seconds apart turn in phase.
150
+ * Pin every ring's dash motion to the document timeline's origin instead of
151
+ * its own start time, so spinners mounted seconds apart stay in phase.
152
152
  *
153
153
  * Setting `startTime` is exact where arithmetic on a clock read is not: a
154
154
  * negative `animation-delay` computed at mount is only as good as the gap
155
155
  * between reading the clock and the frame the animation starts in, which at
156
- * 10x CPU throttling measured 116deg of drift.
156
+ * 10x CPU throttling measured 116deg of drift (from when this animated a
157
+ * literal rotation; the same drift risk applies to the dash offset now).
157
158
  *
158
159
  * Rings are collected and pinned in one frame because `getAnimations()`
159
160
  * resolves style and `startTime` dirties it again, so pinning them one at a
@@ -167,7 +168,9 @@ function pinRingsToTimelineOrigin(): void {
167
168
  flushScheduled = false;
168
169
  const animations: Animation[] = [];
169
170
  for (const svg of pendingRings) {
170
- animations.push(...svg.getAnimations());
171
+ // The animation now runs on the arc <circle>, a descendant of the ring
172
+ // this ref sits on (#6253), so it needs subtree:true to be found here.
173
+ animations.push(...svg.getAnimations({subtree: true}));
171
174
  }
172
175
  pendingRings.clear();
173
176
  for (const animation of animations) {
@@ -205,9 +208,20 @@ function syncRotationPhase(
205
208
  // Animation
206
209
  // =============================================================================
207
210
 
208
- const rotation = stylex.keyframes({
209
- '0%': {transform: 'rotate(0deg)'},
210
- '100%': {transform: 'rotate(360deg)'},
211
+ /**
212
+ * Moves the dash around a stationary circle instead of rotating the whole
213
+ * ring. WebKit re-antialiases a rotating stroked shape's rounded cap
214
+ * (`strokeLinecap: 'round'`, below) slightly differently at each intermediate
215
+ * angle, which reads as a visible wobble on iOS Safari independent of
216
+ * compositing (#6253) — confirmed by a real-device test against this exact
217
+ * fix, since `willChange`/layer promotion alone (the earlier fix for #3617)
218
+ * only smooths the rotation's motion, not the per-frame stroke rasterization.
219
+ * A static circle with an animated `stroke-dashoffset` never rotates, so
220
+ * WebKit never re-rasterizes the cap at an intermediate angle at all.
221
+ */
222
+ const dashMotion = stylex.keyframes({
223
+ '0%': {strokeDashoffset: '0px'},
224
+ '100%': {strokeDashoffset: `calc(var(${RESOLVED_DIAMETER}) * -${PI})`},
211
225
  });
212
226
 
213
227
  // =============================================================================
@@ -271,7 +285,6 @@ const styles = stylex.create({
271
285
  // One expression, one source of truth, resolves in any layout.
272
286
  width: `var(${BOX_SIZE})`,
273
287
  height: `var(${BOX_SIZE})`,
274
- willChange: 'transform',
275
288
  // The svg is sized in CSS (100% of the span) rather than from the size
276
289
  // constant, so one user unit is one CSS pixel AND the frame follows a
277
290
  // themed diameter: the span's box is `diameter + 2 x stroke`, both public
@@ -280,16 +293,6 @@ const styles = stylex.create({
280
293
  // diameter — an overflowing grid item aligns to start rather than centre,
281
294
  // which put the ring 1.5-3.3px off across the four sizes.
282
295
  overflow: 'visible',
283
- // Slow the rotation dramatically under reduced-motion rather than freezing
284
- // it (a frozen spinner reads as broken), matching ProgressBar's approach.
285
- // The role="status" + "Loading" label still convey busy state (obs-6).
286
- animationDuration: {
287
- default: durationVars['--duration-slow-min'],
288
- '@media (prefers-reduced-motion: reduce)': '3s',
289
- },
290
- animationIterationCount: 'infinite',
291
- animationName: rotation,
292
- animationTimingFunction: 'linear',
293
296
  },
294
297
  circle: {
295
298
  fill: 'none',
@@ -327,6 +330,16 @@ const styles = stylex.create({
327
330
  // `0` the way the registered length pair guards against, so it needs no
328
331
  // registration.
329
332
  arc: {
333
+ // Slow the motion dramatically under reduced-motion rather than freezing
334
+ // it (a frozen spinner reads as broken), matching ProgressBar's approach.
335
+ // The role="status" + "Loading" label still convey busy state (obs-6).
336
+ animationDuration: {
337
+ default: durationVars['--duration-slow-min'],
338
+ '@media (prefers-reduced-motion: reduce)': '3s',
339
+ },
340
+ animationIterationCount: 'infinite',
341
+ animationName: dashMotion,
342
+ animationTimingFunction: 'linear',
330
343
  stroke: 'var(--spinner-color)',
331
344
  transform: 'rotate(-90deg)',
332
345
  strokeDasharray: `calc(var(${RESOLVED_DIAMETER}) * ${PI} * var(--spinner-arc-fraction)) calc(var(${RESOLVED_DIAMETER}) * ${PI} * (1 - var(--spinner-arc-fraction)))`,