@astryxdesign/core 0.1.9-canary.d2d84f8 → 0.1.9-canary.e00a241

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 (414) hide show
  1. package/dist/Avatar/Avatar.d.ts +6 -1
  2. package/dist/Avatar/Avatar.d.ts.map +1 -1
  3. package/dist/Avatar/Avatar.js +46 -12
  4. package/dist/Avatar/AvatarStatusDot.d.ts +6 -4
  5. package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  7. package/dist/Breadcrumbs/BreadcrumbItem.js +11 -4
  8. package/dist/Breadcrumbs/index.d.ts +1 -0
  9. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  10. package/dist/Breadcrumbs/index.js +2 -1
  11. package/dist/Calendar/Calendar.d.ts.map +1 -1
  12. package/dist/Calendar/Calendar.js +50 -9
  13. package/dist/Calendar/dayCellUtils.d.ts +4 -4
  14. package/dist/Calendar/dayCellUtils.js +2 -2
  15. package/dist/Calendar/styles.d.ts +30 -31
  16. package/dist/Calendar/styles.d.ts.map +1 -1
  17. package/dist/Calendar/styles.js +30 -31
  18. package/dist/Card/Card.js +1 -1
  19. package/dist/Carousel/Carousel.d.ts.map +1 -1
  20. package/dist/Carousel/Carousel.js +22 -12
  21. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  22. package/dist/Chat/ChatDictationButton.js +3 -1
  23. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  24. package/dist/Chat/ChatMessageBubble.js +8 -8
  25. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  26. package/dist/Chat/ChatToolCalls.js +10 -3
  27. package/dist/CheckboxInput/CheckboxInput.js +4 -4
  28. package/dist/Citation/Citation.d.ts +22 -1
  29. package/dist/Citation/Citation.d.ts.map +1 -1
  30. package/dist/Citation/Citation.js +20 -6
  31. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  32. package/dist/CodeBlock/CodeBlock.js +13 -7
  33. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  34. package/dist/Collapsible/Collapsible.js +3 -1
  35. package/dist/CommandPalette/CommandPalette.d.ts +1 -1
  36. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  37. package/dist/CommandPalette/CommandPalette.js +42 -3
  38. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  39. package/dist/ContextMenu/ContextMenu.js +14 -5
  40. package/dist/ContextMenu/index.d.ts +1 -0
  41. package/dist/ContextMenu/index.d.ts.map +1 -1
  42. package/dist/ContextMenu/index.js +3 -1
  43. package/dist/DateInput/DateInput.d.ts +1 -0
  44. package/dist/DateInput/DateInput.d.ts.map +1 -1
  45. package/dist/DateInput/DateInput.js +31 -19
  46. package/dist/DateRangeInput/DateRangeInput.d.ts +1 -0
  47. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  48. package/dist/DateRangeInput/DateRangeInput.js +20 -19
  49. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  50. package/dist/DateTimeInput/DateTimeInput.js +23 -18
  51. package/dist/Divider/Divider.d.ts +2 -2
  52. package/dist/Divider/Divider.d.ts.map +1 -1
  53. package/dist/Divider/Divider.js +13 -0
  54. package/dist/DropdownMenu/DropdownMenu.d.ts +6 -0
  55. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  56. package/dist/DropdownMenu/DropdownMenu.js +22 -5
  57. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  58. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +4 -1
  59. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  60. package/dist/DropdownMenu/DropdownMenuItem.js +4 -1
  61. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  62. package/dist/DropdownMenu/DropdownMenuRadioItem.js +4 -1
  63. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +104 -0
  64. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -0
  65. package/dist/DropdownMenu/DropdownMenuSubMenu.js +427 -0
  66. package/dist/DropdownMenu/index.d.ts +10 -0
  67. package/dist/DropdownMenu/index.d.ts.map +1 -1
  68. package/dist/DropdownMenu/index.js +13 -0
  69. package/dist/DropdownMenu/menuItemHover.d.ts +23 -0
  70. package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -0
  71. package/dist/DropdownMenu/menuItemHover.js +33 -0
  72. package/dist/DropdownMenu/menuItemRoles.d.ts +8 -0
  73. package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -1
  74. package/dist/DropdownMenu/menuItemRoles.js +10 -1
  75. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  76. package/dist/DropdownMenu/renderDropdownItems.js +21 -6
  77. package/dist/Field/Field.d.ts +3 -1
  78. package/dist/Field/Field.d.ts.map +1 -1
  79. package/dist/Field/Field.js +4 -1
  80. package/dist/Field/inputStyles.stylex.d.ts +1 -0
  81. package/dist/Field/inputStyles.stylex.d.ts.map +1 -1
  82. package/dist/Field/inputStyles.stylex.js +1 -0
  83. package/dist/FieldStatus/FieldStatus.d.ts +4 -1
  84. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  85. package/dist/FieldStatus/FieldStatus.js +4 -2
  86. package/dist/FileInput/FileInput.d.ts +1 -0
  87. package/dist/FileInput/FileInput.d.ts.map +1 -1
  88. package/dist/FileInput/FileInput.js +91 -65
  89. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  90. package/dist/HoverCard/useHoverCard.js +10 -4
  91. package/dist/Icon/Icon.d.ts +15 -1
  92. package/dist/Icon/Icon.d.ts.map +1 -1
  93. package/dist/Icon/Icon.js +19 -3
  94. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  95. package/dist/Lightbox/Lightbox.js +15 -9
  96. package/dist/List/List.d.ts.map +1 -1
  97. package/dist/List/List.js +6 -3
  98. package/dist/Markdown/Markdown.d.ts.map +1 -1
  99. package/dist/Markdown/Markdown.js +7 -0
  100. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  101. package/dist/MultiSelector/MultiSelector.js +65 -40
  102. package/dist/NumberInput/NumberInput.d.ts +1 -0
  103. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  104. package/dist/NumberInput/NumberInput.js +20 -17
  105. package/dist/Outline/Outline.js +2 -2
  106. package/dist/RadioList/RadioList.d.ts +2 -2
  107. package/dist/RadioList/RadioListItem.js +4 -4
  108. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  109. package/dist/Resizable/ResizeHandle.js +49 -19
  110. package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -1
  111. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  112. package/dist/SegmentedControl/SegmentedControlItem.js +2 -1
  113. package/dist/Selector/Selector.d.ts.map +1 -1
  114. package/dist/Selector/Selector.js +58 -16
  115. package/dist/SideNav/SideNav.d.ts.map +1 -1
  116. package/dist/SideNav/SideNav.js +5 -5
  117. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  118. package/dist/SideNav/SideNavCollapseButton.js +13 -10
  119. package/dist/SideNav/SideNavItem.d.ts +1 -1
  120. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  121. package/dist/SideNav/SideNavItem.js +12 -10
  122. package/dist/Slider/Slider.d.ts.map +1 -1
  123. package/dist/Slider/Slider.js +87 -34
  124. package/dist/Spinner/Spinner.d.ts +3 -2
  125. package/dist/Spinner/Spinner.d.ts.map +1 -1
  126. package/dist/Spinner/Spinner.js +11 -2
  127. package/dist/Switch/Switch.d.ts +8 -1
  128. package/dist/Switch/Switch.d.ts.map +1 -1
  129. package/dist/Switch/Switch.js +160 -91
  130. package/dist/TabList/TabList.d.ts +2 -11
  131. package/dist/TabList/TabList.d.ts.map +1 -1
  132. package/dist/TabList/TabList.js +5 -9
  133. package/dist/TabList/TabListContext.d.ts +0 -5
  134. package/dist/TabList/TabListContext.d.ts.map +1 -1
  135. package/dist/TabList/TabListContext.js +0 -5
  136. package/dist/Table/BaseTable.d.ts.map +1 -1
  137. package/dist/Table/BaseTable.js +30 -4
  138. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  139. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +15 -11
  140. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  141. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +36 -26
  142. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  143. package/dist/Table/plugins/selection/useTableSelection.js +3 -0
  144. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
  145. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +14 -6
  146. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  147. package/dist/Table/plugins/tree/useTableTreeData.js +29 -23
  148. package/dist/Table/types.d.ts +32 -0
  149. package/dist/Table/types.d.ts.map +1 -1
  150. package/dist/TextArea/TextArea.d.ts +1 -0
  151. package/dist/TextArea/TextArea.d.ts.map +1 -1
  152. package/dist/TextArea/TextArea.js +16 -19
  153. package/dist/TextInput/TextInput.d.ts +1 -0
  154. package/dist/TextInput/TextInput.d.ts.map +1 -1
  155. package/dist/TextInput/TextInput.js +18 -17
  156. package/dist/TimeInput/TimeInput.d.ts +1 -0
  157. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  158. package/dist/TimeInput/TimeInput.js +15 -17
  159. package/dist/Timestamp/Timestamp.d.ts +31 -3
  160. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  161. package/dist/Timestamp/Timestamp.js +47 -76
  162. package/dist/Timestamp/formatInstant.d.ts +41 -0
  163. package/dist/Timestamp/formatInstant.d.ts.map +1 -0
  164. package/dist/Timestamp/formatInstant.js +177 -0
  165. package/dist/Timestamp/index.d.ts +1 -0
  166. package/dist/Timestamp/index.d.ts.map +1 -1
  167. package/dist/Timestamp/tooltipEntries.d.ts +56 -0
  168. package/dist/Timestamp/tooltipEntries.d.ts.map +1 -0
  169. package/dist/Timestamp/tooltipEntries.js +163 -0
  170. package/dist/Token/Token.d.ts +30 -3
  171. package/dist/Token/Token.d.ts.map +1 -1
  172. package/dist/Token/Token.js +17 -2
  173. package/dist/Token/index.d.ts +1 -1
  174. package/dist/Token/index.d.ts.map +1 -1
  175. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  176. package/dist/Tokenizer/Tokenizer.js +2 -2
  177. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  178. package/dist/Tooltip/useTooltip.js +17 -1
  179. package/dist/TopNav/TopNavItem.d.ts +3 -1
  180. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  181. package/dist/TopNav/TopNavItem.js +30 -3
  182. package/dist/TreeList/TreeList.d.ts.map +1 -1
  183. package/dist/TreeList/TreeList.js +1 -0
  184. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  185. package/dist/TreeList/TreeListBranches.js +8 -6
  186. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  187. package/dist/TreeList/TreeListItem.js +32 -14
  188. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  189. package/dist/Typeahead/BaseTypeahead.js +19 -10
  190. package/dist/Typeahead/Typeahead.js +1 -1
  191. package/dist/Typeahead/TypeaheadItem.d.ts +1 -1
  192. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  193. package/dist/Typeahead/TypeaheadItem.js +20 -10
  194. package/dist/astryx.css +38 -14
  195. package/dist/astryx.umd.js +52 -52
  196. package/dist/astryx.umd.js.map +4 -4
  197. package/dist/hooks/index.d.ts +2 -0
  198. package/dist/hooks/index.d.ts.map +1 -1
  199. package/dist/hooks/index.js +1 -0
  200. package/dist/hooks/isRtlElement.d.ts +18 -0
  201. package/dist/hooks/isRtlElement.d.ts.map +1 -0
  202. package/dist/hooks/isRtlElement.js +27 -0
  203. package/dist/hooks/useAnnounce.d.ts +3 -0
  204. package/dist/hooks/useAnnounce.d.ts.map +1 -1
  205. package/dist/hooks/useAnnounce.js +39 -1
  206. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  207. package/dist/hooks/useFocusTrap.js +52 -11
  208. package/dist/hooks/useGridFocus.d.ts +6 -1
  209. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  210. package/dist/hooks/useGridFocus.js +12 -9
  211. package/dist/hooks/useHotkeys.d.ts +9 -0
  212. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  213. package/dist/hooks/useHotkeys.js +9 -0
  214. package/dist/hooks/useInputStatusIcon.d.ts +38 -0
  215. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -0
  216. package/dist/hooks/useInputStatusIcon.js +158 -0
  217. package/dist/hooks/useListFocus.d.ts +39 -1
  218. package/dist/hooks/useListFocus.d.ts.map +1 -1
  219. package/dist/hooks/useListFocus.js +54 -9
  220. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  221. package/dist/theme/derivedVarRegistry.js +13 -0
  222. package/dist/utils/index.d.ts +1 -0
  223. package/dist/utils/index.d.ts.map +1 -1
  224. package/dist/utils/index.js +2 -1
  225. package/dist/utils/plainDate.d.ts +17 -0
  226. package/dist/utils/plainDate.d.ts.map +1 -1
  227. package/dist/utils/plainDate.js +23 -4
  228. package/dist/utils/rtlStyles.d.ts +24 -0
  229. package/dist/utils/rtlStyles.d.ts.map +1 -0
  230. package/dist/utils/rtlStyles.js +27 -0
  231. package/locales/en.json +76 -0
  232. package/locales/pseudo.json +57 -0
  233. package/package.json +6 -1
  234. package/src/Avatar/Avatar.doc.mjs +18 -4
  235. package/src/Avatar/Avatar.test.tsx +106 -0
  236. package/src/Avatar/Avatar.tsx +62 -10
  237. package/src/Avatar/AvatarStatusDot.doc.mjs +4 -4
  238. package/src/Avatar/AvatarStatusDot.tsx +6 -4
  239. package/src/AvatarGroup/AvatarGroup.test.tsx +16 -1
  240. package/src/Breadcrumbs/BreadcrumbItem.tsx +10 -10
  241. package/src/Breadcrumbs/Breadcrumbs.test.tsx +51 -0
  242. package/src/Breadcrumbs/index.ts +4 -0
  243. package/src/Button/Button.doc.mjs +0 -4
  244. package/src/Calendar/Calendar.test.tsx +170 -20
  245. package/src/Calendar/Calendar.tsx +73 -16
  246. package/src/Calendar/dayCellUtils.test.ts +18 -18
  247. package/src/Calendar/dayCellUtils.ts +2 -2
  248. package/src/Calendar/styles.ts +33 -39
  249. package/src/Card/Card.tsx +1 -1
  250. package/src/Carousel/Carousel.tsx +25 -6
  251. package/src/Chat/ChatComposer.doc.mjs +2 -2
  252. package/src/Chat/ChatComposerInput.doc.mjs +1 -1
  253. package/src/Chat/ChatDictationButton.tsx +7 -1
  254. package/src/Chat/ChatMessageBubble.tsx +15 -10
  255. package/src/Chat/ChatSystemMessage.test.tsx +5 -0
  256. package/src/Chat/ChatToolCalls.tsx +9 -2
  257. package/src/CheckboxInput/CheckboxInput.doc.mjs +6 -0
  258. package/src/CheckboxInput/CheckboxInput.tsx +4 -4
  259. package/src/CheckboxList/CheckboxList.doc.mjs +6 -0
  260. package/src/Citation/Citation.doc.mjs +4 -3
  261. package/src/Citation/Citation.test.tsx +97 -0
  262. package/src/Citation/Citation.tsx +43 -11
  263. package/src/CodeBlock/CodeBlock.tsx +35 -8
  264. package/src/Collapsible/Collapsible.doc.mjs +1 -0
  265. package/src/Collapsible/Collapsible.test.tsx +7 -0
  266. package/src/Collapsible/Collapsible.tsx +16 -6
  267. package/src/CommandPalette/CommandPalette.test.tsx +95 -1
  268. package/src/CommandPalette/CommandPalette.tsx +41 -2
  269. package/src/ContextMenu/ContextMenu.test.tsx +42 -0
  270. package/src/ContextMenu/ContextMenu.tsx +13 -11
  271. package/src/ContextMenu/index.ts +5 -0
  272. package/src/DateInput/DateInput.doc.mjs +15 -5
  273. package/src/DateInput/DateInput.test.tsx +210 -3
  274. package/src/DateInput/DateInput.tsx +41 -29
  275. package/src/DateRangeInput/DateRangeInput.doc.mjs +11 -3
  276. package/src/DateRangeInput/DateRangeInput.test.tsx +120 -3
  277. package/src/DateRangeInput/DateRangeInput.tsx +29 -27
  278. package/src/DateTimeInput/DateTimeInput.doc.mjs +22 -1
  279. package/src/DateTimeInput/DateTimeInput.test.tsx +112 -0
  280. package/src/DateTimeInput/DateTimeInput.tsx +44 -39
  281. package/src/Divider/Divider.test.tsx +32 -8
  282. package/src/Divider/Divider.tsx +14 -1
  283. package/src/DropdownMenu/DropdownMenu.test.tsx +60 -0
  284. package/src/DropdownMenu/DropdownMenu.tsx +31 -12
  285. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +8 -2
  286. package/src/DropdownMenu/DropdownMenuItem.tsx +8 -2
  287. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +8 -2
  288. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +88 -0
  289. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +635 -0
  290. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +553 -0
  291. package/src/DropdownMenu/index.ts +16 -0
  292. package/src/DropdownMenu/menuItemHover.ts +39 -0
  293. package/src/DropdownMenu/menuItemRoles.ts +9 -0
  294. package/src/DropdownMenu/renderDropdownItems.tsx +29 -15
  295. package/src/Field/Field.tsx +14 -9
  296. package/src/Field/inputStyles.stylex.ts +2 -0
  297. package/src/FieldStatus/FieldStatus.tsx +10 -4
  298. package/src/FileInput/FileInput.doc.mjs +13 -7
  299. package/src/FileInput/FileInput.test.tsx +84 -16
  300. package/src/FileInput/FileInput.tsx +96 -79
  301. package/src/HoverCard/useHoverCard.tsx +15 -3
  302. package/src/Icon/Icon.doc.mjs +13 -0
  303. package/src/Icon/Icon.test.tsx +89 -0
  304. package/src/Icon/Icon.tsx +51 -3
  305. package/src/Lightbox/Lightbox.tsx +11 -3
  306. package/src/List/List.test.tsx +17 -29
  307. package/src/List/List.tsx +6 -1
  308. package/src/Markdown/Markdown.test.tsx +21 -0
  309. package/src/Markdown/Markdown.tsx +4 -0
  310. package/src/MultiSelector/MultiSelector.doc.mjs +11 -0
  311. package/src/MultiSelector/MultiSelector.test.tsx +306 -3
  312. package/src/MultiSelector/MultiSelector.tsx +88 -57
  313. package/src/NumberInput/NumberInput.doc.mjs +11 -5
  314. package/src/NumberInput/NumberInput.test.tsx +48 -0
  315. package/src/NumberInput/NumberInput.tsx +25 -26
  316. package/src/Outline/Outline.test.tsx +12 -12
  317. package/src/Outline/Outline.tsx +2 -2
  318. package/src/RadioList/RadioList.doc.mjs +6 -0
  319. package/src/RadioList/RadioList.tsx +2 -2
  320. package/src/RadioList/RadioListItem.tsx +4 -4
  321. package/src/Resizable/ResizeHandle.test.tsx +94 -0
  322. package/src/Resizable/ResizeHandle.tsx +70 -20
  323. package/src/SegmentedControl/SegmentedControlItem.tsx +2 -0
  324. package/src/Selector/Selector.doc.mjs +8 -0
  325. package/src/Selector/Selector.test.tsx +328 -3
  326. package/src/Selector/Selector.tsx +70 -19
  327. package/src/SideNav/SideNav.tsx +1 -8
  328. package/src/SideNav/SideNavCollapseButton.tsx +9 -7
  329. package/src/SideNav/SideNavItem.tsx +5 -3
  330. package/src/Slider/Slider.doc.mjs +6 -0
  331. package/src/Slider/Slider.test.tsx +240 -12
  332. package/src/Slider/Slider.tsx +96 -23
  333. package/src/Spinner/Spinner.test.tsx +10 -8
  334. package/src/Spinner/Spinner.tsx +15 -5
  335. package/src/Switch/Switch.doc.mjs +142 -42
  336. package/src/Switch/Switch.test.tsx +22 -0
  337. package/src/Switch/Switch.tsx +90 -33
  338. package/src/TabList/TabList.doc.mjs +0 -6
  339. package/src/TabList/TabList.test.tsx +3 -15
  340. package/src/TabList/TabList.tsx +6 -17
  341. package/src/TabList/TabListContext.ts +0 -6
  342. package/src/Table/BaseTable.tsx +34 -2
  343. package/src/Table/Table.doc.mjs +11 -0
  344. package/src/Table/Table.test.tsx +85 -0
  345. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +9 -6
  346. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +24 -17
  347. package/src/Table/plugins/selection/useTableSelection.test.tsx +61 -3
  348. package/src/Table/plugins/selection/useTableSelection.tsx +3 -0
  349. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +37 -8
  350. package/src/Table/plugins/tree/useTableTreeData.tsx +17 -13
  351. package/src/Table/types.ts +33 -0
  352. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  353. package/src/TextArea/TextArea.doc.mjs +11 -5
  354. package/src/TextArea/TextArea.tsx +17 -26
  355. package/src/TextInput/TextInput.doc.mjs +11 -5
  356. package/src/TextInput/TextInput.test.tsx +130 -3
  357. package/src/TextInput/TextInput.tsx +18 -25
  358. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  359. package/src/TimeInput/TimeInput.doc.mjs +11 -5
  360. package/src/TimeInput/TimeInput.tsx +14 -25
  361. package/src/Timestamp/Timestamp.doc.mjs +32 -6
  362. package/src/Timestamp/Timestamp.test.tsx +371 -0
  363. package/src/Timestamp/Timestamp.tsx +96 -93
  364. package/src/Timestamp/formatInstant.ts +218 -0
  365. package/src/Timestamp/index.ts +4 -0
  366. package/src/Timestamp/tooltipEntries.test.ts +552 -0
  367. package/src/Timestamp/tooltipEntries.ts +210 -0
  368. package/src/Token/Token.test.tsx +21 -15
  369. package/src/Token/Token.tsx +31 -14
  370. package/src/Token/index.ts +1 -1
  371. package/src/Tokenizer/Tokenizer.doc.mjs +8 -1
  372. package/src/Tokenizer/Tokenizer.tsx +4 -2
  373. package/src/Tooltip/Tooltip.test.tsx +46 -0
  374. package/src/Tooltip/useTooltip.tsx +20 -0
  375. package/src/TopNav/TopNav.test.tsx +93 -2
  376. package/src/TopNav/TopNavItem.doc.mjs +3 -3
  377. package/src/TopNav/TopNavItem.tsx +35 -6
  378. package/src/TopNav/TopNavMegaMenuFeaturedCard.doc.mjs +1 -1
  379. package/src/TreeList/TreeList.doc.mjs +6 -0
  380. package/src/TreeList/TreeList.test.tsx +50 -10
  381. package/src/TreeList/TreeList.tsx +5 -0
  382. package/src/TreeList/TreeListBranches.tsx +8 -6
  383. package/src/TreeList/TreeListItem.tsx +33 -11
  384. package/src/Typeahead/BaseTypeahead.tsx +29 -14
  385. package/src/Typeahead/Typeahead.doc.mjs +6 -0
  386. package/src/Typeahead/Typeahead.test.tsx +27 -0
  387. package/src/Typeahead/Typeahead.tsx +1 -1
  388. package/src/Typeahead/TypeaheadItem.tsx +2 -1
  389. package/src/__tests__/inputWidthContract.test.tsx +62 -0
  390. package/src/doc-mjs.d.ts +19 -0
  391. package/src/hooks/index.ts +6 -0
  392. package/src/hooks/isRtlElement.ts +27 -0
  393. package/src/hooks/useAnnounce.test.tsx +75 -2
  394. package/src/hooks/useAnnounce.ts +47 -1
  395. package/src/hooks/useFocusTrap.test.tsx +104 -0
  396. package/src/hooks/useFocusTrap.ts +61 -12
  397. package/src/hooks/useGridFocus.doc.mjs +3 -3
  398. package/src/hooks/useGridFocus.test.tsx +41 -0
  399. package/src/hooks/useGridFocus.ts +21 -10
  400. package/src/hooks/useHotkeys.ts +9 -0
  401. package/src/hooks/useInputStatusIcon.test.tsx +162 -0
  402. package/src/hooks/useInputStatusIcon.tsx +224 -0
  403. package/src/hooks/useListFocus.doc.mjs +25 -3
  404. package/src/hooks/useListFocus.test.tsx +153 -1
  405. package/src/hooks/useListFocus.ts +102 -9
  406. package/src/reset.css +0 -26
  407. package/src/reset.test.ts +60 -0
  408. package/src/theme/derivedVarRegistry.test.ts +1 -0
  409. package/src/theme/derivedVarRegistry.ts +6 -0
  410. package/src/theme/generateThemeRules.test.ts +42 -0
  411. package/src/utils/index.ts +2 -0
  412. package/src/utils/plainDate.test.ts +24 -6
  413. package/src/utils/plainDate.ts +18 -4
  414. package/src/utils/rtlStyles.ts +27 -0
