@astryxdesign/core 0.1.9 → 0.2.0-canary.136a00f

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 (560) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/Avatar/Avatar.d.ts +42 -3
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +190 -59
  5. package/dist/Avatar/AvatarStatusDot.d.ts +6 -4
  6. package/dist/Avatar/AvatarStatusDot.d.ts.map +1 -1
  7. package/dist/AvatarGroup/AvatarGroup.d.ts +7 -1
  8. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  9. package/dist/AvatarGroup/AvatarGroup.js +58 -6
  10. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
  11. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  12. package/dist/AvatarGroup/AvatarGroupOverflow.js +7 -2
  13. package/dist/Banner/Banner.js +2 -2
  14. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  15. package/dist/Breadcrumbs/BreadcrumbItem.js +11 -4
  16. package/dist/Breadcrumbs/index.d.ts +1 -0
  17. package/dist/Breadcrumbs/index.d.ts.map +1 -1
  18. package/dist/Breadcrumbs/index.js +2 -1
  19. package/dist/Button/Button.d.ts +1 -1
  20. package/dist/Button/Button.d.ts.map +1 -1
  21. package/dist/Button/Button.js +6 -4
  22. package/dist/Calendar/Calendar.d.ts.map +1 -1
  23. package/dist/Calendar/Calendar.js +50 -9
  24. package/dist/Calendar/dayCellUtils.d.ts +4 -4
  25. package/dist/Calendar/dayCellUtils.js +2 -2
  26. package/dist/Calendar/styles.d.ts +30 -31
  27. package/dist/Calendar/styles.d.ts.map +1 -1
  28. package/dist/Calendar/styles.js +30 -31
  29. package/dist/Card/Card.js +1 -1
  30. package/dist/Carousel/Carousel.d.ts.map +1 -1
  31. package/dist/Carousel/Carousel.js +22 -12
  32. package/dist/Chat/ChatComposerInput.js +1 -1
  33. package/dist/Chat/ChatDictationButton.d.ts.map +1 -1
  34. package/dist/Chat/ChatDictationButton.js +3 -1
  35. package/dist/Chat/ChatLayout.js +4 -4
  36. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  37. package/dist/Chat/ChatMessageBubble.js +8 -8
  38. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  39. package/dist/Chat/ChatToolCalls.js +10 -3
  40. package/dist/Chat/useTriggerMenu.js +2 -2
  41. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  42. package/dist/CheckboxInput/CheckboxInput.js +6 -6
  43. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  44. package/dist/CheckboxList/CheckboxListItem.js +19 -4
  45. package/dist/Citation/Citation.d.ts +22 -1
  46. package/dist/Citation/Citation.d.ts.map +1 -1
  47. package/dist/Citation/Citation.js +20 -6
  48. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  49. package/dist/CodeBlock/CodeBlock.js +16 -10
  50. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  51. package/dist/Collapsible/Collapsible.js +3 -1
  52. package/dist/CommandPalette/CommandPalette.d.ts +1 -1
  53. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  54. package/dist/CommandPalette/CommandPalette.js +42 -3
  55. package/dist/CommandPalette/CommandPaletteItem.js +1 -1
  56. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  57. package/dist/ContextMenu/ContextMenu.js +23 -5
  58. package/dist/ContextMenu/index.d.ts +1 -0
  59. package/dist/ContextMenu/index.d.ts.map +1 -1
  60. package/dist/ContextMenu/index.js +3 -1
  61. package/dist/DateInput/DateInput.d.ts +10 -2
  62. package/dist/DateInput/DateInput.d.ts.map +1 -1
  63. package/dist/DateInput/DateInput.js +33 -19
  64. package/dist/DateRangeInput/DateRangeInput.d.ts +10 -2
  65. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  66. package/dist/DateRangeInput/DateRangeInput.js +23 -20
  67. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  68. package/dist/DateTimeInput/DateTimeInput.js +23 -18
  69. package/dist/Divider/Divider.d.ts +2 -2
  70. package/dist/Divider/Divider.d.ts.map +1 -1
  71. package/dist/Divider/Divider.js +13 -0
  72. package/dist/DropdownMenu/DropdownMenu.d.ts +6 -0
  73. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  74. package/dist/DropdownMenu/DropdownMenu.js +22 -5
  75. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  76. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +4 -1
  77. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  78. package/dist/DropdownMenu/DropdownMenuItem.js +5 -2
  79. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +8 -11
  80. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  81. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +3 -1
  82. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
  83. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  84. package/dist/DropdownMenu/DropdownMenuRadioItem.js +5 -2
  85. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +104 -0
  86. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -0
  87. package/dist/DropdownMenu/DropdownMenuSubMenu.js +427 -0
  88. package/dist/DropdownMenu/index.d.ts +10 -0
  89. package/dist/DropdownMenu/index.d.ts.map +1 -1
  90. package/dist/DropdownMenu/index.js +13 -0
  91. package/dist/DropdownMenu/menuItemHover.d.ts +23 -0
  92. package/dist/DropdownMenu/menuItemHover.d.ts.map +1 -0
  93. package/dist/DropdownMenu/menuItemHover.js +33 -0
  94. package/dist/DropdownMenu/menuItemRoles.d.ts +8 -0
  95. package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -1
  96. package/dist/DropdownMenu/menuItemRoles.js +10 -1
  97. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  98. package/dist/DropdownMenu/renderDropdownItems.js +21 -6
  99. package/dist/Field/Field.d.ts +3 -1
  100. package/dist/Field/Field.d.ts.map +1 -1
  101. package/dist/Field/Field.js +4 -1
  102. package/dist/Field/FieldLabel.d.ts +2 -1
  103. package/dist/Field/FieldLabel.d.ts.map +1 -1
  104. package/dist/Field/FieldLabel.js +9 -5
  105. package/dist/Field/inputStyles.stylex.d.ts +1 -0
  106. package/dist/Field/inputStyles.stylex.d.ts.map +1 -1
  107. package/dist/Field/inputStyles.stylex.js +1 -0
  108. package/dist/FieldStatus/FieldStatus.d.ts +10 -0
  109. package/dist/FieldStatus/FieldStatus.d.ts.map +1 -1
  110. package/dist/FieldStatus/FieldStatus.js +39 -3
  111. package/dist/FileInput/FileInput.d.ts +10 -2
  112. package/dist/FileInput/FileInput.d.ts.map +1 -1
  113. package/dist/FileInput/FileInput.js +93 -65
  114. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  115. package/dist/HoverCard/useHoverCard.js +10 -4
  116. package/dist/Icon/Icon.d.ts +15 -1
  117. package/dist/Icon/Icon.d.ts.map +1 -1
  118. package/dist/Icon/Icon.js +19 -3
  119. package/dist/Item/Item.d.ts +16 -3
  120. package/dist/Item/Item.d.ts.map +1 -1
  121. package/dist/Item/Item.js +31 -6
  122. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  123. package/dist/Layer/layerAnimations.stylex.js +10 -2
  124. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  125. package/dist/Lightbox/Lightbox.js +19 -13
  126. package/dist/List/List.d.ts.map +1 -1
  127. package/dist/List/List.js +6 -3
  128. package/dist/List/ListItem.d.ts +11 -1
  129. package/dist/List/ListItem.d.ts.map +1 -1
  130. package/dist/List/ListItem.js +2 -0
  131. package/dist/Markdown/Markdown.d.ts.map +1 -1
  132. package/dist/Markdown/Markdown.js +7 -0
  133. package/dist/MetadataList/MetadataList.d.ts +1 -1
  134. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  135. package/dist/MetadataList/MetadataList.js +4 -2
  136. package/dist/MultiSelector/MultiSelector.d.ts +9 -1
  137. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  138. package/dist/MultiSelector/MultiSelector.js +80 -42
  139. package/dist/NavMenu/NavHeadingMenuItem.js +1 -1
  140. package/dist/NumberInput/NumberInput.d.ts +10 -2
  141. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  142. package/dist/NumberInput/NumberInput.js +22 -17
  143. package/dist/Outline/Outline.js +2 -2
  144. package/dist/Pagination/Pagination.d.ts.map +1 -1
  145. package/dist/Pagination/Pagination.js +11 -3
  146. package/dist/PowerSearch/PowerSearch.d.ts +9 -2
  147. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  148. package/dist/PowerSearch/PowerSearch.js +2 -0
  149. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  150. package/dist/ProgressBar/ProgressBar.js +6 -1
  151. package/dist/RadioList/RadioList.d.ts +2 -2
  152. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  153. package/dist/RadioList/RadioListItem.js +5 -5
  154. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  155. package/dist/Resizable/ResizeHandle.js +49 -19
  156. package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -1
  157. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  158. package/dist/SegmentedControl/SegmentedControlItem.js +5 -3
  159. package/dist/Selector/Selector.d.ts +8 -0
  160. package/dist/Selector/Selector.d.ts.map +1 -1
  161. package/dist/Selector/Selector.js +66 -17
  162. package/dist/SideNav/SideNav.d.ts.map +1 -1
  163. package/dist/SideNav/SideNav.js +5 -5
  164. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  165. package/dist/SideNav/SideNavCollapseButton.js +13 -10
  166. package/dist/SideNav/SideNavItem.d.ts +1 -1
  167. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  168. package/dist/SideNav/SideNavItem.js +12 -10
  169. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  170. package/dist/Skeleton/Skeleton.js +2 -2
  171. package/dist/Slider/Slider.d.ts.map +1 -1
  172. package/dist/Slider/Slider.js +87 -34
  173. package/dist/Spinner/Spinner.d.ts +3 -2
  174. package/dist/Spinner/Spinner.d.ts.map +1 -1
  175. package/dist/Spinner/Spinner.js +11 -2
  176. package/dist/Switch/Switch.d.ts +8 -1
  177. package/dist/Switch/Switch.d.ts.map +1 -1
  178. package/dist/Switch/Switch.js +171 -91
  179. package/dist/TabList/Tab.js +2 -2
  180. package/dist/TabList/TabList.d.ts +2 -11
  181. package/dist/TabList/TabList.d.ts.map +1 -1
  182. package/dist/TabList/TabList.js +5 -9
  183. package/dist/TabList/TabListContext.d.ts +0 -5
  184. package/dist/TabList/TabListContext.d.ts.map +1 -1
  185. package/dist/TabList/TabListContext.js +0 -5
  186. package/dist/TabList/TabMenu.js +1 -1
  187. package/dist/Table/BaseTable.d.ts.map +1 -1
  188. package/dist/Table/BaseTable.js +30 -4
  189. package/dist/Table/TableCell.js +3 -3
  190. package/dist/Table/TableHeaderCell.js +3 -3
  191. package/dist/Table/plugins/groupedRows/useTableGroupedRows.d.ts.map +1 -1
  192. package/dist/Table/plugins/groupedRows/useTableGroupedRows.js +15 -11
  193. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  194. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +37 -27
  195. package/dist/Table/plugins/selection/useTableSelection.d.ts.map +1 -1
  196. package/dist/Table/plugins/selection/useTableSelection.js +3 -0
  197. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -1
  198. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +15 -7
  199. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -1
  200. package/dist/Table/plugins/tree/useTableTreeData.js +29 -23
  201. package/dist/Table/types.d.ts +32 -0
  202. package/dist/Table/types.d.ts.map +1 -1
  203. package/dist/TextArea/TextArea.d.ts +10 -1
  204. package/dist/TextArea/TextArea.d.ts.map +1 -1
  205. package/dist/TextArea/TextArea.js +18 -19
  206. package/dist/TextInput/TextInput.d.ts +10 -2
  207. package/dist/TextInput/TextInput.d.ts.map +1 -1
  208. package/dist/TextInput/TextInput.js +20 -17
  209. package/dist/Thumbnail/Thumbnail.js +2 -2
  210. package/dist/TimeInput/TimeInput.d.ts +10 -2
  211. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  212. package/dist/TimeInput/TimeInput.js +17 -17
  213. package/dist/Timestamp/Timestamp.d.ts +31 -3
  214. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  215. package/dist/Timestamp/Timestamp.js +47 -76
  216. package/dist/Timestamp/formatInstant.d.ts +41 -0
  217. package/dist/Timestamp/formatInstant.d.ts.map +1 -0
  218. package/dist/Timestamp/formatInstant.js +177 -0
  219. package/dist/Timestamp/index.d.ts +1 -0
  220. package/dist/Timestamp/index.d.ts.map +1 -1
  221. package/dist/Timestamp/tooltipEntries.d.ts +56 -0
  222. package/dist/Timestamp/tooltipEntries.d.ts.map +1 -0
  223. package/dist/Timestamp/tooltipEntries.js +163 -0
  224. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  225. package/dist/Toast/ToastViewport.js +61 -9
  226. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  227. package/dist/ToggleButton/ToggleButton.js +3 -1
  228. package/dist/Token/Token.d.ts +34 -4
  229. package/dist/Token/Token.d.ts.map +1 -1
  230. package/dist/Token/Token.js +85 -45
  231. package/dist/Token/TokenLink.d.ts +62 -0
  232. package/dist/Token/TokenLink.d.ts.map +1 -0
  233. package/dist/Token/TokenLink.js +76 -0
  234. package/dist/Token/index.d.ts +1 -1
  235. package/dist/Token/index.d.ts.map +1 -1
  236. package/dist/Tokenizer/Tokenizer.d.ts +9 -2
  237. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  238. package/dist/Tokenizer/Tokenizer.js +4 -2
  239. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  240. package/dist/Tooltip/useTooltip.js +17 -1
  241. package/dist/TopNav/TopNavItem.d.ts +3 -1
  242. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  243. package/dist/TopNav/TopNavItem.js +30 -3
  244. package/dist/TreeList/TreeList.d.ts.map +1 -1
  245. package/dist/TreeList/TreeList.js +1 -0
  246. package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
  247. package/dist/TreeList/TreeListBranches.js +8 -6
  248. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  249. package/dist/TreeList/TreeListItem.js +32 -14
  250. package/dist/Typeahead/BaseTypeahead.d.ts +9 -0
  251. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  252. package/dist/Typeahead/BaseTypeahead.js +22 -11
  253. package/dist/Typeahead/Typeahead.d.ts +9 -2
  254. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  255. package/dist/Typeahead/Typeahead.js +11 -2
  256. package/dist/Typeahead/TypeaheadItem.d.ts +1 -1
  257. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  258. package/dist/Typeahead/TypeaheadItem.js +20 -10
  259. package/dist/astryx.css +70 -30
  260. package/dist/astryx.umd.js +51 -51
  261. package/dist/astryx.umd.js.map +4 -4
  262. package/dist/hooks/index.d.ts +2 -0
  263. package/dist/hooks/index.d.ts.map +1 -1
  264. package/dist/hooks/index.js +1 -0
  265. package/dist/hooks/isRtlElement.d.ts +18 -0
  266. package/dist/hooks/isRtlElement.d.ts.map +1 -0
  267. package/dist/hooks/isRtlElement.js +27 -0
  268. package/dist/hooks/useAnnounce.d.ts +3 -0
  269. package/dist/hooks/useAnnounce.d.ts.map +1 -1
  270. package/dist/hooks/useAnnounce.js +39 -1
  271. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  272. package/dist/hooks/useFocusTrap.js +52 -11
  273. package/dist/hooks/useGridFocus.d.ts +6 -1
  274. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  275. package/dist/hooks/useGridFocus.js +12 -9
  276. package/dist/hooks/useHotkeys.d.ts +9 -0
  277. package/dist/hooks/useHotkeys.d.ts.map +1 -1
  278. package/dist/hooks/useHotkeys.js +9 -0
  279. package/dist/hooks/useInputStatusIcon.d.ts +38 -0
  280. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -0
  281. package/dist/hooks/useInputStatusIcon.js +158 -0
  282. package/dist/hooks/useKeyboardHint.d.ts +1 -1
  283. package/dist/hooks/useKeyboardHint.d.ts.map +1 -1
  284. package/dist/hooks/useKeyboardHint.js +4 -2
  285. package/dist/hooks/useListFocus.d.ts +39 -1
  286. package/dist/hooks/useListFocus.d.ts.map +1 -1
  287. package/dist/hooks/useListFocus.js +54 -9
  288. package/dist/i18n/InternationalizationContext.d.ts +1 -0
  289. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  290. package/dist/i18n/InternationalizationContext.js +3 -0
  291. package/dist/i18n/InternationalizationProvider.d.ts +8 -1
  292. package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
  293. package/dist/i18n/InternationalizationProvider.js +5 -1
  294. package/dist/i18n/getLocaleDirection.d.ts +18 -0
  295. package/dist/i18n/getLocaleDirection.d.ts.map +1 -0
  296. package/dist/i18n/getLocaleDirection.js +29 -0
  297. package/dist/i18n/index.d.ts +6 -0
  298. package/dist/i18n/index.d.ts.map +1 -1
  299. package/dist/i18n/index.js +7 -1
  300. package/dist/i18n/useDirection.d.ts +2 -0
  301. package/dist/i18n/useDirection.d.ts.map +1 -0
  302. package/dist/i18n/useDirection.js +27 -0
  303. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  304. package/dist/theme/derivedVarRegistry.js +13 -0
  305. package/dist/utils/index.d.ts +1 -0
  306. package/dist/utils/index.d.ts.map +1 -1
  307. package/dist/utils/index.js +2 -1
  308. package/dist/utils/plainDate.d.ts +17 -0
  309. package/dist/utils/plainDate.d.ts.map +1 -1
  310. package/dist/utils/plainDate.js +23 -4
  311. package/dist/utils/rtlStyles.d.ts +24 -0
  312. package/dist/utils/rtlStyles.d.ts.map +1 -0
  313. package/dist/utils/rtlStyles.js +27 -0
  314. package/locales/en.json +112 -0
  315. package/locales/pseudo.json +84 -0
  316. package/package.json +6 -1
  317. package/src/Avatar/Avatar.doc.mjs +50 -3
  318. package/src/Avatar/Avatar.test.tsx +218 -0
  319. package/src/Avatar/Avatar.tsx +285 -79
  320. package/src/Avatar/AvatarStatusDot.doc.mjs +4 -4
  321. package/src/Avatar/AvatarStatusDot.tsx +6 -4
  322. package/src/AvatarGroup/AvatarGroup.doc.mjs +2 -0
  323. package/src/AvatarGroup/AvatarGroup.test.tsx +162 -2
  324. package/src/AvatarGroup/AvatarGroup.tsx +58 -3
  325. package/src/AvatarGroup/AvatarGroupOverflow.tsx +5 -2
  326. package/src/Banner/Banner.tsx +6 -6
  327. package/src/Breadcrumbs/BreadcrumbItem.tsx +10 -10
  328. package/src/Breadcrumbs/Breadcrumbs.test.tsx +52 -1
  329. package/src/Breadcrumbs/index.ts +4 -0
  330. package/src/Button/Button.doc.mjs +0 -4
  331. package/src/Button/Button.test.tsx +16 -0
  332. package/src/Button/Button.tsx +6 -4
  333. package/src/Calendar/Calendar.test.tsx +170 -20
  334. package/src/Calendar/Calendar.tsx +73 -16
  335. package/src/Calendar/dayCellUtils.test.ts +18 -18
  336. package/src/Calendar/dayCellUtils.ts +2 -2
  337. package/src/Calendar/styles.ts +33 -39
  338. package/src/Card/Card.tsx +1 -1
  339. package/src/Carousel/Carousel.tsx +25 -6
  340. package/src/Chat/ChatComposer.doc.mjs +2 -2
  341. package/src/Chat/ChatComposerInput.doc.mjs +1 -1
  342. package/src/Chat/ChatComposerInput.tsx +2 -2
  343. package/src/Chat/ChatDictationButton.tsx +7 -1
  344. package/src/Chat/ChatLayout.tsx +4 -4
  345. package/src/Chat/ChatMessageBubble.tsx +15 -10
  346. package/src/Chat/ChatSystemMessage.test.tsx +5 -0
  347. package/src/Chat/ChatToolCalls.tsx +9 -2
  348. package/src/Chat/useTriggerMenu.tsx +1 -1
  349. package/src/CheckboxInput/CheckboxInput.doc.mjs +6 -0
  350. package/src/CheckboxInput/CheckboxInput.test.tsx +23 -0
  351. package/src/CheckboxInput/CheckboxInput.tsx +21 -6
  352. package/src/CheckboxList/CheckboxList.doc.mjs +6 -0
  353. package/src/CheckboxList/CheckboxList.test.tsx +94 -3
  354. package/src/CheckboxList/CheckboxListItem.tsx +17 -10
  355. package/src/Citation/Citation.doc.mjs +4 -3
  356. package/src/Citation/Citation.test.tsx +97 -0
  357. package/src/Citation/Citation.tsx +43 -11
  358. package/src/CodeBlock/CodeBlock.test.tsx +17 -0
  359. package/src/CodeBlock/CodeBlock.tsx +38 -11
  360. package/src/Collapsible/Collapsible.doc.mjs +1 -0
  361. package/src/Collapsible/Collapsible.test.tsx +7 -0
  362. package/src/Collapsible/Collapsible.tsx +16 -6
  363. package/src/CommandPalette/CommandPalette.test.tsx +95 -1
  364. package/src/CommandPalette/CommandPalette.tsx +41 -2
  365. package/src/CommandPalette/CommandPaletteItem.tsx +1 -1
  366. package/src/ContextMenu/ContextMenu.test.tsx +59 -2
  367. package/src/ContextMenu/ContextMenu.tsx +22 -11
  368. package/src/ContextMenu/index.ts +5 -0
  369. package/src/DateInput/DateInput.doc.mjs +25 -0
  370. package/src/DateInput/DateInput.test.tsx +230 -0
  371. package/src/DateInput/DateInput.tsx +51 -29
  372. package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
  373. package/src/DateRangeInput/DateRangeInput.test.tsx +140 -0
  374. package/src/DateRangeInput/DateRangeInput.tsx +42 -30
  375. package/src/DateTimeInput/DateTimeInput.doc.mjs +22 -1
  376. package/src/DateTimeInput/DateTimeInput.test.tsx +112 -0
  377. package/src/DateTimeInput/DateTimeInput.tsx +44 -39
  378. package/src/Divider/Divider.test.tsx +32 -8
  379. package/src/Divider/Divider.tsx +14 -1
  380. package/src/DropdownMenu/DropdownMenu.test.tsx +60 -0
  381. package/src/DropdownMenu/DropdownMenu.tsx +31 -12
  382. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +8 -2
  383. package/src/DropdownMenu/DropdownMenuItem.tsx +9 -3
  384. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +4 -12
  385. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +9 -10
  386. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +9 -3
  387. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +20 -2
  388. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +88 -0
  389. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +635 -0
  390. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +553 -0
  391. package/src/DropdownMenu/index.ts +16 -0
  392. package/src/DropdownMenu/menuItemHover.ts +39 -0
  393. package/src/DropdownMenu/menuItemRoles.ts +9 -0
  394. package/src/DropdownMenu/renderDropdownItems.tsx +29 -15
  395. package/src/Field/Field.tsx +14 -9
  396. package/src/Field/FieldLabel.test.tsx +25 -0
  397. package/src/Field/FieldLabel.tsx +12 -3
  398. package/src/Field/inputStyles.stylex.ts +2 -0
  399. package/src/FieldStatus/FieldStatus.test.tsx +72 -0
  400. package/src/FieldStatus/FieldStatus.tsx +48 -2
  401. package/src/FileInput/FileInput.doc.mjs +23 -2
  402. package/src/FileInput/FileInput.test.tsx +104 -13
  403. package/src/FileInput/FileInput.tsx +106 -79
  404. package/src/HoverCard/useHoverCard.tsx +15 -3
  405. package/src/Icon/Icon.doc.mjs +13 -0
  406. package/src/Icon/Icon.test.tsx +89 -0
  407. package/src/Icon/Icon.tsx +51 -3
  408. package/src/Item/Item.doc.mjs +5 -0
  409. package/src/Item/Item.test.tsx +88 -0
  410. package/src/Item/Item.tsx +57 -11
  411. package/src/Layer/layerAnimations.stylex.ts +23 -0
  412. package/src/Lightbox/Lightbox.tsx +15 -7
  413. package/src/List/List.test.tsx +17 -29
  414. package/src/List/List.tsx +6 -1
  415. package/src/List/ListItem.doc.mjs +12 -0
  416. package/src/List/ListItem.tsx +13 -0
  417. package/src/Markdown/Markdown.test.tsx +21 -0
  418. package/src/Markdown/Markdown.tsx +4 -0
  419. package/src/MetadataList/MetadataList.test.tsx +24 -0
  420. package/src/MetadataList/MetadataList.tsx +6 -2
  421. package/src/MultiSelector/MultiSelector.doc.mjs +20 -0
  422. package/src/MultiSelector/MultiSelector.test.tsx +402 -0
  423. package/src/MultiSelector/MultiSelector.tsx +120 -59
  424. package/src/NavMenu/NavHeadingMenuItem.tsx +1 -1
  425. package/src/NumberInput/NumberInput.doc.mjs +21 -0
  426. package/src/NumberInput/NumberInput.test.tsx +71 -0
  427. package/src/NumberInput/NumberInput.tsx +35 -26
  428. package/src/Outline/Outline.test.tsx +12 -12
  429. package/src/Outline/Outline.tsx +2 -2
  430. package/src/Pagination/Pagination.tsx +11 -3
  431. package/src/PowerSearch/PowerSearch.doc.mjs +15 -0
  432. package/src/PowerSearch/PowerSearch.test.tsx +23 -0
  433. package/src/PowerSearch/PowerSearch.tsx +10 -1
  434. package/src/ProgressBar/ProgressBar.test.tsx +34 -0
  435. package/src/ProgressBar/ProgressBar.tsx +16 -1
  436. package/src/RadioList/RadioList.doc.mjs +6 -0
  437. package/src/RadioList/RadioList.test.tsx +26 -4
  438. package/src/RadioList/RadioList.tsx +2 -2
  439. package/src/RadioList/RadioListItem.tsx +12 -5
  440. package/src/Resizable/ResizeHandle.test.tsx +94 -0
  441. package/src/Resizable/ResizeHandle.tsx +70 -20
  442. package/src/SegmentedControl/SegmentedControl.test.tsx +28 -0
  443. package/src/SegmentedControl/SegmentedControlItem.tsx +22 -2
  444. package/src/Selector/Selector.doc.mjs +15 -0
  445. package/src/Selector/Selector.test.tsx +370 -0
  446. package/src/Selector/Selector.tsx +88 -20
  447. package/src/SideNav/SideNav.tsx +1 -8
  448. package/src/SideNav/SideNavCollapseButton.tsx +9 -7
  449. package/src/SideNav/SideNavItem.tsx +5 -3
  450. package/src/Skeleton/Skeleton.test.tsx +16 -0
  451. package/src/Skeleton/Skeleton.tsx +13 -1
  452. package/src/Slider/Slider.doc.mjs +6 -0
  453. package/src/Slider/Slider.test.tsx +240 -12
  454. package/src/Slider/Slider.tsx +96 -23
  455. package/src/Spinner/Spinner.test.tsx +10 -8
  456. package/src/Spinner/Spinner.tsx +15 -5
  457. package/src/Switch/Switch.doc.mjs +142 -42
  458. package/src/Switch/Switch.test.tsx +111 -0
  459. package/src/Switch/Switch.tsx +149 -32
  460. package/src/TabList/Tab.tsx +2 -2
  461. package/src/TabList/TabList.doc.mjs +0 -6
  462. package/src/TabList/TabList.test.tsx +3 -15
  463. package/src/TabList/TabList.tsx +6 -17
  464. package/src/TabList/TabListContext.ts +0 -6
  465. package/src/TabList/TabMenu.tsx +2 -2
  466. package/src/Table/BaseTable.tsx +34 -2
  467. package/src/Table/Table.doc.mjs +11 -0
  468. package/src/Table/Table.test.tsx +85 -0
  469. package/src/Table/TableCell.tsx +3 -3
  470. package/src/Table/TableHeaderCell.tsx +3 -3
  471. package/src/Table/plugins/groupedRows/useTableGroupedRows.tsx +9 -6
  472. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +19 -0
  473. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +27 -18
  474. package/src/Table/plugins/selection/useTableSelection.test.tsx +61 -3
  475. package/src/Table/plugins/selection/useTableSelection.tsx +3 -0
  476. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +38 -9
  477. package/src/Table/plugins/tree/useTableTreeData.tsx +17 -13
  478. package/src/Table/types.ts +33 -0
  479. package/src/Table/useTableRowStatus.doc.mjs +1 -1
  480. package/src/TextArea/TextArea.doc.mjs +21 -0
  481. package/src/TextArea/TextArea.test.tsx +23 -0
  482. package/src/TextArea/TextArea.tsx +27 -26
  483. package/src/TextInput/TextInput.doc.mjs +21 -0
  484. package/src/TextInput/TextInput.test.tsx +150 -0
  485. package/src/TextInput/TextInput.tsx +28 -25
  486. package/src/Thumbnail/Thumbnail.doc.mjs +1 -1
  487. package/src/Thumbnail/Thumbnail.tsx +1 -1
  488. package/src/TimeInput/TimeInput.doc.mjs +21 -0
  489. package/src/TimeInput/TimeInput.test.tsx +23 -0
  490. package/src/TimeInput/TimeInput.tsx +24 -25
  491. package/src/Timestamp/Timestamp.doc.mjs +32 -6
  492. package/src/Timestamp/Timestamp.test.tsx +371 -0
  493. package/src/Timestamp/Timestamp.tsx +96 -93
  494. package/src/Timestamp/formatInstant.ts +218 -0
  495. package/src/Timestamp/index.ts +4 -0
  496. package/src/Timestamp/tooltipEntries.test.ts +552 -0
  497. package/src/Timestamp/tooltipEntries.ts +210 -0
  498. package/src/Toast/ToastViewport.test.tsx +218 -8
  499. package/src/Toast/ToastViewport.tsx +71 -12
  500. package/src/ToggleButton/ToggleButton.test.tsx +24 -0
  501. package/src/ToggleButton/ToggleButton.tsx +19 -1
  502. package/src/Token/Token.test.tsx +166 -15
  503. package/src/Token/Token.tsx +104 -47
  504. package/src/Token/TokenLink.tsx +107 -0
  505. package/src/Token/index.ts +1 -1
  506. package/src/Tokenizer/Tokenizer.doc.mjs +23 -1
  507. package/src/Tokenizer/Tokenizer.test.tsx +23 -0
  508. package/src/Tokenizer/Tokenizer.tsx +14 -2
  509. package/src/Tooltip/Tooltip.test.tsx +46 -0
  510. package/src/Tooltip/useTooltip.tsx +20 -0
  511. package/src/TopNav/TopNav.test.tsx +93 -2
  512. package/src/TopNav/TopNavItem.doc.mjs +3 -3
  513. package/src/TopNav/TopNavItem.tsx +35 -6
  514. package/src/TopNav/TopNavMegaMenuFeaturedCard.doc.mjs +1 -1
  515. package/src/TreeList/TreeList.doc.mjs +6 -0
  516. package/src/TreeList/TreeList.test.tsx +50 -10
  517. package/src/TreeList/TreeList.tsx +5 -0
  518. package/src/TreeList/TreeListBranches.tsx +8 -6
  519. package/src/TreeList/TreeListItem.tsx +33 -11
  520. package/src/Typeahead/BaseTypeahead.tsx +42 -15
  521. package/src/Typeahead/Typeahead.doc.mjs +13 -0
  522. package/src/Typeahead/Typeahead.test.tsx +112 -0
  523. package/src/Typeahead/Typeahead.tsx +17 -1
  524. package/src/Typeahead/TypeaheadItem.tsx +2 -1
  525. package/src/__tests__/forcedColors.ts +60 -0
  526. package/src/__tests__/inputWidthContract.test.tsx +62 -0
  527. package/src/doc-mjs.d.ts +19 -0
  528. package/src/hooks/index.ts +6 -0
  529. package/src/hooks/isRtlElement.ts +27 -0
  530. package/src/hooks/useAnnounce.test.tsx +75 -2
  531. package/src/hooks/useAnnounce.ts +47 -1
  532. package/src/hooks/useFocusTrap.test.tsx +104 -0
  533. package/src/hooks/useFocusTrap.ts +61 -12
  534. package/src/hooks/useGridFocus.doc.mjs +3 -3
  535. package/src/hooks/useGridFocus.test.tsx +41 -0
  536. package/src/hooks/useGridFocus.ts +21 -10
  537. package/src/hooks/useHotkeys.ts +9 -0
  538. package/src/hooks/useInputStatusIcon.test.tsx +162 -0
  539. package/src/hooks/useInputStatusIcon.tsx +224 -0
  540. package/src/hooks/useKeyboardHint.test.tsx +22 -0
  541. package/src/hooks/useKeyboardHint.tsx +6 -2
  542. package/src/hooks/useListFocus.doc.mjs +25 -3
  543. package/src/hooks/useListFocus.test.tsx +153 -1
  544. package/src/hooks/useListFocus.ts +102 -9
  545. package/src/i18n/InternationalizationContext.ts +4 -0
  546. package/src/i18n/InternationalizationProvider.tsx +12 -2
  547. package/src/i18n/__tests__/getLocaleDirection.test.ts +40 -0
  548. package/src/i18n/__tests__/useDirection.test.tsx +66 -0
  549. package/src/i18n/getLocaleDirection.ts +29 -0
  550. package/src/i18n/index.ts +6 -0
  551. package/src/i18n/useDirection.ts +29 -0
  552. package/src/reset.css +0 -26
  553. package/src/reset.test.ts +60 -0
  554. package/src/theme/derivedVarRegistry.test.ts +1 -0
  555. package/src/theme/derivedVarRegistry.ts +6 -0
  556. package/src/theme/generateThemeRules.test.ts +42 -0
  557. package/src/utils/index.ts +2 -0
  558. package/src/utils/plainDate.test.ts +24 -6
  559. package/src/utils/plainDate.ts +18 -4
  560. package/src/utils/rtlStyles.ts +27 -0
