@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
@@ -7,13 +7,13 @@
7
7
  * @position Test file; validates selection behavior (checkboxes, aria, select-all)
8
8
  */
9
9
 
10
- import {describe, it, expect} from 'vitest';
10
+ import {describe, it, expect, vi} from 'vitest';
11
11
  import {useState} from 'react';
12
- import {render, screen, within} from '@testing-library/react';
12
+ import {act, render, renderHook, screen, within} from '@testing-library/react';
13
13
  import userEvent from '@testing-library/user-event';
14
14
  import {Table} from '../../Table';
15
15
  import {useTableSelection} from './useTableSelection';
16
- import type {TableColumn} from '../../types';
16
+ import type {BodyRowRenderProps, TableColumn} from '../../types';
17
17
 
18
18
  // =============================================================================
19
19
  // Test Data
@@ -210,4 +210,62 @@ describe('useTableSelection', () => {
210
210
  const headers = within(headerRow).getAllByRole('columnheader');
211
211
  expect(headers).toHaveLength(3);
212
212
  });
213
+
214
+ it('unsubscribes detached row refs instead of accumulating listeners', () => {
215
+ const getIsItemSelected = vi.fn(() => false);
216
+ const {result, rerender} = renderHook(() =>
217
+ useTableSelection<SelectableUser>({
218
+ getIsItemSelected,
219
+ onSelectItem: vi.fn(),
220
+ onSelectAll: vi.fn(),
221
+ getIsAllSelected: () => false,
222
+ }),
223
+ );
224
+
225
+ const initialProps: BodyRowRenderProps = {
226
+ htmlProps: {},
227
+ xstyle: [],
228
+ children: null,
229
+ };
230
+ const row = document.createElement('tr');
231
+ document.body.append(row);
232
+
233
+ let detach: (() => void) | undefined;
234
+
235
+ // Simulate React replacing the callback ref as this row re-renders.
236
+ // Each detach must unsubscribe the previous ref before the next attaches.
237
+ for (let i = 0; i < 3; i++) {
238
+ act(() => {
239
+ detach?.();
240
+ });
241
+
242
+ const transformed = result.current.transformBodyRow?.(
243
+ initialProps,
244
+ selectableUsers[0],
245
+ 0,
246
+ );
247
+ expect(transformed?.ref).toBeTypeOf('function');
248
+
249
+ act(() => {
250
+ const cleanup = (
251
+ transformed?.ref as React.RefCallback<HTMLTableRowElement>
252
+ )(row);
253
+ expect(cleanup).toBeTypeOf('function');
254
+ detach = cleanup as () => void;
255
+ });
256
+ }
257
+
258
+ getIsItemSelected.mockClear();
259
+ rerender();
260
+ expect(getIsItemSelected).toHaveBeenCalledTimes(1);
261
+
262
+ act(() => {
263
+ detach?.();
264
+ });
265
+ getIsItemSelected.mockClear();
266
+ rerender();
267
+ expect(getIsItemSelected).not.toHaveBeenCalled();
268
+
269
+ row.remove();
270
+ });
213
271
  });
@@ -371,6 +371,9 @@ export function useTableSelection<T extends Record<string, unknown>>(
371
371
  store.getConfig().getIsItemSelected(item),
372
372
  );
373
373
  });
374
+ return () => {
375
+ unsub();
376
+ };
374
377
  };
375
378
 