@@ -545,6 +545,21 @@ function DefaultOption({option}: {option: SelectorOptionData}) {
545
545
  );
546
546
  }
547
547
 
548
+ // Case-insensitive substring match for a single option. The one predicate used
549
+ // by both the flat filter (count + keyboard nav) and the grouped renderer, so
550
+ // what is shown while searching stays in lockstep with the announced count.
551
+ function optionMatchesQuery(
552
+ option: SelectorOptionData,
553
+ query: string,
554
+ ): boolean {
555
+ if (!query) {
556
+ return true;
557
+ }
558
+ return (option.label ?? option.value)
559
+ .toLowerCase()
560
+ .includes(query.toLowerCase());
561
+ }
562
+
548
563
  // Case-insensitive substring filter over the selectable options. Shared by the
549
564
  // `filteredItems` memo (rendering) and the search-change handler, which needs
550
565
  // the count for the *next* query synchronously to announce it exactly once per
@@ -556,10 +571,7 @@ function filterOptionsByQuery(
556
571
  if (!query) {
557
572
  return items;
558
573
  }
559
- const q = query.toLowerCase();
560
- return items.filter(item =>
561
- (item.label ?? item.value).toLowerCase().includes(q),
562
- );
574
+ return items.filter(item => optionMatchesQuery(item, query));
563
575
  }