@@ -7,7 +7,9 @@
7
7
  * @input Uses React useCallback; DOM APIs for a singleton live-region pair
8
8
  * @output Exports useAnnounce hook and AnnouncePoliteness type
9
9
  * @position Core a11y hook; provides imperative screen-reader announcements
10
- * via persistently-mounted polite/assertive live regions
10
+ * via persistently-mounted polite/assertive live regions; regions are
11
+ * auto-cleared shortly after announcing so stale status text does not
12
+ * linger in the accessibility tree
11
13
  *
12
14
  * SYNC: When modified, update:
13
15
  * - /packages/core/src/hooks/index.ts
@@ -47,6 +49,40 @@ interface LiveRegions {
47
49
  assertive: HTMLElement;
48
50
  }
49
51
 
52
+ /**
53
+ * How long an announcement stays in its region before being auto-cleared.
54
+ * Long enough for screen readers to pick the message up and finish reading
55
+ * it; clearing afterwards keeps stale status text out of the accessibility
56
+ * tree for users who browse the DOM later. Each announce resets the timer.
57
+ */
58
+ const CLEAR_DELAY_MS = 2000;
59
+
60
+ const clearTimers: {
61
+ [K in AnnouncePoliteness]: ReturnType<typeof setTimeout> | null;
62
+ } = {
63
+ polite: null,
64
+ assertive: null,
65
+ };
66
+
67
+ function cancelScheduledClear(politeness: AnnouncePoliteness): void {
68
+ const timer = clearTimers[politeness];
69
+ if (timer != null) {
70
+ clearTimeout(timer);
71
+ clearTimers[politeness] = null;
72
+ }
73
+ }
74
+
75
+ function scheduleClear(
76
+ politeness: AnnouncePoliteness,
77
+ target: HTMLElement,
78
+ ): void {
79
+ cancelScheduledClear(politeness);
80
+ clearTimers[politeness] = setTimeout(() => {
81
+ clearTimers[politeness] = null;
82
+ target.textContent = '';
83
+ }, CLEAR_DELAY_MS);
84
+ }
85
+
50
86
  // Singleton: the live regions are created ONCE and stay mounted for the