376
379
  return {
@@ -213,7 +213,12 @@ const SHADOW_WIDTH = '6px';
213
213
  // --color-shadow is only ~10% alpha (too faint here); use a slightly stronger soft tint.
214
214
  const SHADOW_TINT = 'light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.32))';
215
215
  const shadowStyles = stylex.create({
216
- // start-pinned: shadow falls to the right (inline-end), over scrolled content
216
+ // start-pinned: shadow falls toward inline-end (over scrolled content).
217
+ // insetInlineEnd anchors the strip to the column's inline-end edge; the strip
218
+ // must then be nudged one full width OUTWARD (past that edge) into the
219
+ // scrolled region. Outward is +X under LTR (inline-end = right) but −X under
220
+ // RTL (inline-end = left), and the fade must run from the pinned edge toward
221
+ // content in each direction — hence both transform and gradient flip on dir.
217
222
  start: {
218
223
  '::after': {
219
224
  content: '""',
@@ -222,14 +227,24 @@ const shadowStyles = stylex.create({
222
227
  bottom: 0,
223
228
  insetInlineEnd: 0,
224
229
  width: SHADOW_WIDTH,
225
- transform: 'translateX(100%)',
230
+ transform: {
231
+ default: 'translateX(100%)',
232
+ ':is([dir="rtl"] *)': 'translateX(-100%)',
233
+ },
226
234
  pointerEvents: 'none',
227
235
  transition: 'opacity 150ms ease',
228
236
  opacity: `var(${SHADOW_VAR_START}, 0)`,
229
- backgroundImage: `linear-gradient(to right, ${SHADOW_TINT}, transparent)`,
237
+ backgroundImage: {
238
+ default: `linear-gradient(to right, ${SHADOW_TINT}, transparent)`,
239
+ ':is([dir="rtl"] *)': `linear-gradient(to left, ${SHADOW_TINT}, transparent)`,
240
+ },
230
241
  },
231
242
  },
232
- // end-pinned: shadow falls to the left (inline-start), over scrolled content
243
+ // end-pinned: shadow falls toward inline-start (over scrolled content).
244
+ // insetInlineStart anchors the strip to the column's inline-start edge; the
245
+ // strip is nudged one full width OUTWARD into content — −X under LTR
246
+ // (inline-start = left) but +X under RTL (inline-start = right) — with the
247
+ // gradient fading from the pinned edge toward content in each direction.
233
248
  end: {
234
249
  '::after': {
235
250
  content: '""',
@@ -238,11 +253,17 @@ const shadowStyles = stylex.create({
238
253
  bottom: 0,
239
254
  insetInlineStart: 0,
240
255
  width: SHADOW_WIDTH,
241
- transform: 'translateX(-100%)',
256
+ transform: {
257
+ default: 'translateX(-100%)',
258
+ ':is([dir="rtl"] *)': 'translateX(100%)',
259
+ },
242
260
  pointerEvents: 'none',
243
261
  transition: 'opacity 150ms ease',
244
262
  opacity: `var(${SHADOW_VAR_END}, 0)`,
245
- backgroundImage: `linear-gradient(to left, ${SHADOW_TINT}, transparent)`,
263
+ backgroundImage: {
264
+ default: `linear-gradient(to left, ${SHADOW_TINT}, transparent)`,
265
+ ':is([dir="rtl"] *)': `linear-gradient(to right, ${SHADOW_TINT}, transparent)`,
266
+ },
246
267
  },
247
268
  },
248
269
  });
@@ -289,16 +310,24 @@ export function useTableStickyColumns<T extends Record<string, unknown>>(
289
310
  const {hasStart: hs, hasEnd: he} = stateRef.current;
290
311
  const maxScroll = el.scrollWidth - el.clientWidth;
291
312
  const hasOverflow = maxScroll > 1;
313
+ // RTL-safe scroll position. Spec-compliant browsers report a NEGATIVE
314
+ // scrollLeft under RTL (0 at the inline-start edge, decreasing toward the
315
+ // inline-end edge), so a raw `scrollLeft > 1` start test would never fire
316
+ // and the end test would be wrong-signed. Math.abs collapses both
317
+ // conventions to a distance-from-inline-start, matching the repo's own
318
+ // useScrollOverflow (overflowStart/overflowEnd use Math.abs, tolerance 1).
319
+ // No-op under LTR where scrollLeft is already >= 0.
320
+ const pos = Math.abs(el.scrollLeft);
292
321
  if (hs) {
293
322
  el.style.setProperty(
294
323
  SHADOW_VAR_START,
295
- hasOverflow && el.scrollLeft > 1 ? '1' : '0',
324
+ hasOverflow && pos > 1 ? '1' : '0',
296
325
  );
297
326
  }
298
327
  if (he) {
299
328
  el.style.setProperty(
300
329
  SHADOW_VAR_END,
301
- hasOverflow && el.scrollLeft < maxScroll - 1 ? '1' : '0',
330
+ hasOverflow && pos < maxScroll - 1 ? '1' : '0',
302
331
  );
303
332
  }
304
333
  };
@@ -42,7 +42,7 @@ import {
42
42
  import * as stylex from '@stylexjs/stylex';
43
43
  import {colorVars, radiusVars, spacingVars} from '../../../theme/tokens.stylex';
44
44
  import {Icon} from '../../../Icon';
45
- import {mergeRefs} from '../../../utils';
45
+ import {mergeRefs, rtlStyles} from '../../../utils';
46
46
  import type {
47
47
  TablePlugin,
48
48
  TableColumn,
@@ -299,12 +299,14 @@ function TreeExpander({
299
299
  : t('@astryx.tableTree.expandRow')
300
300
  }
301
301
  aria-expanded={isExpanded}>
302
- <span
303
- {...stylex.props(
304
- treeStyles.chevron,
305
- isExpanded && treeStyles.chevronExpanded,
306
- )}>
307
- <Icon icon="chevronRight" size="xsm" />
302
+ <span {...stylex.props(rtlStyles.mirror)}>
303
+ <span
304
+ {...stylex.props(
305
+ treeStyles.chevron,
306
+ isExpanded && treeStyles.chevronExpanded,
307
+ )}>
308
+ <Icon icon="chevronRight" size="xsm" />
309
+ </span>
308
310
  </span>
309
311
  </button>
310
312
  );
@@ -346,12 +348,14 @@ function TreeExpandAllToggle({
346
348
  : t('@astryx.tableTree.expandAllRows')
347
349
  }
348
350
  aria-expanded={allExpanded}>
349
- <span
350
- {...stylex.props(
351
- treeStyles.chevron,
352
- allExpanded && treeStyles.chevronExpanded,
353
- )}>
354
- <Icon icon="chevronRight" size="xsm" />
351
+ <span {...stylex.props(rtlStyles.mirror)}>
352
+ <span
353
+ {...stylex.props(
354
+ treeStyles.chevron,
355
+ allExpanded && treeStyles.chevronExpanded,
356
+ )}>
357
+ <Icon icon="chevronRight" size="xsm" />
358
+ </span>
355
359
  </span>
356
360
  </button>
357
361
  );
@@ -540,6 +540,39 @@ export interface BaseTableProps<
540
540
  /** Plugins to transform render props at each level */
541
541
  plugins?: TablePlugin<T>[];
542
542
 
543
+ /**
544
+ * ARIA row index (1-based) assigned to the **first** rendered body row.
545
+ *
546
+ * The row ordinal is an accessibility concern independent of any visible
547
+ * index column: `aria-rowindex` on each `<tr>` should reflect the row's
548
+ * position in the **full** dataset, not just the current page. For a
549
+ * paginated / windowed view, pass the offset of the first visible row,
550
+ * e.g. `(page - 1) * pageSize + 1`.
551
+ *
552
+ * Setting this (or {@link rowCount}) opts the table into emitting
553
+ * `aria-rowindex` on body rows and `aria-rowcount` on the `<table>`. Data
554
+ * rows are numbered from this value; the header row keeps native table
555
+ * semantics and is not assigned an ARIA row index (so the visible
556
+ * `useTableRowIndex` numbering and `aria-rowindex` stay in agreement).
557
+ *
558
+ * Applies to data-driven mode only (ignored in children mode).
559
+ *
560
+ * @default 1
561
+ */
562
+ rowIndexStart?: number;
563
+ /**
564
+ * Total number of body rows across **all** pages/windows, used for
565
+ * `aria-rowcount` on the `<table>`. Provide this for paginated data so
566
+ * assistive tech can announce "row X of Y" against the full dataset.
567
+ *
568
+ * When omitted but {@link rowIndexStart} is set (a windowed view with an
569
+ * unknown total, e.g. cursor pagination), `aria-rowcount` is set to `-1`
570
+ * per the ARIA convention for an unknown row count.
571
+ *
572
+ * Applies to data-driven mode only (ignored in children mode).
573
+ */
574
+ rowCount?: number;
575
+
543
576
  /** Children mode — render `<tr>`/`<td>` directly instead of data-driven */
544
577
  children?: ReactNode;
545
578
  /**
@@ -13,7 +13,7 @@ export const docs = {
13
13
  name: 'getStatus',
14
14
  type: "(item: T) => { color: 'accent' | 'success' | 'error' | 'warning' | 'red' | 'orange' | 'green' | 'yellow' | 'blue' | 'gray' | string; icon?: IconName; label: string } | null",
15
15
  description:
16
- 'Derive the status indicator for a row: a semantic color (accent, success, error, warning, red, orange, green, yellow, blue, gray — mapped to a theme token) or a raw CSS color as an escape hatch; an optional icon to signal status by shape instead of the dot (recommended when multiple statuses coexist; valid names: close, chevronDown, chevronLeft, chevronRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone); and a required accessible label (announced via role="img" and shown in a tooltip on hover, so a status is never conveyed by color alone). Return null for rows with no status. Memoize with useCallback for a stable plugin identity.',
16
+ 'Derive the status indicator for a row: a semantic color (accent, success, error, warning, red, orange, green, yellow, blue, gray, each mapped to a theme token) or a raw CSS color as an escape hatch; an optional icon to signal status by shape instead of the dot (recommended when multiple statuses coexist; valid names: close, chevronDown, chevronLeft, chevronRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone); and a required accessible label (announced via role="img" and shown in a tooltip on hover, so a status is never conveyed by color alone). Return null for rows with no status. Memoize with useCallback for a stable plugin identity.',
17
17
  required: true,
18
18
  },
19
19
  ],
@@ -108,9 +108,9 @@ export const docs = {
108
108
  },
109
109
  {
110
110
  name: 'statusVariant',
111
- type: "'attached' | 'detached'",
111
+ type: "'attached' | 'detached' | 'tooltip'",
112
112
  description:
113
- 'How the status message is placed relative to the input. attached overlaps directly below the input (bordered treatment); detached floats below as a separate element with spacing.',
113
+ 'How the status message is placed relative to the input. attached overlaps directly below the input (bordered treatment); detached floats below as a separate element with spacing; tooltip hides the message box and surfaces it in a tooltip on the status icon.',
114
114
  default: "'attached'",
115
115
  },
116
116
  {
@@ -154,6 +154,12 @@ export const docs = {
154
154
  description:
155
155
  'HTML name attribute for the textarea element, useful for form submissions.',
156
156
  },
157
+ {
158
+ name: 'width',
159
+ type: 'SizeValue',
160
+ description:
161
+ '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.',
162
+ },
157
163
  {
158
164
  name: 'onFocus',
159
165
  type: '(e: FocusEvent<HTMLTextAreaElement>) => void',
@@ -288,9 +294,9 @@ export const docsZh = {
288
294
  },
289
295
  {
290
296
  name: 'statusVariant',
291
- type: "'attached' | 'detached'",
297
+ type: "'attached' | 'detached' | 'tooltip'",
292
298
  description:
293
- '状态消息相对于输入框的放置方式。attached 直接叠加在输入框下方(带边框处理);detached 作为独立元素浮于下方并留有间距。',
299
+ '状态消息相对于输入框的放置方式。attached 直接叠加在输入框下方(带边框处理);detached 作为独立元素浮于下方并留有间距;tooltip 隐藏消息框,并在状态图标上以提示气泡形式显示。',
294
300
  default: "'attached'",
295
301
  },
296
302
  {
@@ -403,7 +409,7 @@ export const docsDense = {
403
409
  rows: 'Visible text rows.',
404
410
  maxLength: 'Max chars allowed. Shows counter (current/max) below textarea. No native enforcement.',
405
411
  status: 'Colored border+icon status. Optional floating message below textarea.',
406
- statusVariant: 'How status message is placed: attached overlaps below input; detached floats below w/ spacing.',
412
+ statusVariant: 'How status message is placed: attached overlaps below input; detached floats below w/ spacing; tooltip hides the box and shows it on the status icon.',
407
413
  labelTooltip: 'Tooltip in info icon at label end.',
408
414
  startIcon: 'Icon inside leading edge of textarea wrapper.',
409
415
  hasSpellCheck: 'Enables/disables browser spell checking.',
@@ -27,7 +27,6 @@ import {
27
27
  type ReactNode,
28
28
  } from 'react';
29
29
  import * as stylex from '@stylexjs/stylex';
30
- import type {IconName} from '../Icon';
31
30
  import {
32
31
  colorVars,
33
32
  spacingVars,
@@ -42,13 +41,14 @@ import {
42
41
  inputStatusFocusWithinStyles,
43
42
  type FieldStatusVariant,
44
43
  } from '../Field';
45
- import {Icon, renderIconSlot, type IconType} from '../Icon';
44
+ import {renderIconSlot, type IconType} from '../Icon';
46
45
  import {Spinner} from '../Spinner';
47
46
  import {useTooltip} from '../Tooltip';
48
47
  import {mergeProps, mergeRefs} from '../utils';
49
48
  import type {BaseProps} from '../BaseProps';
50
49
  import type {SizeValue} from '../utils/types';
51
50
  import {useInputContainer} from '../hooks/useInputContainer';
51
+ import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
52
52
  import {useSize} from '../SizeContext/SizeContext';
53
53
  import {themeProps} from '../utils/themeProps';
54
54
  import {VisuallyHidden} from '../VisuallyHidden';
@@ -222,6 +222,7 @@ export interface TextAreaProps extends Omit<
222
222
  * How the status message is placed relative to the input.
223
223
  * - 'attached': message overlaps directly below the input (bordered treatment)
224
224
  * - 'detached': message floats below as a separate element with spacing
225
+ * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap
225
226
  * @default 'attached'
226
227
  */
227
228
  statusVariant?: FieldStatusVariant;
@@ -350,25 +351,19 @@ export function TextArea({
350
351
  isEnabled: showsDisabledMessage,
351
352
  });
352
353
 
353
- const statusIconMap: Record<TextAreaStatusType, IconName> = {
354
- warning: 'warning',
355
- error: 'error',
356
- success: 'success',
357
- };
358
-
359
- const statusIconColorMap: Record<
360
- TextAreaStatusType,
361
- 'warning' | 'error' | 'success'
362
- > = {
363
- warning: 'warning',
364
- error: 'error',
365
- success: 'success',
366
- };
354
+ const {statusIcon, describedBy: statusTooltipDescribedBy} =
355
+ useInputStatusIcon({
356
+ status,
357
+ statusVariant,
358
+ });
367
359
 
368
360
  const ariaDescribedBy =
369
361
  [
370
362
  description ? descriptionID : null,
371
- status?.message ? statusMessageID : null,
363
+ statusVariant !== 'tooltip' && status?.message ? statusMessageID : null,
364
+ // The tooltip variant renders no message box; describe the input by the
365
+ // tooltip's content instead so the status is still announced.
366
+ statusTooltipDescribedBy,
372
367
  maxLength != null ? counterID : null,
373
368
  showsDisabledMessage ? disabledMessageTooltip.describedBy : null,
374
369
  ]
@@ -442,7 +437,9 @@ export function TextArea({
442
437
  textareaSizeStyles[size],
443
438
  effectivelyDisabled && inputWrapperStyles.disabled,
444
439
  status && inputStatusBorderStyles[status.type],
445
- status && inputStatusHoverShadowStyles[status.type],
440
+ status &&
441
+ !effectivelyDisabled &&
442
+ inputStatusHoverShadowStyles[status.type],
446
443
  status && inputStatusFocusWithinStyles[status.type],
447
444
  xstyle,
448
445
  ),
@@ -488,14 +485,8 @@ export function TextArea({
488
485
  )}
489
486
  />
490
487
  {isBusy && <Spinner size="sm" />}
491
- {status && (
492
- <span {...stylex.props(styles.statusIcon)}>
493
- <Icon
494
- icon={statusIconMap[status.type]}
495
- size="md"
496
- color={statusIconColorMap[status.type]}
497
- />
498
- </span>
488
+ {statusIcon && (
489
+ <span {...stylex.props(styles.statusIcon)}>{statusIcon}</span>
499
490
  )}
500
491
  </div>
501
492
  {maxLength != null && (
@@ -116,9 +116,9 @@ export const docs = {
116
116
  },
117
117
  {
118
118
  name: 'statusVariant',
119
- type: "'attached' | 'detached'",
119
+ type: "'attached' | 'detached' | 'tooltip'",
120
120
  description:
121
- 'How the status message is placed relative to the input. attached overlaps directly below the input (bordered treatment); detached floats below as a separate element with spacing.',
121
+ 'How the status message is placed relative to the input. attached overlaps directly below the input (bordered treatment); detached floats below as a separate element with spacing; tooltip hides the message box and surfaces it in a tooltip on the status icon.',
122
122
  default: "'attached'",
123
123
  },
124
124
  {
@@ -140,6 +140,12 @@ export const docs = {
140
140
  description:
141
141
  'The HTML name attribute for the input, useful for form submissions.',
142
142
  },
143
+ {
144
+ name: 'width',
145
+ type: 'SizeValue',
146
+ description:
147
+ '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.',
148
+ },
143
149
  ],
144
150
  theming: {
145
151
  targets: [
@@ -283,9 +289,9 @@ export const docsZh = {
283
289
  },
284
290
  {
285
291
  name: 'statusVariant',
286
- type: "'attached' | 'detached'",
292
+ type: "'attached' | 'detached' | 'tooltip'",
287
293
  description:
288
- '状态消息相对于输入框的放置方式。attached 直接叠加在输入框下方(带边框处理);detached 作为独立元素浮于下方并留有间距。',
294
+ '状态消息相对于输入框的放置方式。attached 直接叠加在输入框下方(带边框处理);detached 作为独立元素浮于下方并留有间距;tooltip 隐藏消息框,并在状态图标上以提示气泡形式显示。',
289
295
  default: "'attached'",
290
296
  },
291
297
  {
@@ -382,7 +388,7 @@ export const docsDense = {
382
388
  labelTooltip: 'Tooltip in info icon at label end.',
383
389
  startIcon: 'SVG icon at input start (e.g. heroicons or lucide).',
384
390
  status: 'Validation status; colored border+icon. Message floats below. Error sets aria-invalid.',
385
- statusVariant: 'How status message is placed: attached overlaps below input; detached floats below w/ spacing.',
391
+ statusVariant: 'How status message is placed: attached overlaps below input; detached floats below w/ spacing; tooltip hides the box and shows it on the status icon.',
386
392
  hasClear: 'Shows clear button when input has value. Clears value on click.',
387
393
  hasAutoFocus: 'Auto-focus input on mount.',
388
394
  htmlName: 'HTML name attr for form submissions.',
@@ -13,6 +13,7 @@ import {describe, it, expect, vi, beforeEach} from 'vitest';
13
13
  import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {TestIcon} from '../__tests__/TestIcon';
16
+ import {InputGroup} from '../InputGroup';
16
17
  import {TextInput} from './TextInput';
17
18
 
18
19
  // Mock showPopover/hidePopover since jsdom does not implement them. Used by the
@@ -229,6 +230,26 @@ describe('TextInput', () => {
229
230
  expect(screen.getByText('Invalid email address')).toBeInTheDocument();
230
231
  });
231
232
 
233
+ it('has no dangling aria-describedby ids inside InputGroup (WCAG 1.3.1)', () => {
234
+ // Inside an InputGroup no Field renders, so the status message element
235
+ // does not exist; aria-describedby must not reference its id.
236
+ render(
237
+ <InputGroup label="Contact">
238
+ <TextInput
239
+ label="Email"
240
+ value=""
241
+ onChange={() => {}}
242
+ status={{type: 'error', message: 'Invalid email address'}}
243
+ />
244
+ </InputGroup>,
245
+ );
246
+ const input = screen.getByRole('textbox');
247
+ const describedBy = input.getAttribute('aria-describedby') ?? '';
248
+ for (const idToken of describedBy.split(/\s+/).filter(Boolean)) {
249
+ expect(document.getElementById(idToken)).not.toBeNull();
250
+ }
251
+ });
252
+
232
253
  it('does not render status message when not provided', () => {
233
254
  render(
234
255
  <TextInput
@@ -660,11 +681,15 @@ describe('TextInput', () => {
660
681
  });
661
682
  });
662
683
 
663
-
664
684
  describe('TextInput statusVariant forwarding', () => {
665
685
  it('defaults to attached (status renders with data-variant="attached")', () => {
666
686
  const {container} = render(
667
- <TextInput label="Email" value="" onChange={() => {}} status={{type: 'error', message: 'Invalid email'}} />,
687
+ <TextInput
688
+ label="Email"
689
+ value=""
690
+ onChange={() => {}}
691
+ status={{type: 'error', message: 'Invalid email'}}
692
+ />,
668
693
  );
669
694
  expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
670
695
  'data-variant',
@@ -674,11 +699,113 @@ describe('TextInput statusVariant forwarding', () => {
674
699
 
675
700
  it('forwards statusVariant="detached" to the underlying Field status', () => {
676
701
  const {container} = render(
677
- <TextInput label="Email" value="" onChange={() => {}} status={{type: 'error', message: 'Invalid email'}} statusVariant="detached" />,
702
+ <TextInput
703
+ label="Email"
704
+ value=""
705
+ onChange={() => {}}
706
+ status={{type: 'error', message: 'Invalid email'}}
707
+ statusVariant="detached"
708
+ />,
678
709
  );
679
710
  expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
680
711
  'data-variant',
681
712
  'detached',
682
713
  );
683
714
  });
715
+
716
+ it('renders no message box for statusVariant="tooltip"', () => {
717
+ const {container} = render(
718
+ <TextInput
719
+ label="Email"
720
+ value=""
721
+ onChange={() => {}}
722
+ status={{type: 'error', message: 'Invalid email'}}
723
+ statusVariant="tooltip"
724
+ />,
725
+ );
726
+ expect(
727
+ container.querySelector('.astryx-field-status'),
728
+ ).not.toBeInTheDocument();
729
+ });
730
+
731
+ it('surfaces the status message in a tooltip for statusVariant="tooltip"', () => {
732
+ render(
733
+ <TextInput
734
+ label="Email"
735
+ value=""
736
+ onChange={() => {}}
737
+ status={{type: 'error', message: 'Invalid email'}}
738
+ statusVariant="tooltip"
739
+ />,
740
+ );
741
+ const tooltip = screen.getByRole('tooltip', h);
742
+ expect(tooltip).toHaveTextContent('Invalid email');
743
+ });
744
+
745
+ it('describes the input by the status tooltip for statusVariant="tooltip"', () => {
746
+ render(
747
+ <TextInput
748
+ label="Email"
749
+ value=""
750
+ onChange={() => {}}
751
+ status={{type: 'error', message: 'Invalid email'}}
752
+ statusVariant="tooltip"
753
+ />,
754
+ );
755
+ const input = screen.getByRole('textbox');
756
+ const tooltip = screen.getByRole('tooltip', h);
757
+ expect(input.getAttribute('aria-describedby')).toContain(tooltip.id);
758
+ });
759
+
760
+ it('renders the tooltip status affordance as a focusable button (WCAG 2.1.1)', () => {
761
+ render(
762
+ <TextInput
763
+ label="Email"
764
+ value=""
765
+ onChange={() => {}}
766
+ status={{type: 'error', message: 'Invalid email'}}
767
+ statusVariant="tooltip"
768
+ />,
769
+ );
770
+ // The status affordance is a real button with an accessible name naming
771
+ // the status type (WCAG 4.1.2), so keyboard-only users (no AT) can reach it.
772
+ const statusButton = screen.getByRole('button', {name: /error details/i});
773
+ expect(statusButton).toBeInTheDocument();
774
+ expect(statusButton).toHaveAttribute('type', 'button');
775
+ });
776
+
777
+ it('opens the status tooltip on keyboard focus for statusVariant="tooltip"', async () => {
778
+ const user = userEvent.setup();
779
+ render(
780
+ <TextInput
781
+ label="Email"
782
+ value=""
783
+ onChange={() => {}}
784
+ status={{type: 'error', message: 'Invalid email'}}
785
+ statusVariant="tooltip"
786
+ />,
787
+ );
788
+ const tooltip = screen.getByRole('tooltip', h);
789
+ // Tab from the input to the status button; keyboard focus reveals the tip.
790
+ await user.tab();
791
+ await user.tab();
792
+ await waitFor(() => {
793
+ expect(tooltip).toHaveAttribute('popover-open');
794
+ });
795
+ });
796
+
797
+ it('describes the status button by the tooltip content for statusVariant="tooltip"', () => {
798
+ render(
799
+ <TextInput
800
+ label="Email"
801
+ value=""
802
+ onChange={() => {}}
803
+ status={{type: 'error', message: 'Invalid email'}}
804
+ statusVariant="tooltip"
805
+ />,
806
+ );
807
+ const statusButton = screen.getByRole('button', {name: /error details/i});
808
+ const tooltip = screen.getByRole('tooltip', h);
809
+ expect(statusButton.getAttribute('aria-describedby')).toContain(tooltip.id);
810
+ });
684
811
  });