564
576
 
565
577
  /**
@@ -947,16 +959,15 @@ export function Selector<T extends SelectorOptionType>(
947
959
 
948
960
  // Render all options (handling sections/dividers)
949
961
  const renderOptions = useCallback(() => {
950
- // When search is active, render filtered items flat (no sections/dividers)
951
- if (hasSearch && searchQuery) {
952
- if (filteredItems.length === 0) {
953
- return [
954
- <div key="empty" {...stylex.props(styles.emptyState)}>
955
- No results found
956
- </div>,
957
- ];
958
- }
959
- return filteredItems.map((item, index) => renderItem(item, index));
962
+ const isSearching = hasSearch && Boolean(searchQuery);
963
+
964
+ // Nothing matched across every group/option: show the empty state.
965
+ if (isSearching && filteredItems.length === 0) {
966
+ return [
967
+ <div key="empty" {...stylex.props(styles.emptyState)}>
968
+ No results found
969
+ </div>,
970
+ ];
960
971
  }
961
972
 
962
973
  let flatIndex = 0;
@@ -966,13 +977,27 @@ export function Selector<T extends SelectorOptionType>(
966
977
  const option = options[i];
967
978
 
968
979
  if (isDivider(option)) {
980
+ // While searching, a standalone divider between groups would orphan
981
+ // itself once its neighbors are filtered out, so skip it.
982
+ if (isSearching) {
983
+ continue;
984
+ }
969
985
  elements.push(<Divider key={`divider-${i}`} xstyle={styles.divider} />);
970
986
  } else if (isSection(option)) {
971
987
  const sectionItems: ReactNode[] = [];
972
988
  for (const opt of option.options) {
973
- sectionItems.push(renderItem(normalizeOption(opt), flatIndex));
989
+ const normalized = normalizeOption(opt);
990
+ if (isSearching && !optionMatchesQuery(normalized, searchQuery)) {
991
+ continue;
992
+ }
993
+ sectionItems.push(renderItem(normalized, flatIndex));
974
994
  flatIndex++;
975
995
  }
996
+ // Hide a group entirely (header + wrapper) when none of its items
997
+ // match the query, so no header is left standing over nothing.
998
+ if (sectionItems.length === 0) {
999
+ continue;
1000
+ }
976
1001
  if (option.title) {
977
1002
  elements.push(
978
1003
  <Divider
@@ -988,7 +1013,11 @@ export function Selector<T extends SelectorOptionType>(
988
1013
  </div>,
989
1014
  );
990
1015
  } else if (isOptionData(option)) {
991
- elements.push(renderItem(normalizeOption(option), flatIndex));
1016
+ const normalized = normalizeOption(option);
1017
+ if (isSearching && !optionMatchesQuery(normalized, searchQuery)) {
1018
+ continue;
1019
+ }
1020
+ elements.push(renderItem(normalized, flatIndex));
992
1021
  flatIndex++;
993
1022
  }
994
1023
  }
@@ -1017,7 +1046,7 @@ export function Selector<T extends SelectorOptionType>(
1017
1046
  isDisabled && inputWrapperStyles.disabled,
1018
1047
  !selectedItem && styles.triggerPlaceholder,
1019
1048
  status && inputStatusBorderStyles[status.type],
1020
- status && inputStatusHoverShadowStyles[status.type],
1049
+ status && !isDisabled && inputStatusHoverShadowStyles[status.type],
1021
1050
  inputGroup && groupStyles.inGroup,
1022
1051
  xstyle,
1023
1052
  ),
@@ -1080,7 +1109,17 @@ export function Selector<T extends SelectorOptionType>(
1080
1109
  onClick={handleClear}
1081
1110
  aria-label={t('@astryx.selector.clearLabel', {label})}
1082
1111
  {...stylex.props(styles.clearButton)}>
1083
- <Icon icon="close" size="sm" color="secondary" />
1112
+ <Icon
1113
+ icon="close"
1114
+ size="sm"
1115
+ color="secondary"
1116
+ // Stable theme target on the clear glyph itself, so a theme can
1117
+ // restyle just this icon (color, size, hover) via `defineTheme`.
1118
+ // Same-element rules in @layer astryx-theme win over the icon's
1119
+ // own base color/size, which a button-level target could not
1120
+ // reach.
1121
+ {...themeProps('selector-clear-icon')}
1122
+ />
1084
1123
  </button>
1085
1124
  )}
1086
1125
  <span
@@ -1096,7 +1135,19 @@ export function Selector<T extends SelectorOptionType>(
1096
1135
  color={STATUS_ICON_COLOR_MAP[status.type]}
1097
1136
  />
1098
1137
  ) : (
1099
- <Icon icon="chevronDown" size="sm" color="inherit" />
1138
+ <Icon
1139
+ icon="chevronDown"
1140
+ size="sm"
1141
+ color="inherit"
1142
+ // Stable theme target on the chevron glyph itself, so a theme can
1143
+ // restyle just this icon (color, size, hover) — and its
1144
+ // open/closed state — via `defineTheme`. Same-element rules in
1145
+ // @layer astryx-theme win over the icon's own base color/size,
1146
+ // which a button-level target could not reach.
1147
+ {...themeProps('selector-indicator-icon', {
1148
+ state: popover.isOpen ? 'expanded' : 'collapsed',
1149
+ })}
1150
+ />
1100
1151
  )}
1101
1152
  </span>
1102
1153
  </div>
@@ -31,7 +31,7 @@ import {
31
31
  import type {BaseProps} from '../BaseProps';
32
32
  import * as stylex from '@stylexjs/stylex';
33
33
  import type {StyleXStyles} from '@stylexjs/stylex';
34
- import {borderVars, colorVars, spacingVars} from '../theme/tokens.stylex';
34
+ import {spacingVars} from '../theme/tokens.stylex';
35
35
  import {mergeProps, mergeRefs} from '../utils';
36
36
  import {
37
37
  SideNavCollapseContext,
@@ -123,9 +123,6 @@ const styles = stylex.create({
123
123
  paddingInline: spacingVars['--spacing-2'],
124
124
  paddingBlockStart: spacingVars['--spacing-1'],
125
125
  paddingBlockEnd: spacingVars['--spacing-2'],
126
- borderBlockStartWidth: borderVars['--border-width'],
127
- borderBlockStartStyle: 'solid',
128
- borderBlockStartColor: colorVars['--color-border'],
129
126
  },
130
127
  footerRow: {
131
128
  display: 'flex',
@@ -145,7 +142,6 @@ const styles = stylex.create({
145
142
  alignItems: 'center',
146
143
  },
147
144
  stickyBottomCollapsed: {
148
- borderBlockStart: 'none',
149
145
  paddingBlockStart: 0,
150
146
  },
151
147
  // Drawer footer — pushed to bottom of the scrollable content area
@@ -155,9 +151,6 @@ const styles = stylex.create({
155
151
  marginBlockStart: 'auto',
156
152
  gap: spacingVars['--spacing-2'],
157
153
  paddingBlockStart: spacingVars['--spacing-2'],
158
- borderBlockStartWidth: borderVars['--border-width'],
159
- borderBlockStartStyle: 'solid',
160
- borderBlockStartColor: colorVars['--color-border'],
161
154
  },
162
155
  drawerFooterIcons: {
163
156
  display: 'flex',
@@ -23,7 +23,7 @@ import {durationVars, easeVars} from '../theme/tokens.stylex';
23
23
  import {getIcon} from '../Icon/globalIconRegistry';
24
24
  import {Button} from '../Button';
25
25
  import type {BaseProps} from '../BaseProps';
26
- import {composeEventHandlers} from '../utils';
26
+ import {composeEventHandlers, rtlStyles} from '../utils';
27
27
  import {
28
28
  useSideNavCollapse,
29
29
  type SideNavCollapseState,
@@ -131,12 +131,14 @@ export function SideNavCollapseButton({
131
131
  onClick={composeEventHandlers(onClickProp, toggle)}
132
132
  icon={
133
133
  children ?? (
134
- <span
135
- {...stylex.props(
136
- styles.chevron,
137
- isCollapsed && styles.chevronCollapsed,
138
- )}>
139
- {getIcon('chevronLeft')}
134
+ <span {...stylex.props(rtlStyles.mirror)}>
135
+ <span
136
+ {...stylex.props(
137
+ styles.chevron,
138
+ isCollapsed && styles.chevronCollapsed,
139
+ )}>
140
+ {getIcon('chevronLeft')}
141
+ </span>
140
142
  </span>
141
143
  )
142
144
  }
@@ -356,6 +356,8 @@ export function SideNavItem({
356
356
  size = 'md',
357
357
  'data-testid': testId,
358
358
  ref,
359
+ xstyle,
360
+ ...rest
359
361
  }: SideNavItemProps) {
360
362
  const t = useTranslator();
361
363
  const {isCollapsed} = useSideNavCollapse();
@@ -502,7 +504,7 @@ export function SideNavItem({
502
504
  // Items with children: popover trigger + popover
503
505
  if (hasChildren) {
504
506
  return (
505
- <div {...stylex.props(styles.root)}>
507
+ <div {...stylex.props(styles.root, xstyle)}>
506
508
  <button
507
509
  ref={mergeRefs(ref, popover.triggerRef)}
508
510
  type="button"
@@ -554,7 +556,7 @@ export function SideNavItem({
554
556
  );
555
557
 
556
558
  return (
557
- <div ref={itemRef} {...stylex.props(styles.root)}>
559
+ <div ref={itemRef} {...stylex.props(styles.root, xstyle)}>
558
560
  {collapsedElement}
559
561
  <Tooltip content={label} placement="end" anchorRef={itemRef} />
560
562
  </div>
@@ -670,7 +672,7 @@ export function SideNavItem({
670
672
  }
671
673
 
672
674
  const item = (
673
- <div ref={itemRef} {...stylex.props(styles.root)}>
675
+ <div ref={itemRef} {...stylex.props(styles.root, xstyle)}>
674
676
  {itemElement}
675
677
  {hasChildren && !isCollapsed && (
676
678
  <div
@@ -141,6 +141,12 @@ export const docs = {
141
141
  type: 'string',
142
142
  description: 'Tooltip text for an info icon displayed next to the label.',
143
143
  },
144
+ {
145
+ name: 'width',
146
+ type: 'SizeValue',
147
+ description:
148
+ 'Width of the field (number = pixels, string used as-is, e.g. "100%"). Sizes the whole field (label, control, and status) so they stay aligned.',
149
+ },
144
150
  {
145
151
  name: 'xstyle',
146
152
  type: 'StyleXStyles',
@@ -9,6 +9,7 @@
9
9
  * SYNC: When Slider.tsx changes, update tests to match new behavior
10
10
  */