51
87
  // lifetime of the document. This is the crux of reliable announcements — many
52
88
  // screen readers will not announce content injected into a live region that is
@@ -103,6 +139,10 @@ function announceMessage(
103
139
  requestAnimationFrame(() => {
104
140
  target.textContent = message;
105
141
  });
142
+ // Auto-clear so the last message does not linger in the accessibility tree
143
+ // indefinitely. Scheduled per announce, so a newer announcement always
144
+ // resets the countdown (the delay comfortably outlasts the rAF re-set).
145
+ scheduleClear(politeness, target);
106
146
  }
107
147
 
108
148
  /**
@@ -111,6 +151,7 @@ function announceMessage(
111
151
  * so stale status text does not linger in the accessibility tree.
112
152
  */
113
153
  function clearRegion(politeness: AnnouncePoliteness): void {
154
+ cancelScheduledClear(politeness);
114
155
  if (!regions) {
115
156
  return;
116
157
  }
@@ -126,6 +167,9 @@ function clearRegion(politeness: AnnouncePoliteness): void {
126
167
  * The polite and assertive regions are created once on first use and kept
127
168
  * mounted, so announcements are reliable even for messages that appear
128
169
  * immediately (unlike a live region rendered together with its content).
170
+ * Each message is automatically cleared a couple of seconds after being
171
+ * announced, so users browsing the DOM later do not encounter stale status
172
+ * text; announcing again before the clear simply resets the countdown.
129
173
  *
130
174
  * @example
131
175
  * ```
@@ -160,6 +204,8 @@ export function useAnnounce(): AnnounceFn {
160
204
  * @internal
161
205
  */
162
206
  export function __resetLiveRegionsForTest(): void {
207
+ cancelScheduledClear('polite');
208
+ cancelScheduledClear('assertive');
163
209
  if (regions) {
164
210
  regions.polite.remove();
165
211
  regions.assertive.remove();
@@ -50,6 +50,39 @@ function EscapeTrap({
50
50
  );
51
51
  }
52
52
 
53
+ function NestedInnerTrap({onEscape}: {onEscape: () => void}) {
54
+ const {containerRef} = useFocusTrap<HTMLDivElement>({
55
+ isActive: true,
56
+ onEscape,
57
+ });
58
+ return (
59
+ <div ref={containerRef} data-testid="nested-inner">
60
+ <button type="button">inner-btn</button>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ function NestedTraps({
66
+ onOuterEscape,
67
+ onInnerEscape,
68
+ showInner = true,
69
+ }: {
70
+ onOuterEscape: () => void;
71
+ onInnerEscape: () => void;
72
+ showInner?: boolean;
73
+ }) {
74
+ const {containerRef} = useFocusTrap<HTMLDivElement>({
75
+ isActive: true,
76
+ onEscape: onOuterEscape,
77
+ });
78
+ return (
79
+ <div ref={containerRef} data-testid="nested-outer">
80
+ <button type="button">outer-btn</button>
81
+ {showInner && <NestedInnerTrap onEscape={onInnerEscape} />}
82
+ </div>
83
+ );
84
+ }
85
+
53
86
  function RestoreTrap({isActive}: {isActive: boolean}) {
54
87
  const {containerRef} = useFocusTrap<HTMLDivElement>({isActive});
55
88
  return (
@@ -118,6 +151,47 @@ describe('useFocusTrap tabbable model (infra-8)', () => {
118
151
  // Focus skips the inert button and lands on the real one.
119
152
  expect(screen.getByTestId('real-btn')).toHaveFocus();
120
153
  });
154
+
155
+ it('ignores an aria-hidden subtree when finding focusables', () => {
156
+ render(
157
+ <Trap>
158
+ <div aria-hidden="true">
159
+ <button type="button" data-testid="aria-hidden-btn">
160
+ Hidden from AT
161
+ </button>
162
+ </div>
163
+ <button type="button" data-testid="real-btn">
164
+ Real
165
+ </button>
166
+ </Trap>,
167
+ );
168
+ fireEvent.click(screen.getByTestId('focus-first'));
169
+ // An element AT cannot perceive must not be a trap tab stop (WCAG 4.1.2).
170
+ expect(screen.getByTestId('real-btn')).toHaveFocus();
171
+ });
172
+
173
+ it('excludes an aria-hidden focusable from the Tab wrap boundary', () => {
174
+ render(
175
+ <Trap>
176
+ <button type="button" data-testid="first">
177
+ First
178
+ </button>
179
+ <button type="button" data-testid="visible-last">
180
+ Visible last
181
+ </button>
182
+ <div aria-hidden="true">
183
+ <button type="button" data-testid="aria-hidden-btn">
184
+ Hidden from AT
185
+ </button>
186
+ </div>
187
+ </Trap>,
188
+ );
189
+ // The last VISIBLE-to-AT element is the wrap boundary: Tab from it wraps
190
+ // to the first element instead of landing on the aria-hidden button.
191
+ screen.getByTestId('visible-last').focus();
192
+ fireEvent.keyDown(document, {key: 'Tab'});
193
+ expect(screen.getByTestId('first')).toHaveFocus();
194
+ });
121
195
  });
122
196
 
123
197
  describe('FOCUSABLE_SELECTOR href matching', () => {
@@ -198,6 +272,36 @@ describe('useFocusTrap Escape coordination', () => {
198
272
  expect(onEscape).toHaveBeenCalledTimes(1);
199
273
  });
200
274
 
275
+ it('resolves Escape to the DOM-nested inner trap when both mount in one commit', () => {
276
+ const outer = vi.fn();
277
+ const inner = vi.fn();
278
+ // Outer and inner mount in the SAME React commit. React runs child
279
+ // effects before parent effects, so the inner trap is PUSHED first —
280
+ // DOM containment, not push order, must decide who answers Escape.
281
+ render(<NestedTraps onOuterEscape={outer} onInnerEscape={inner} />);
282
+ fireEvent.keyDown(document, {key: 'Escape'});
283
+ expect(inner).toHaveBeenCalledTimes(1);
284
+ expect(outer).not.toHaveBeenCalled();
285
+ });
286
+
287
+ it('falls back to the outer trap once the nested inner trap unmounts', () => {
288
+ const outer = vi.fn();
289
+ const inner = vi.fn();
290
+ const {rerender} = render(
291
+ <NestedTraps onOuterEscape={outer} onInnerEscape={inner} />,
292
+ );
293
+ rerender(
294
+ <NestedTraps
295
+ onOuterEscape={outer}
296
+ onInnerEscape={inner}
297
+ showInner={false}
298
+ />,
299
+ );
300
+ fireEvent.keyDown(document, {key: 'Escape'});
301
+ expect(outer).toHaveBeenCalledTimes(1);
302
+ expect(inner).not.toHaveBeenCalled();
303
+ });
304
+
201
305
  it('does not respond after the trap is deactivated', () => {
202
306
  const onEscape = vi.fn();
203
307
  const {rerender} = render(
@@ -26,24 +26,62 @@ import {FOCUSABLE_SELECTOR} from './focusableSelector';
26
26
  * Every active `useFocusTrap` used to attach its own document-level `keydown`
27
27
  * listener with no coordination, so a single Escape press closed *every* open
28
28
  * layer at once (e.g. a popover nested inside a Dialog closed both). Tracking
29
- * traps in a shared stack lets only the most recently activated (top-most)
30
- * trap respond to Escape.
29
+ * traps in a shared stack lets only the top-most trap respond to Escape.
30
+ *
31
+ * "Top-most" is resolved by DOM containment first, push order second. Push
32
+ * order alone is not reliable: React runs child effects before parent
33
+ * effects, so when an outer and an inner (DOM-nested) trap mount in the SAME
34
+ * commit, the inner trap pushes first and the outer trap would wrongly win a
35
+ * pure last-pushed comparison.
31
36
  */
32
- const escapeStack: (() => void)[] = [];
37
+ interface EscapeStackEntry {
38
+ handler: () => void;
39
+ getContainer: () => HTMLElement | null;
40
+ }
33
41
 
34
- function pushEscapeHandler(handler: () => void): void {
35
- escapeStack.push(handler);
42
+ const escapeStack: EscapeStackEntry[] = [];
43
+
44
+ function pushEscapeHandler(entry: EscapeStackEntry): void {
45
+ escapeStack.push(entry);
36
46
  }
37
47
 
38
48
  function removeEscapeHandler(handler: () => void): void {
39
- const index = escapeStack.lastIndexOf(handler);
40
- if (index !== -1) {
41
- escapeStack.splice(index, 1);
49
+ for (let i = escapeStack.length - 1; i >= 0; i--) {
50
+ if (escapeStack[i].handler === handler) {
51
+ escapeStack.splice(i, 1);
52
+ return;
53
+ }
42
54
  }
43
55
  }
44
56
 
57
+ /**
58
+ * Resolve the top-most trap: walk the stack in push order, keeping the
59
+ * deepest container by DOM containment. When a later entry's container
60
+ * contains the current candidate's container, the candidate is nested inside
61
+ * it and stays on top; otherwise the later push wins (containment for nested
62
+ * traps, push order as the tiebreaker for unrelated ones).
63
+ */
45
64
  function isTopEscapeHandler(handler: () => void): boolean {
46
- return escapeStack[escapeStack.length - 1] === handler;
65
+ if (escapeStack.length === 0) {
66
+ return false;
67
+ }
68
+ let top = escapeStack[0];
69
+ for (let i = 1; i < escapeStack.length; i++) {
70
+ const entry = escapeStack[i];
71
+ const topContainer = top.getContainer();
72
+ const entryContainer = entry.getContainer();
73
+ if (
74
+ topContainer != null &&
75
+ entryContainer != null &&
76
+ entryContainer !== topContainer &&
77
+ entryContainer.contains(topContainer)
78
+ ) {
79
+ // The current top is nested inside this entry — it stays on top.
80
+ continue;
81
+ }
82
+ top = entry;
83
+ }
84
+ return top.handler === handler;
47
85
  }
48
86
 
49
87
  /**
@@ -73,7 +111,9 @@ export function isImeKeyEvent(event: {
73
111
  /**
74
112
  * Whether an element is currently perceivable/focusable — excludes ones hidden
75
113
  * via `display:none`/`visibility:hidden` or inside an `inert`/`hidden` subtree,
76
- * which the browser skips for Tab.
114
+ * which the browser skips for Tab, and ones inside an `aria-hidden="true"`
115
+ * subtree, which sighted-keyboard users could Tab to while AT skips them
116
+ * (WCAG 4.1.2 — focusable content must be exposed to assistive tech).
77
117
  */
78
118
  function isVisiblyFocusable(el: HTMLElement): boolean {
79
119
  if (el.hasAttribute('inert') || el.closest('[inert]')) {
@@ -82,6 +122,10 @@ function isVisiblyFocusable(el: HTMLElement): boolean {
82
122
  if (el.hidden || el.closest('[hidden]')) {
83
123
  return false;
84
124
  }
125
+ // closest() matches the element itself as well as any ancestor.
126
+ if (el.closest('[aria-hidden="true"]')) {
127
+ return false;
128
+ }
85
129
  // offsetParent is null for display:none (and fixed elements); pair with a
86
130
  // visibility check via getComputedStyle when available.
87
131
  if (typeof window !== 'undefined' && window.getComputedStyle) {
@@ -323,12 +367,17 @@ export function useFocusTrap<T extends HTMLElement = HTMLElement>(
323
367
 
324
368
  // Register this trap on the shared Escape stack so only the top-most
325
369
  // active trap responds to Escape. A stable identity per active period is
326
- // enough — we push on activate and remove on cleanup.
370
+ // enough — we push on activate and remove on cleanup. The container is
371
+ // read lazily: the ref may not be attached yet at effect time, and the
372
+ // stack resolves top-most by DOM containment at keydown time.
327
373
  const escapeHandler = () => {
328
374
  onEscape?.();
329
375
  };
330
376
  if (onEscape) {
331
- pushEscapeHandler(escapeHandler);
377
+ pushEscapeHandler({
378
+ handler: escapeHandler,
379
+ getContainer: () => containerRef.current,
380
+ });
332
381
  }
333
382
 
334
383
  const handleKeyDown = (event: KeyboardEvent) => {
@@ -64,8 +64,8 @@ export const docs = {
64
64
  {
65
65
  name: 'options.isRtl',
66
66
  type: 'boolean',
67
- description: 'Swap ArrowLeft/ArrowRight so horizontal navigation follows visual direction in right-to-left contexts.',
68
- default: 'false',
67
+ description: 'Swap ArrowLeft/ArrowRight so horizontal navigation follows visual direction in right-to-left contexts. When omitted, auto-detected from the container computed direction on keydown.',
68
+ default: "undefined (auto-detect from the container's computed direction)",
69
69
  required: false,
70
70
  },
71
71
  {
@@ -138,7 +138,7 @@ export const docsDense = {
138
138
  'options.onNavigateAfter': 'callback when navigation would go after last cell. Receives column index + offset.',
139
139
  'options.onPageUp': 'callback for Page Up key (e.g. navigate to previous month in calendars).',
140
140
  'options.onPageDown': 'callback for Page Down key (e.g. navigate to next month in calendars).',
141
- 'options.isRtl': 'swap ArrowLeft/ArrowRight for RTL horizontal navigation. default false.',
141
+ 'options.isRtl': 'swap ArrowLeft/ArrowRight for RTL horizontal navigation. default: auto-detect from container computed direction; explicit boolean wins.',
142
142
  'options.hasRovingTabIndex': 'own a single roving tab stop across the grid (one focus target tabindex=0, rest -1); repaired on render + moved with arrows. Attach handleFocus to onFocus. default false.',
143
143
  },
144
144
  returnDescriptions: {
@@ -23,10 +23,12 @@ const NO_DISABLED: number[] = [];
23
23
  function Grid({
24
24
  disabled = NO_DISABLED,
25
25
  seed = 0,
26
+ dir,
26
27
  ...opts
27
28
  }: {
28
29
  disabled?: number[];
29
30
  seed?: number;
31
+ dir?: 'ltr' | 'rtl';
30
32
  } & Partial<Parameters<typeof useGridFocus>[0]>) {
31
33
  const {gridRef, handleKeyDown, handleFocus} = useGridFocus<HTMLDivElement>({
32
34
  columns: 3,
@@ -41,6 +43,7 @@ function Grid({
41
43
  <div
42
44
  ref={gridRef}
43
45
  role="grid"
46
+ dir={dir}
44
47
  onKeyDown={handleKeyDown}
45
48
  onFocus={handleFocus}>
46
49
  {Array.from({length: 9}, (_, i) => (
@@ -130,3 +133,41 @@ describe('useGridFocus roving tabindex (hasRovingTabIndex)', () => {
130
133
  expect(screen.getByTestId('cell-1')).toHaveAttribute('tabindex', '-1');
131
134
  });
132
135
  });
136
+
137
+ describe('useGridFocus RTL auto-detection (WCAG 1.3.2)', () => {
138
+ // jsdom reflects the `dir` attribute into computed style only on the element
139
+ // that carries it, so tests set dir="rtl" on the grid container itself — the
140
+ // element the hook reads via gridRef.
141
+
142
+ it('auto-detects dir="rtl": ArrowLeft moves forward (next cell)', () => {
143
+ render(<Grid seed={1} dir="rtl" />);
144
+ const grid = screen.getByRole('grid');
145
+ screen.getByTestId('cell-1').focus();
146
+ fireEvent.keyDown(grid, {key: 'ArrowLeft'});
147
+ expect(screen.getByTestId('cell-2')).toHaveFocus();
148
+ });
149
+
150
+ it('auto-detects dir="rtl": ArrowRight moves backward (previous cell)', () => {
151
+ render(<Grid seed={1} dir="rtl" />);
152
+ const grid = screen.getByRole('grid');
153
+ screen.getByTestId('cell-1').focus();
154
+ fireEvent.keyDown(grid, {key: 'ArrowRight'});
155
+ expect(screen.getByTestId('cell-0')).toHaveFocus();
156
+ });
157
+
158
+ it('leaves vertical navigation unaffected under dir="rtl"', () => {
159
+ render(<Grid seed={0} dir="rtl" />);
160
+ const grid = screen.getByRole('grid');
161
+ screen.getByTestId('cell-0').focus();
162
+ fireEvent.keyDown(grid, {key: 'ArrowDown'});
163
+ expect(screen.getByTestId('cell-3')).toHaveFocus();
164
+ });
165
+
166
+ it('explicit isRtl={false} overrides a dir="rtl" container', () => {
167
+ render(<Grid seed={1} dir="rtl" isRtl={false} />);
168
+ const grid = screen.getByRole('grid');
169
+ screen.getByTestId('cell-1').focus();
170
+ fireEvent.keyDown(grid, {key: 'ArrowRight'});
171
+ expect(screen.getByTestId('cell-2')).toHaveFocus();
172
+ });
173
+ });
@@ -4,9 +4,12 @@
4
4
 
5
5
  /**
6
6
  * @file useGridFocus.ts
7
- * @input Uses React useCallback, useRef, useIsomorphicLayoutEffect
7
+ * @input Uses React useCallback, useRef, useIsomorphicLayoutEffect,
8
+ * isRtlElement
8
9
  * @output Exports useGridFocus hook for grid keyboard navigation
9
- * @position Core hook; used by Calendar for date grid navigation
10
+ * @position Core hook; used by Calendar for date grid navigation. Auto-detects
11
+ * RTL from the container's computed direction so horizontal arrow keys
12
+ * follow visual direction (WCAG 1.3.2).
10
13
  *
11
14
  * SYNC: When modified, update:
12
15
  * - /packages/core/src/hooks/index.ts
@@ -14,6 +17,7 @@
14
17
  */
15
18
 
16
19
  import {useCallback, useRef} from 'react';
20
+ import {isRtlElement} from './isRtlElement';
17
21
  import {useIsomorphicLayoutEffect} from './useIsomorphicLayoutEffect';
18
22
 
19
23
  /**
@@ -88,7 +92,12 @@ export interface UseGridFocusOptions {
88
92
  /**
89
93
  * Whether the grid is in a right-to-left context. When true, ArrowLeft and
90
94
  * ArrowRight are swapped so horizontal navigation follows visual direction.
91
- * @default false
95
+ *
96
+ * When omitted, the direction is auto-detected from the container's computed
97
+ * `direction` (read lazily on keydown, only for horizontal arrow keys), so
98
+ * grids inside `dir="rtl"` subtrees flip automatically. Pass an explicit
99
+ * boolean to override detection.
100
+ * @default undefined (auto-detect from the container)
92
101
  */
93
102
  isRtl?: boolean;
94
103
 
@@ -217,7 +226,7 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
217
226
  onNavigateAfter,
218
227
  onPageUp,
219
228
  onPageDown,
220
- isRtl = false,
229
+ isRtl,
221
230
  hasRovingTabIndex = false,
222
231
  } = options;
223
232
 
@@ -455,13 +464,15 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
455
464
 
456
465
  // In RTL, ArrowLeft/ArrowRight are swapped so horizontal navigation
457
466
  // follows visual direction. Vertical keys (Up/Down) are unaffected.
467
+ // Direction is resolved lazily — getComputedStyle runs only when a
468
+ // horizontal arrow key is actually pressed (SSR-safe, no layout thrash
469
+ // on unrelated keys) — and an explicit `isRtl` always wins.
458
470
  let key = e.key;
459
- if (isRtl) {
460
- if (key === 'ArrowLeft') {
461
- key = 'ArrowRight';
462
- } else if (key === 'ArrowRight') {
463
- key = 'ArrowLeft';
464
- }
471
+ if (
472
+ (key === 'ArrowLeft' || key === 'ArrowRight') &&
473
+ (isRtl ?? isRtlElement(gridRef.current))
474
+ ) {
475
+ key = key === 'ArrowLeft' ? 'ArrowRight' : 'ArrowLeft';
465
476
  }
466
477
 
467
478
  switch (key) {
@@ -155,6 +155,15 @@ function matchesCombo(
155
155
  * definitions live in a ref, so re-renders update behavior without
156
156
  * re-subscribing. First matching hotkey wins per event.
157
157
  *
158
+ * Accessibility (WCAG 2.1.4 — Character Key Shortcuts): a shortcut whose
159
+ * combo is a single unmodified character (letter, digit, punctuation or
160
+ * symbol — e.g. 'c' or '/') is easily triggered by accident by speech-input
161
+ * users and keyboard users with motor impairments. If you register one, you
162
+ * MUST also provide at least one of: (a) a way to turn the shortcut off,
163
+ * (b) a way to remap it to include a modifier, or (c) scope it so it is only
164
+ * active while the relevant component has focus. Prefer `mod`-based combos
165
+ * where possible; the `isDisabled` flag can back a user-facing off switch.
166
+ *
158
167
  * @param hotkeys - Shortcut registrations to listen for.
159
168
  *
160
169
  * @example
@@ -0,0 +1,162 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, it, expect} from 'vitest';
4
+ import type {ReactElement} from 'react';
5
+ import {render, screen} from '@testing-library/react';
6
+ import {TextInput} from '../TextInput/TextInput';
7
+ import {TextArea} from '../TextArea/TextArea';
8
+ import {NumberInput} from '../NumberInput/NumberInput';
9
+ import {DateInput} from '../DateInput/DateInput';
10
+ import {DateRangeInput} from '../DateRangeInput/DateRangeInput';
11
+ import {TimeInput} from '../TimeInput/TimeInput';
12
+ import {FileInput} from '../FileInput/FileInput';
13
+ import type {FieldStatusVariant} from '../FieldStatus/FieldStatus';
14
+
15
+ // Every id referenced by an aria-describedby must resolve to an element in the
16
+ // document (WCAG 1.3.1) — a described-by pointing at a non-rendered node is a
17
+ // dangling reference. The tooltip variant is the interesting case: it swaps the
18
+ // message box for a tooltip layer, so the referenced ids change per variant.
19
+ function expectNoDanglingDescribedBy(root: HTMLElement) {
20
+ for (const el of Array.from(root.querySelectorAll('[aria-describedby]'))) {
21
+ const value = el.getAttribute('aria-describedby') ?? '';
22
+ for (const id of value.split(/\s+/).filter(Boolean)) {
23
+ expect(
24
+ document.getElementById(id),
25
+ `dangling aria-describedby id "${id}" on <${el.tagName.toLowerCase()}>`,
26
+ ).not.toBeNull();
27
+ }
28
+ }
29
+ }
30
+
31
+ const VARIANTS: FieldStatusVariant[] = ['attached', 'detached', 'tooltip'];
32
+
33
+ const INPUTS: [string, (variant: FieldStatusVariant) => ReactElement][] = [
34
+ [
35
+ 'TextInput',
36
+ v => (
37
+ <TextInput
38
+ label="Field"
39
+ value=""
40
+ onChange={() => {}}
41
+ status={{type: 'error', message: 'Message'}}
42
+ statusVariant={v}
43
+ />
44
+ ),
45
+ ],
46
+ [
47
+ 'TextArea',
48
+ v => (
49
+ <TextArea
50
+ label="Field"
51
+ value=""
52
+ onChange={() => {}}
53
+ status={{type: 'error', message: 'Message'}}
54
+ statusVariant={v}
55
+ />
56
+ ),
57
+ ],
58
+ [
59
+ 'NumberInput',
60
+ v => (
61
+ <NumberInput
62
+ label="Field"
63
+ value={undefined}
64
+ onChange={() => {}}
65
+ status={{type: 'error', message: 'Message'}}
66
+ statusVariant={v}
67
+ />
68
+ ),
69
+ ],
70
+ [
71
+ 'DateInput',
72
+ v => (
73
+ <DateInput
74
+ label="Field"
75
+ value={undefined}
76
+ onChange={() => {}}
77
+ status={{type: 'error', message: 'Message'}}
78
+ statusVariant={v}
79
+ />
80
+ ),
81
+ ],
82
+ [
83
+ 'DateRangeInput',
84
+ v => (
85
+ <DateRangeInput
86
+ label="Field"
87
+ value={null}
88
+ onChange={() => {}}
89
+ status={{type: 'error', message: 'Message'}}
90
+ statusVariant={v}
91
+ />
92
+ ),
93
+ ],
94
+ [
95
+ 'TimeInput',
96
+ v => (
97
+ <TimeInput
98
+ label="Field"
99
+ value={undefined}
100
+ onChange={() => {}}
101
+ status={{type: 'error', message: 'Message'}}
102
+ statusVariant={v}
103
+ />
104
+ ),
105
+ ],
106
+ [
107
+ 'FileInput',
108
+ v => (
109
+ <FileInput
110
+ label="Field"
111
+ value={null}
112
+ onChange={() => {}}
113
+ status={{type: 'error', message: 'Message'}}
114
+ statusVariant={v}
115
+ />
116
+ ),
117
+ ],
118
+ ];
119
+
120
+ describe('useInputStatusIcon — no dangling aria-describedby (WCAG 1.3.1)', () => {
121
+ for (const [name, make] of INPUTS) {
122
+ for (const variant of VARIANTS) {
123
+ it(`${name} / statusVariant="${variant}"`, () => {
124
+ const {container} = render(make(variant));
125
+ expectNoDanglingDescribedBy(container);
126
+ });
127
+ }
128
+ }
129
+
130
+ it('tooltip variant links the input to the rendered tooltip element', () => {
131
+ render(
132
+ <TextInput
133
+ label="Field"
134
+ value=""
135
+ onChange={() => {}}
136
+ status={{type: 'error', message: 'Message'}}
137
+ statusVariant="tooltip"
138
+ />,
139
+ );
140
+ const input = screen.getByRole('textbox');
141
+ const tooltip = screen.getByRole('tooltip', {hidden: true});
142
+ expect(input.getAttribute('aria-describedby')).toContain(tooltip.id);
143
+ });
144
+
145
+ it('does not render a tooltip (or reference one) when there is no message', () => {
146
+ // No message → the tooltip variant renders no tooltip layer and no
147
+ // described-by should point at a non-existent status element.
148
+ const {container} = render(
149
+ <TextInput
150
+ label="Field"
151
+ value=""
152
+ onChange={() => {}}
153
+ status={{type: 'error'}}
154
+ statusVariant="tooltip"
155
+ />,
156
+ );
157
+ expect(
158
+ screen.queryByRole('tooltip', {hidden: true}),
159
+ ).not.toBeInTheDocument();
160
+ expectNoDanglingDescribedBy(container);
161
+ });
162
+ });