11
11
 
12
+ import {useState} from 'react';
12
13
  import {describe, it, expect, vi, beforeEach} from 'vitest';
13
14
  import {render, screen, act, fireEvent, waitFor} from '@testing-library/react';
14
15
  import userEvent from '@testing-library/user-event';
@@ -50,23 +51,17 @@ beforeEach(() => {
50
51
  describe('Slider', () => {
51
52
  // --- Aria labels ---
52
53
 
53
- it('single thumb uses label as aria-label', () => {
54
+ it('single thumb takes its accessible name from the label', () => {
54
55
  render(<Slider label="Volume" value={50} />);
55
56
  const slider = screen.getByRole('slider');
56
- expect(slider).toHaveAttribute('aria-label', 'Volume');
57
+ expect(slider).toHaveAccessibleName('Volume');
57
58
  });
58
59
 
59
- it('range thumbs have correct aria-labels', () => {
60
+ it('range thumbs have individual names composing with the group label', () => {
60
61
  render(<Slider label="Price range" value={[20, 80] as [number, number]} />);
61
62
  const sliders = screen.getAllByRole('slider');
62
- expect(sliders[0]).toHaveAttribute(
63
- 'aria-label',
64
- 'Price range, minimum value',
65
- );
66
- expect(sliders[1]).toHaveAttribute(
67
- 'aria-label',
68
- 'Price range, maximum value',
69
- );
63
+ expect(sliders[0]).toHaveAccessibleName('Minimum value');
64
+ expect(sliders[1]).toHaveAccessibleName('Maximum value');
70
65
  });
71
66
 
72
67
  it('sets aria-valuetext with formatValue', () => {
@@ -94,7 +89,9 @@ describe('Slider', () => {
94
89
  render(<Slider label="Volume" value={value} min={0} max={100} />);
95
90
  const slider = screen.getByRole('slider');
96
91
  expect(slider).toHaveAttribute('aria-valuenow', String(expectedValue));
97
- expect(slider).toHaveStyle({left: expectedPosition});
92
+ // Thumb positions via the logical `inset-inline-start` so it mirrors
93
+ // under RTL (see RTL Phase 4). In LTR this resolves to the left edge.
94
+ expect(slider).toHaveStyle({insetInlineStart: expectedPosition});
98
95
  },
99
96
  );
100
97
 
@@ -110,6 +107,102 @@ describe('Slider', () => {
110
107
  const sliders = screen.getAllByRole('slider');
111
108
  expect(sliders[0]).toHaveAttribute('aria-valuenow', '25');
112
109
  expect(sliders[1]).toHaveAttribute('aria-valuenow', '75');
110
+ // Per the APG multi-thumb pattern, each thumb's bounds are constrained by
111
+ // its sibling: the lower thumb can't exceed the upper thumb's value and
112
+ // the upper thumb can't go below the lower thumb's value.
113
+ expect(sliders[0]).toHaveAttribute('aria-valuemin', '0');
114
+ expect(sliders[0]).toHaveAttribute('aria-valuemax', '75');
115
+ expect(sliders[1]).toHaveAttribute('aria-valuemin', '25');
116
+ expect(sliders[1]).toHaveAttribute('aria-valuemax', '100');
117
+ });
118
+
119
+ it('range thumb bounds update after moving a thumb', async () => {
120
+ const user = userEvent.setup();
121
+ function ControlledRange() {
122
+ const [value, setValue] = useState<[number, number]>([25, 75]);
123
+ return (
124
+ <Slider
125
+ label="Range"
126
+ value={value}
127
+ onChange={setValue}
128
+ min={0}
129
+ max={100}
130
+ />
131
+ );
132
+ }
133
+ render(<ControlledRange />);
134
+ const sliders = screen.getAllByRole('slider');
135
+ act(() => {
136
+ sliders[0].focus();
137
+ });
138
+ await user.keyboard('{ArrowRight}');
139
+ expect(sliders[0]).toHaveAttribute('aria-valuenow', '26');
140
+ // The upper thumb's floor tracks the lower thumb's new value.
141
+ expect(sliders[1]).toHaveAttribute('aria-valuemin', '26');
142
+ expect(sliders[0]).toHaveAttribute('aria-valuemax', '75');
143
+ });
144
+
145
+ it('range thumb bounds include the minStepsBetweenThumbs gap', () => {
146
+ render(
147
+ <Slider
148
+ label="Range"
149
+ value={[20, 80] as [number, number]}
150
+ min={0}
151
+ max={100}
152
+ step={5}
153
+ minStepsBetweenThumbs={2}
154
+ />,
155
+ );
156
+ const sliders = screen.getAllByRole('slider');
157
+ // minGap = 2 steps * 5 = 10
158
+ expect(sliders[0]).toHaveAttribute('aria-valuemax', '70');
159
+ expect(sliders[1]).toHaveAttribute('aria-valuemin', '30');
160
+ });
161
+
162
+ // --- Label association ---
163
+
164
+ // A <label htmlFor> must point at an existing form-associated element;
165
+ // div[role="slider"] is not labelable, so the Slider label must render as a
166
+ // group label instead (WCAG 1.3.1).
167
+ const LABELABLE_TAGS = new Set([
168
+ 'BUTTON',
169
+ 'INPUT',
170
+ 'METER',
171
+ 'OUTPUT',
172
+ 'PROGRESS',
173
+ 'SELECT',
174
+ 'TEXTAREA',
175
+ ]);
176
+
177
+ function expectNoOrphanedLabels(container: HTMLElement) {
178
+ for (const labelEl of container.querySelectorAll('label[for]')) {
179
+ const target = document.getElementById(labelEl.getAttribute('for')!);
180
+ expect(target).not.toBeNull();
181
+ expect(LABELABLE_TAGS.has(target!.tagName)).toBe(true);
182
+ }
183
+ }
184
+
185
+ it('single mode renders the label as a group label naming the thumb', () => {
186
+ const {container} = render(<Slider label="Volume" value={50} />);
187
+ expectNoOrphanedLabels(container);
188
+ const slider = screen.getByRole('slider');
189
+ const labelledBy = slider.getAttribute('aria-labelledby');
190
+ expect(labelledBy).toBeTruthy();
191
+ expect(document.getElementById(labelledBy!)).toHaveTextContent('Volume');
192
+ expect(slider).toHaveAccessibleName('Volume');
193
+ });
194
+
195
+ it('range mode labels the slider group via aria-labelledby', () => {
196
+ const {container} = render(
197
+ <Slider label="Price range" value={[20, 80] as [number, number]} />,
198
+ );
199
+ expectNoOrphanedLabels(container);
200
+ const group = screen.getByRole('group', {name: 'Price range'});
201
+ const labelledBy = group.getAttribute('aria-labelledby');
202
+ expect(labelledBy).toBeTruthy();
203
+ expect(document.getElementById(labelledBy!)).toHaveTextContent(
204
+ 'Price range',
205
+ );
113
206
  });
114
207
 
115
208
  it('sets aria-orientation for vertical', () => {
@@ -301,6 +394,58 @@ describe('Slider', () => {
301
394
  expect(handleChangeEnd).toHaveBeenCalledWith(55);
302
395
  });
303
396
 
397
+ // --- Fractional step precision ---
398
+
399
+ it('emits exact decimal values for fractional steps on keyboard', async () => {
400
+ const user = userEvent.setup();
401
+ const handleChange = vi.fn();
402
+ const handleChangeEnd = vi.fn();
403
+ render(
404
+ <Slider
405
+ label="Opacity"
406
+ value={0.2}
407
+ min={0}
408
+ max={1}
409
+ step={0.1}
410
+ onChange={handleChange}
411
+ onChangeEnd={handleChangeEnd}
412
+ />,
413
+ );
414
+ const slider = screen.getByRole('slider');
415
+ act(() => {
416
+ slider.focus();
417
+ });
418
+ await user.keyboard('{ArrowRight}');
419
+ // 0.2 + 0.1 must not surface binary float error (0.30000000000000004)
420
+ expect(handleChange).toHaveBeenCalledWith(0.3);
421
+ expect(handleChangeEnd).toHaveBeenCalledWith(0.3);
422
+ });
423
+
424
+ it('emits exact decimal values for fractional steps in range mode', async () => {
425
+ const user = userEvent.setup();
426
+ const handleChange = vi.fn();
427
+ const handleChangeEnd = vi.fn();
428
+ render(
429
+ <Slider
430
+ label="Range"
431
+ value={[0.2, 0.6] as [number, number]}
432
+ min={0}
433
+ max={1}
434
+ step={0.1}
435
+ onChange={handleChange}
436
+ onChangeEnd={handleChangeEnd}
437
+ />,
438
+ );
439
+ const sliders = screen.getAllByRole('slider');
440
+ act(() => {
441
+ sliders[1].focus();
442
+ });
443
+ await user.keyboard('{ArrowRight}');
444
+ // 0.6 + 0.1 snaps to 7 * 0.1, which is 0.7000000000000001 without rounding
445
+ expect(handleChange).toHaveBeenCalledWith([0.2, 0.7]);
446
+ expect(handleChangeEnd).toHaveBeenCalledWith([0.2, 0.7]);
447
+ });
448
+
304
449
  it('fires onChangeEnd on keyboard Home/End with correct value', async () => {
305
450
  const user = userEvent.setup();
306
451
  const handleChangeEnd = vi.fn();
@@ -416,6 +561,89 @@ describe('Slider', () => {
416
561
  expect(document.activeElement).toBe(slider);
417
562
  });
418
563
 
564
+ // --- RTL pointer value mapping (RTL Phase 4) ---
565
+ // Contract: the inline-start edge (value = min) is the RIGHT edge under RTL,
566
+ // so the same physical click maps to the mirrored value relative to LTR.
567
+ // `valueDisplay="none"` keeps the thumb a direct child of the track element
568
+ // (no Tooltip wrapper) so the mocked rect lands on trackRef.
569
+
570
+ it('maps a track click to the LTR value in the default direction', () => {
571
+ const handleChange = vi.fn();
572
+ render(
573
+ <Slider
574
+ label="Volume"
575
+ value={0}
576
+ min={0}
577
+ max={100}
578
+ valueDisplay="none"
579
+ onChange={handleChange}
580
+ />,
581
+ );
582
+ const track = screen.getByRole('slider').parentElement!;
583
+ track.getBoundingClientRect = () =>
584
+ ({
585
+ left: 0,
586
+ top: 0,
587
+ right: 200,
588
+ bottom: 20,
589
+ width: 200,
590
+ height: 20,
591
+ x: 0,
592
+ y: 0,
593
+ toJSON: () => {},
594
+ });
595
+
596
+ // Click at 25% of the track from the left (x=50 of 200) → value 25 in LTR.
597
+ fireEvent.pointerDown(track, {clientX: 50, clientY: 10, pointerId: 1});
598
+ expect(handleChange).toHaveBeenLastCalledWith(25);
599
+ });
600
+
601
+ it('mirrors a track click to the RTL value when the track is rtl', () => {
602
+ const handleChange = vi.fn();
603
+ render(
604
+ <Slider
605
+ label="Volume"
606
+ value={0}
607
+ min={0}
608
+ max={100}
609
+ valueDisplay="none"
610
+ onChange={handleChange}
611
+ />,
612
+ );
613
+ const track = screen.getByRole('slider').parentElement!;
614
+ track.getBoundingClientRect = () =>
615
+ ({
616
+ left: 0,
617
+ top: 0,
618
+ right: 200,
619
+ bottom: 20,
620
+ width: 200,
621
+ height: 20,
622
+ x: 0,
623
+ y: 0,
624
+ toJSON: () => {},
625
+ });
626
+
627
+ // Force the track's computed direction to rtl (isRtlElement reads this).
628
+ const realGetComputedStyle = window.getComputedStyle;
629
+ const gcsSpy = vi
630
+ .spyOn(window, 'getComputedStyle')
631
+ .mockImplementation((el: Element, pseudo?: string | null) => {
632
+ if (el === track) {
633
+ return {direction: 'rtl'} as CSSStyleDeclaration;
634
+ }
635
+ return realGetComputedStyle(el, pseudo ?? undefined);
636
+ });
637
+
638
+ // Same physical click at 25% from the left (x=50). Under RTL the inline
639
+ // start is the right edge, so fraction = (right - x)/width = 150/200 = 0.75
640
+ // → value 75 (the mirror of the LTR value 25).
641
+ fireEvent.pointerDown(track, {clientX: 50, clientY: 10, pointerId: 1});
642
+ expect(handleChange).toHaveBeenLastCalledWith(75);
643
+
644
+ gcsSpy.mockRestore();
645
+ });
646
+
419
647
  // --- Mark label click snapping ---
420
648
 
421
649
  it('clicking a mark label snaps to that mark value, not pointer position', () => {