@astryxdesign/core 0.7.5 → 0.7.6-canary.e482998

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 (409) hide show
  1. package/dist/AlertDialog/AlertDialog.d.ts +1 -1
  2. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  3. package/dist/AlertDialog/AlertDialog.js +5 -5
  4. package/dist/Avatar/Avatar.d.ts.map +1 -1
  5. package/dist/Avatar/Avatar.js +12 -3
  6. package/dist/Badge/Badge.d.ts.map +1 -1
  7. package/dist/Badge/Badge.js +7 -1
  8. package/dist/BottomSheet/BottomSheet.js +1 -1
  9. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  10. package/dist/BottomSheet/BottomSheetPanel.js +2 -1
  11. package/dist/BottomSheet/useSheetGestures.d.ts +12 -0
  12. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  13. package/dist/BottomSheet/useSheetGestures.js +30 -3
  14. package/dist/Button/Button.js +4 -4
  15. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  16. package/dist/CheckboxInput/CheckboxInput.js +6 -5
  17. package/dist/DateInput/DateInput.d.ts.map +1 -1
  18. package/dist/DateInput/DateInput.js +12 -4
  19. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  20. package/dist/DateInput/MonthScroller.js +101 -15
  21. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  22. package/dist/DateInput/TouchDateField.js +8 -3
  23. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  24. package/dist/DateRangeInput/DateRangeInput.js +1 -1
  25. package/dist/DateTimeInput/DateTimeInput.d.ts +5 -2
  26. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  27. package/dist/DateTimeInput/DateTimeInput.js +23 -12
  28. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
  29. package/dist/DateTimeInput/NativeTimeSegment.js +5 -4
  30. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  31. package/dist/DateTimeInput/TouchDateTimeField.js +4 -3
  32. package/dist/Dialog/Dialog.d.ts.map +1 -1
  33. package/dist/Dialog/Dialog.js +34 -9
  34. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  35. package/dist/Dialog/DialogHeader.js +5 -4
  36. package/dist/Divider/Divider.d.ts.map +1 -1
  37. package/dist/Divider/Divider.js +2 -1
  38. package/dist/Drawer/Drawer.d.ts +1 -0
  39. package/dist/Drawer/Drawer.d.ts.map +1 -1
  40. package/dist/Drawer/Drawer.js +7 -2
  41. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  42. package/dist/DropdownMenu/DropdownMenu.js +4 -1
  43. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
  44. package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
  45. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +10 -1
  46. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
  47. package/dist/DropdownMenu/lazyMenuBottomSheet.js +16 -2
  48. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  49. package/dist/DropdownMenu/renderDropdownItems.js +21 -16
  50. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  51. package/dist/EmptyState/EmptyState.js +2 -1
  52. package/dist/Field/FieldLabel.d.ts +1 -1
  53. package/dist/Field/FieldLabel.d.ts.map +1 -1
  54. package/dist/Field/FieldLabel.js +4 -4
  55. package/dist/Field/InputClearButton.js +3 -3
  56. package/dist/Field/PanelSearchInput.js +2 -2
  57. package/dist/FieldStatus/FieldStatus.js +2 -2
  58. package/dist/FileInput/FileInput.d.ts.map +1 -1
  59. package/dist/FileInput/FileInput.js +7 -1
  60. package/dist/Heading/Heading.js +1 -1
  61. package/dist/Icon/Icon.js +1 -1
  62. package/dist/Indicator/CheckIndicator.d.ts.map +1 -1
  63. package/dist/Indicator/CheckIndicator.js +2 -1
  64. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
  65. package/dist/Indicator/CheckboxIndicator.js +3 -1
  66. package/dist/Indicator/RadioIndicator.d.ts.map +1 -1
  67. package/dist/Indicator/RadioIndicator.js +3 -1
  68. package/dist/Item/Item.d.ts.map +1 -1
  69. package/dist/Item/Item.js +2 -1
  70. package/dist/Item/ItemSwipeLayer.js +1 -1
  71. package/dist/Layout/Layout.js +1 -1
  72. package/dist/Layout/LayoutContent.js +1 -1
  73. package/dist/Layout/LayoutFooter.js +1 -1
  74. package/dist/Layout/LayoutHeader.js +1 -1
  75. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  76. package/dist/Lightbox/Lightbox.js +5 -0
  77. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +5 -4
  78. package/dist/List/List.js +1 -1
  79. package/dist/List/ListItem.js +3 -3
  80. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  81. package/dist/MobileNav/MobileNav.js +7 -2
  82. package/dist/MultiSelector/MultiSelector.d.ts +1 -1
  83. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  84. package/dist/MultiSelector/MultiSelector.js +5 -2
  85. package/dist/NumberInput/NumberInput.d.ts +3 -1
  86. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  87. package/dist/NumberInput/NumberInput.js +3 -3
  88. package/dist/NumberInput/numberParser.d.ts +3 -0
  89. package/dist/NumberInput/numberParser.d.ts.map +1 -1
  90. package/dist/NumberInput/numberParser.js +7 -44
  91. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  92. package/dist/ProgressBar/ProgressBar.js +16 -12
  93. package/dist/Resizable/useResizable.d.ts.map +1 -1
  94. package/dist/Resizable/useResizable.js +28 -2
  95. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  96. package/dist/SegmentedControl/SegmentedControlItem.js +4 -1
  97. package/dist/Selector/useSelectorPresentation.d.ts +8 -1
  98. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  99. package/dist/Selector/useSelectorPresentation.js +20 -4
  100. package/dist/Slider/Slider.d.ts.map +1 -1
  101. package/dist/Slider/Slider.js +7 -1
  102. package/dist/Spinner/Spinner.js +2 -2
  103. package/dist/Stack/Stack.js +1 -1
  104. package/dist/Stack/StackItem.js +1 -1
  105. package/dist/TabList/TabList.d.ts.map +1 -1
  106. package/dist/TabList/TabList.js +16 -1
  107. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  108. package/dist/Table/plugins/columnResize/useTableColumnResize.js +9 -1
  109. package/dist/Text/Text.js +1 -1
  110. package/dist/Text/useTruncation.d.ts.map +1 -1
  111. package/dist/Text/useTruncation.js +23 -5
  112. package/dist/TextArea/TextArea.d.ts +1 -1
  113. package/dist/TextArea/TextArea.d.ts.map +1 -1
  114. package/dist/TextArea/TextArea.js +13 -2
  115. package/dist/TextInput/TextInput.d.ts.map +1 -1
  116. package/dist/TextInput/TextInput.js +3 -3
  117. package/dist/TimeInput/TimeInput.d.ts +6 -2
  118. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  119. package/dist/TimeInput/TimeInput.js +21 -14
  120. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  121. package/dist/TimeInput/TouchTimeField.js +5 -4
  122. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  123. package/dist/Timestamp/Timestamp.js +2 -2
  124. package/dist/Timestamp/TimestampHoverCard.js +4 -4
  125. package/dist/Toast/Toast.d.ts.map +1 -1
  126. package/dist/Toast/Toast.js +60 -20
  127. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  128. package/dist/Toast/ToastViewport.js +82 -51
  129. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  130. package/dist/Tokenizer/Tokenizer.js +13 -2
  131. package/dist/Tokenizer/TokenizerBottomSheet.js +3 -3
  132. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  133. package/dist/Toolbar/Toolbar.js +2 -1
  134. package/dist/Typeahead/Typeahead.d.ts +1 -1
  135. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  136. package/dist/Typeahead/Typeahead.js +9 -1
  137. package/dist/astryx.css +15 -1
  138. package/dist/gestures/PinchGestureRecognizer.d.ts +30 -0
  139. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
  140. package/dist/gestures/PinchGestureRecognizer.js +125 -0
  141. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +70 -0
  142. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
  143. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +220 -0
  144. package/dist/gestures/SwipeGestureRecognizer.d.ts +36 -0
  145. package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
  146. package/dist/gestures/SwipeGestureRecognizer.js +145 -0
  147. package/dist/gestures/VelocityTracker.d.ts +6 -2
  148. package/dist/gestures/VelocityTracker.d.ts.map +1 -1
  149. package/dist/gestures/VelocityTracker.js +7 -3
  150. package/dist/gestures/types.d.ts +11 -7
  151. package/dist/gestures/types.d.ts.map +1 -1
  152. package/dist/gestures/types.js +22 -2
  153. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
  154. package/dist/hooks/useAdaptivePresentation.js +4 -1
  155. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  156. package/dist/hooks/useMenuHover.js +12 -1
  157. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  158. package/dist/i18n/generated-locales/en.generated.js +3 -0
  159. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  160. package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
  161. package/dist/motion/animator.d.ts +7 -1
  162. package/dist/motion/animator.d.ts.map +1 -1
  163. package/dist/motion/animator.js +119 -27
  164. package/dist/theme/defineTheme.js +1 -1
  165. package/dist/theme/domainTokens/domainTokenDefaults.d.ts +3 -0
  166. package/dist/theme/domainTokens/domainTokenDefaults.d.ts.map +1 -0
  167. package/dist/theme/domainTokens/domainTokenDefaults.js +19 -0
  168. package/dist/theme/domainTokens/index.d.ts +5 -4
  169. package/dist/theme/domainTokens/index.d.ts.map +1 -1
  170. package/dist/theme/domainTokens/index.js +5 -9
  171. package/dist/utils/focusReturn.d.ts +17 -1
  172. package/dist/utils/focusReturn.d.ts.map +1 -1
  173. package/dist/utils/focusReturn.js +11 -4
  174. package/dist/utils/getNodeText.d.ts +8 -0
  175. package/dist/utils/getNodeText.d.ts.map +1 -0
  176. package/dist/utils/getNodeText.js +37 -0
  177. package/dist/utils/interactionModality.d.ts +11 -2
  178. package/dist/utils/interactionModality.d.ts.map +1 -1
  179. package/dist/utils/interactionModality.js +58 -5
  180. package/dist/utils/plainDate.d.ts +2 -0
  181. package/dist/utils/plainDate.d.ts.map +1 -1
  182. package/dist/utils/plainDate.js +46 -3
  183. package/dist/utils/presentation.d.ts +50 -0
  184. package/dist/utils/presentation.d.ts.map +1 -0
  185. package/dist/utils/presentation.js +125 -0
  186. package/dist/utils/timeFormat.d.ts +47 -0
  187. package/dist/utils/timeFormat.d.ts.map +1 -0
  188. package/dist/utils/timeFormat.js +240 -0
  189. package/dist/utils/touchPresentation.d.ts.map +1 -1
  190. package/dist/utils/touchPresentation.js +5 -3
  191. package/dist/utils/touchPresentation.stylex.d.ts +15 -0
  192. package/dist/utils/touchPresentation.stylex.d.ts.map +1 -0
  193. package/dist/utils/touchPresentation.stylex.js +33 -0
  194. package/dist/utils/unicodeDigits.d.ts +20 -0
  195. package/dist/utils/unicodeDigits.d.ts.map +1 -0
  196. package/dist/utils/unicodeDigits.js +70 -0
  197. package/locales/en.json +15 -3
  198. package/locales/pseudo.json +9 -0
  199. package/package.json +5 -4
  200. package/src/AlertDialog/AlertDialog.tsx +6 -5
  201. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +18 -6
  202. package/src/Avatar/Avatar.spec.md +22 -22
  203. package/src/Avatar/Avatar.tsx +18 -1
  204. package/src/Avatar/__tests__/AvatarForcedColors.a11y.chromium.spec.ts +126 -0
  205. package/src/Badge/Badge.doc.mjs +1 -1
  206. package/src/Badge/Badge.tsx +19 -2
  207. package/src/Badge/__tests__/BadgeLargerText.a11y.chromium.spec.ts +260 -0
  208. package/src/BottomSheet/BottomSheet.test.tsx +46 -0
  209. package/src/BottomSheet/BottomSheet.tsx +1 -1
  210. package/src/BottomSheet/BottomSheetPanel.tsx +2 -1
  211. package/src/BottomSheet/useSheetGestures.test.ts +52 -0
  212. package/src/BottomSheet/useSheetGestures.ts +37 -6
  213. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +18 -6
  214. package/src/Button/Button.tsx +4 -4
  215. package/src/Calendar/Calendar.spec.md +87 -48
  216. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +21 -7
  217. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +21 -7
  218. package/src/CheckboxInput/CheckboxInput.tsx +6 -5
  219. package/src/Collapsible/Collapsible.spec.md +2 -0
  220. package/src/Collapsible/CollapsibleGroup.spec.md +57 -29
  221. package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +18 -7
  222. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +18 -6
  223. package/src/ComplexSelector/ComplexSelector.test.tsx +37 -0
  224. package/src/ContextMenu/ContextMenu.test.tsx +35 -0
  225. package/src/DateInput/DateInput.test.tsx +115 -3
  226. package/src/DateInput/DateInput.tsx +29 -14
  227. package/src/DateInput/DateInputTouch.test.tsx +76 -0
  228. package/src/DateInput/MonthScroller.tsx +106 -20
  229. package/src/DateInput/MonthScrollerSettle.test.tsx +165 -0
  230. package/src/DateInput/TouchDateField.tsx +14 -3
  231. package/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts +312 -0
  232. package/src/DateRangeInput/DateRangeInput.test.tsx +46 -3
  233. package/src/DateRangeInput/DateRangeInput.tsx +16 -12
  234. package/src/DateTimeInput/DateTimeInput.doc.mjs +10 -8
  235. package/src/DateTimeInput/DateTimeInput.spec.md +75 -51
  236. package/src/DateTimeInput/DateTimeInput.test.tsx +81 -0
  237. package/src/DateTimeInput/DateTimeInput.tsx +46 -24
  238. package/src/DateTimeInput/NativeTimeSegment.tsx +4 -6
  239. package/src/DateTimeInput/TouchDateTimeField.tsx +3 -7
  240. package/src/Dialog/Dialog.test.tsx +216 -2
  241. package/src/Dialog/Dialog.tsx +42 -9
  242. package/src/Dialog/DialogHeader.tsx +5 -4
  243. package/src/Dialog/__tests__/DialogSecondDocument.a11y.browser.spec.ts +202 -0
  244. package/src/Divider/Divider.tsx +2 -1
  245. package/src/Drawer/Drawer.spec.md +111 -80
  246. package/src/Drawer/Drawer.test.tsx +35 -0
  247. package/src/Drawer/Drawer.tsx +13 -2
  248. package/src/Drawer/DrawerHeader.spec.md +181 -0
  249. package/src/Drawer/__tests__/DrawerNonModalPageScroll.a11y.chromium.spec.ts +267 -0
  250. package/src/DropdownMenu/DropdownMenu.test.tsx +41 -0
  251. package/src/DropdownMenu/DropdownMenu.tsx +7 -1
  252. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +34 -0
  253. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -5
  254. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +19 -7
  255. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +21 -9
  256. package/src/DropdownMenu/lazyMenuBottomSheet.test.ts +53 -0
  257. package/src/DropdownMenu/lazyMenuBottomSheet.ts +23 -6
  258. package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
  259. package/src/EmptyState/EmptyState.tsx +2 -1
  260. package/src/Field/FieldLabel.tsx +5 -4
  261. package/src/Field/InputClearButton.tsx +3 -3
  262. package/src/Field/PanelSearchInput.tsx +2 -2
  263. package/src/FieldStatus/FieldStatus.tsx +2 -2
  264. package/src/FileInput/FileInput.test.tsx +21 -0
  265. package/src/FileInput/FileInput.tsx +4 -1
  266. package/src/FormLayout/FormLayout.spec.md +7 -8
  267. package/src/Heading/Heading.tsx +1 -1
  268. package/src/Icon/Icon.tsx +1 -1
  269. package/src/Indicator/CheckIndicator.spec.md +47 -42
  270. package/src/Indicator/CheckIndicator.tsx +2 -1
  271. package/src/Indicator/CheckboxIndicator.spec.md +40 -37
  272. package/src/Indicator/CheckboxIndicator.tsx +3 -1
  273. package/src/Indicator/Indicator.spec.md +188 -0
  274. package/src/Indicator/RadioIndicator.tsx +3 -1
  275. package/src/Item/Item.tsx +2 -1
  276. package/src/Item/ItemSwipeLayer.tsx +1 -1
  277. package/src/Layout/Layout.spec.md +127 -131
  278. package/src/Layout/Layout.tsx +1 -1
  279. package/src/Layout/LayoutContent.spec.md +205 -0
  280. package/src/Layout/LayoutContent.tsx +1 -1
  281. package/src/Layout/LayoutFooter.spec.md +190 -0
  282. package/src/Layout/LayoutFooter.tsx +1 -1
  283. package/src/Layout/LayoutHeader.spec.md +193 -0
  284. package/src/Layout/LayoutHeader.tsx +1 -1
  285. package/src/Layout/LayoutPanel.spec.md +194 -0
  286. package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +18 -6
  287. package/src/Lightbox/Lightbox.test.tsx +18 -0
  288. package/src/Lightbox/Lightbox.tsx +5 -0
  289. package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +8 -4
  290. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +18 -8
  291. package/src/List/List.tsx +1 -1
  292. package/src/List/ListItem.spec.md +205 -0
  293. package/src/List/ListItem.tsx +3 -3
  294. package/src/MobileNav/MobileNav.test.tsx +15 -0
  295. package/src/MobileNav/MobileNav.tsx +9 -1
  296. package/src/MobileNav/__tests__/MobileNavHeader.a11y.chromium.spec.ts +89 -0
  297. package/src/MoreMenu/MoreMenu.test.tsx +11 -0
  298. package/src/MultiSelector/MultiSelector.spec.md +7 -4
  299. package/src/MultiSelector/MultiSelector.test.tsx +118 -1
  300. package/src/MultiSelector/MultiSelector.tsx +6 -2
  301. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +8 -1
  302. package/src/NavMenu/NavHeadingMenu.spec.md +1 -5
  303. package/src/NumberInput/NumberInput.doc.mjs +13 -7
  304. package/src/NumberInput/NumberInput.spec.md +59 -37
  305. package/src/NumberInput/NumberInput.tsx +13 -3
  306. package/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts +102 -0
  307. package/src/NumberInput/numberParser.ts +7 -45
  308. package/src/ProgressBar/ProgressBar.test.tsx +108 -32
  309. package/src/ProgressBar/ProgressBar.tsx +58 -10
  310. package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +21 -9
  311. package/src/ProgressBar/__tests__/ProgressBarMotion.a11y.chromium.spec.ts +831 -0
  312. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +21 -14
  313. package/src/Resizable/useResizable.test.ts +43 -0
  314. package/src/Resizable/useResizable.ts +32 -9
  315. package/src/SegmentedControl/SegmentedControl.test.tsx +84 -0
  316. package/src/SegmentedControl/SegmentedControlItem.tsx +11 -5
  317. package/src/Selector/Selector.test.tsx +90 -1
  318. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +8 -1
  319. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +8 -1
  320. package/src/Selector/useSelectorPresentation.ts +31 -3
  321. package/src/SideNav/SideNav.test.tsx +27 -0
  322. package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +176 -51
  323. package/src/Slider/Slider.test.tsx +12 -0
  324. package/src/Slider/Slider.tsx +4 -1
  325. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +19 -7
  326. package/src/Spinner/Spinner.tsx +2 -2
  327. package/src/Stack/Stack.tsx +1 -1
  328. package/src/Stack/StackItem.tsx +1 -1
  329. package/src/TabList/TabList.test.tsx +58 -4
  330. package/src/TabList/TabList.tsx +16 -1
  331. package/src/TabList/__tests__/TabPressPointer.interaction.browser.spec.ts +163 -0
  332. package/src/Table/__tests__/Table.a11y.chromium.spec.ts +18 -6
  333. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +22 -0
  334. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
  335. package/src/Text/Text.tsx +1 -1
  336. package/src/Text/useTruncation.test.tsx +128 -1
  337. package/src/Text/useTruncation.ts +23 -5
  338. package/src/TextArea/TextArea.spec.md +10 -13
  339. package/src/TextArea/TextArea.test.tsx +60 -0
  340. package/src/TextArea/TextArea.tsx +15 -1
  341. package/src/TextInput/TextInput.test.tsx +21 -0
  342. package/src/TextInput/TextInput.tsx +3 -1
  343. package/src/TimeInput/NativeTimeInput.test.tsx +17 -0
  344. package/src/TimeInput/Presentation.test.tsx +21 -0
  345. package/src/TimeInput/TimeInput.doc.mjs +8 -8
  346. package/src/TimeInput/TimeInput.spec.md +45 -48
  347. package/src/TimeInput/TimeInput.test.tsx +76 -0
  348. package/src/TimeInput/TimeInput.tsx +36 -18
  349. package/src/TimeInput/TouchTimeField.tsx +4 -6
  350. package/src/Timestamp/Timestamp.spec.md +6 -12
  351. package/src/Timestamp/Timestamp.test.tsx +30 -5
  352. package/src/Timestamp/Timestamp.tsx +6 -2
  353. package/src/Timestamp/TimestampHoverCard.tsx +4 -4
  354. package/src/Toast/Toast.spec.md +14 -20
  355. package/src/Toast/Toast.tsx +104 -36
  356. package/src/Toast/ToastViewport.test.tsx +719 -1
  357. package/src/Toast/ToastViewport.tsx +100 -50
  358. package/src/Tokenizer/Tokenizer.spec.md +5 -3
  359. package/src/Tokenizer/Tokenizer.test.tsx +68 -0
  360. package/src/Tokenizer/Tokenizer.tsx +14 -2
  361. package/src/Tokenizer/TokenizerBottomSheet.test.tsx +15 -4
  362. package/src/Tokenizer/TokenizerBottomSheet.tsx +3 -3
  363. package/src/Toolbar/Toolbar.spec.md +5 -6
  364. package/src/Toolbar/Toolbar.test.tsx +30 -0
  365. package/src/Toolbar/Toolbar.tsx +2 -1
  366. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +18 -6
  367. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +18 -7
  368. package/src/TopNav/TopNav.test.tsx +27 -0
  369. package/src/Typeahead/Typeahead.test.tsx +40 -0
  370. package/src/Typeahead/Typeahead.tsx +12 -1
  371. package/src/gestures/PinchGestureRecognizer.ts +144 -0
  372. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +252 -0
  373. package/src/gestures/SwipeGestureRecognizer.ts +193 -0
  374. package/src/gestures/VelocityTracker.ts +10 -3
  375. package/src/gestures/__tests__/PanGestureRecognizer.test.ts +4 -4
  376. package/src/gestures/__tests__/PinchGestureRecognizer.test.ts +74 -0
  377. package/src/gestures/__tests__/ScreenEdgePanGestureRecognizer.test.ts +498 -0
  378. package/src/gestures/__tests__/SwipeGestureRecognizer.test.ts +108 -0
  379. package/src/gestures/__tests__/arenaPinchSwipeEdge.test.ts +188 -0
  380. package/src/gestures/__tests__/harness.ts +21 -2
  381. package/src/gestures/types.ts +23 -9
  382. package/src/hooks/useAdaptivePresentation.ts +4 -1
  383. package/src/hooks/useMenuHover.ts +15 -1
  384. package/src/i18n/generated-locales/en.generated.ts +4 -0
  385. package/src/i18n/generated-locales/pseudo.generated.ts +4 -0
  386. package/src/motion/README.md +11 -1
  387. package/src/motion/animator.test.ts +151 -3
  388. package/src/motion/animator.ts +149 -33
  389. package/src/theme/defineTheme.ts +1 -1
  390. package/src/theme/derivedVarRegistry.test.ts +4 -0
  391. package/src/theme/domainTokens/domainTokenDefaults.ts +19 -0
  392. package/src/theme/domainTokens/index.ts +6 -10
  393. package/src/utils/focusReturn.ts +17 -6
  394. package/src/utils/getNodeText.test.ts +24 -0
  395. package/src/utils/getNodeText.ts +37 -0
  396. package/src/utils/interactionModality.test.ts +45 -1
  397. package/src/utils/interactionModality.ts +64 -5
  398. package/src/utils/plainDate.test.ts +90 -0
  399. package/src/utils/plainDate.ts +58 -3
  400. package/src/utils/presentation.test.ts +380 -0
  401. package/src/utils/presentation.ts +180 -0
  402. package/src/utils/pressPaintPriority.test.ts +80 -0
  403. package/src/utils/timeFormat.test.ts +204 -0
  404. package/src/utils/timeFormat.ts +314 -0
  405. package/src/utils/touchPresentation.stylex.ts +43 -0
  406. package/src/utils/touchPresentation.test.ts +140 -1
  407. package/src/utils/touchPresentation.ts +5 -3
  408. package/src/utils/unicodeDigits.test.ts +44 -0
  409. package/src/utils/unicodeDigits.ts +71 -0
@@ -15,7 +15,6 @@
15
15
  */
16
16
 
17
17
  import {
18
- isValidElement,
19
18
  useCallback,
20
19
  useEffect,
21
20
  useLayoutEffect,
@@ -28,6 +27,7 @@ import {createPortal} from 'react-dom';
28
27
  import * as stylex from '@stylexjs/stylex';
29
28
  import {spacingVars, durationVars, easeVars} from '../theme/tokens.stylex';
30
29
  import {mergeProps} from '../utils';
30
+ import {getNodeText} from '../utils/getNodeText';
31
31
  import {INTERACTIVE_SELECTORS} from '../hooks/useClickableContainer';
32
32
  import {useAnnounce} from '../hooks/useAnnounce';
33
33
  import {layerTextReset} from '../Layer/layerTextReset.stylex';
@@ -172,29 +172,6 @@ const styles = stylex.create({
172
172
  },
173
173
  });
174
174
 
175
- // Flatten a toast's rendered content (title, description, etc.) to the plain
176
- // text that should be spoken by a screen reader. Only text is announced —
177
- // interactive endContent is deliberately excluded (it is reachable via F6).
178
- function getNodeText(node: ReactNode): string {
179
- if (node == null || typeof node === 'boolean') {
180
- return '';
181
- }
182
- if (typeof node === 'string') {
183
- return node;
184
- }
185
- if (typeof node === 'number') {
186
- return String(node);
187
- }
188
- if (Array.isArray(node)) {
189
- return node.map(getNodeText).filter(Boolean).join(' ');
190
- }
191
- if (isValidElement(node)) {
192
- const {children} = node.props as {children?: ReactNode};
193
- return getNodeText(children);
194
- }
195
- return '';
196
- }
197
-
198
175
  export interface ToastViewportProps {
199
176
  position?: ToastPosition;
200
177
  /** Maximum number of visible toasts. @default 5 */
@@ -344,6 +321,19 @@ export function ToastViewport({
344
321
  const [exitingIds, setExitingIds] = useState<Set<string>>(new Set());
345
322
  const toastsRef = useRef(toasts);
346
323
  toastsRef.current = toasts;
324
+ // The list as last rendered: addToast moves toastsRef ahead of it within
325
+ // a batch, so this tells a rendered toast from one raised since.
326
+ const renderedToastsRef = useRef(toasts);
327
+ renderedToastsRef.current = toasts;
328
+ // Read by removeToast, which keeps one identity across renders.
329
+ const maxVisibleRef = useRef(maxVisible);
330
+ maxVisibleRef.current = maxVisible;
331
+ // Rendered entries a repeat has replaced in this batch: still found by a
332
+ // dismissal in the same handler, as before the swap. One replaced before it
333
+ // rendered was never shown and reports no onHide (Toast.spec.md FR3). A
334
+ // render starts a new batch.
335
+ const replacedRef = useRef<Map<string, ToastEntry>>(new Map());
336
+ replacedRef.current.clear();
347
337
 
348
338
  const viewportRef = useRef<HTMLDivElement>(null);
349
339
  // Toast ids whose exit has begun — guards onHide from double-firing (see
@@ -410,6 +400,10 @@ export function ToastViewport({
410
400
  const addToast = useCallback(
411
401
  (entry: ToastEntry) => {
412
402
  const {uniqueID, collisionBehavior = 'overwrite'} = entry.options;
403
+ // A toast holds its uniqueID until it unmounts, leaving included
404
+ // (Toast.spec.md FR8): an overwrite replaces it in place, and an ignored
405
+ // namesake is dropped.
406
+ const holdsUniqueID = (t: ToastEntry) => t.options.uniqueID === uniqueID;
413
407
  // Resolve an ignored collision synchronously against the committed list
414
408
  // so a suppressed toast is neither shown nor announced. The remaining
415
409
  // announce + setToasts run outside the setToasts updater, which React may
@@ -417,16 +411,46 @@ export function ToastViewport({
417
411
  if (
418
412
  uniqueID &&
419
413
  collisionBehavior === 'ignore' &&
420
- toastsRef.current.some(t => t.options.uniqueID === uniqueID)
414
+ toastsRef.current.some(holdsUniqueID)
421
415
  ) {
422
416
  return;
423
417
  }
418
+ // Only text is announced; interactive endContent is reachable via F6.
424
419
  const text = getNodeText(entry.options.body);
425
420
  if (text) {
426
421
  // Error toasts map to the assertive region (role="alert"); everything
427
422
  // else to the polite region (role="status") — mirrors Toast.tsx.
428
423
  announce(text, entry.options.type === 'error' ? 'assertive' : 'polite');
429
424
  }
425
+ // A leaving toast that a new one pushes out of the visible window can no
426
+ // longer finish its exit on screen, so it leaves the list now (it
427
+ // already reported onHide); otherwise its row would come back collapsed
428
+ // when the window reached it again, holding a slot and the landmark.
429
+ const committed = toastsRef.current;
430
+ const overwritten = uniqueID ? committed.find(holdsUniqueID) : undefined;
431
+ if (overwritten && renderedToastsRef.current.includes(overwritten)) {
432
+ replacedRef.current.set(overwritten.id, overwritten);
433
+ }
434
+ const projected = overwritten
435
+ ? committed.map(t => (t === overwritten ? entry : t))
436
+ : [...committed, entry];
437
+ // `maxVisible={0}` shows every toast (FR7): nothing leaves the window.
438
+ const firstVisible = maxVisible > 0 ? projected.length - maxVisible : 0;
439
+ const cutIds = new Set(
440
+ projected
441
+ .filter((t, i) => i < firstVisible && exitingIdsRef.current.has(t.id))
442
+ .map(t => t.id),
443
+ );
444
+ if (cutIds.size > 0) {
445
+ // Gone from every list, so a later dismissal reports nothing more.
446
+ for (const id of cutIds) {
447
+ exitingIdsRef.current.delete(id);
448
+ pendingEntriesRef.current.delete(id);
449
+ }
450
+ setExitingIds(prev => new Set([...prev].filter(id => !cutIds.has(id))));
451
+ }
452
+ // Later toasts in the same batch see this one before the next render.
453
+ toastsRef.current = projected.filter(t => !cutIds.has(t.id));
430
454
  const pending = pendingEntriesRef.current;
431
455
  if (uniqueID) {
432
456
  // An overwrite replaces a toast that has not rendered yet too.
@@ -442,17 +466,17 @@ export function ToastViewport({
442
466
  // dismissed. Its settled id is dropped by the prune below, which covers
443
467
  // every silent exit with one rule rather than one branch per path.
444
468
  setToasts(prev => {
445
- if (uniqueID) {
446
- const existing = prev.find(t => t.options.uniqueID === uniqueID);
447
- if (existing) {
448
- // An ignored collision already returned above; overwrite in place.
449
- return prev.map(t => (t.options.uniqueID === uniqueID ? entry : t));
450
- }
451
- }
452
- return [...prev, entry];
469
+ // The cut entries go first, then the choice the projection above made
470
+ // (by identity): a dismissal in the same handler may have marked the
471
+ // replaced namesake as leaving since, which must not turn the
472
+ // overwrite into an append. An ignored collision already returned.
473
+ const kept = prev.filter(t => !cutIds.has(t.id));
474
+ return overwritten && kept.includes(overwritten)
475
+ ? kept.map(t => (t === overwritten ? entry : t))
476
+ : [...kept, entry];
453
477
  });
454
478
  },
455
- [announce],
479
+ [announce, maxVisible],
456
480
  );
457
481
 
458
482
  const removeToast = useCallback((id: string, reason: ToastDismissReason) => {
@@ -465,6 +489,7 @@ export function ToastViewport({
465
489
  }
466
490
  const entry =
467
491
  toastsRef.current.find(t => t.id === id) ??
492
+ replacedRef.current.get(id) ??
468
493
  pendingEntriesRef.current.get(id);
469
494
  if (entry == null) {
470
495
  // Already gone: nothing to hide or report.
@@ -485,29 +510,54 @@ export function ToastViewport({
485
510
  dismissedNode.contains(active)
486
511
  ) {
487
512
  focusHandoffIdRef.current = id;
488
- // Pick the neighbor to receive focus while the DOM is still intact:
489
- // prefer the next toast, then the previous, else restore.
490
- const remaining = toastsRef.current.filter(t => t.id !== id);
491
- if (remaining.length > 0) {
492
- const dismissedIndex = toastsRef.current.findIndex(t => t.id === id);
493
- const next =
494
- toastsRef.current[dismissedIndex + 1] ??
495
- toastsRef.current[dismissedIndex - 1];
496
- pendingFocusRef.current = next ? next.id : 'restore';
497
- } else {
498
- pendingFocusRef.current = 'restore';
499
- }
513
+ // Pick the toast to receive focus while the DOM is still intact: the
514
+ // nearest one after it, then before it, that shows once it is gone
515
+ // (FR10), else restore. The list may already run ahead of the render
516
+ // within this handler: a toast a repeat replaced has left it, and the
517
+ // replacement holds its place, so the neighbors are taken around the
518
+ // replacement, which takes focus only when no neighbor shows.
519
+ const list = toastsRef.current;
520
+ const {uniqueID} = entry.options;
521
+ const ownIndex = list.findIndex(t => t.id === id);
522
+ const placeIndex =
523
+ ownIndex >= 0 || uniqueID == null
524
+ ? ownIndex
525
+ : list.findIndex(t => t.options.uniqueID === uniqueID);
526
+ const remaining = list.filter(t => t.id !== id);
527
+ const max = maxVisibleRef.current;
528
+ const shown = new Set(max > 0 ? remaining.slice(-max) : remaining);
529
+ const isShown = (t: ToastEntry) => shown.has(t);
530
+ const replacement = ownIndex < 0 ? list[placeIndex] : undefined;
531
+ const next =
532
+ placeIndex < 0
533
+ ? undefined
534
+ : (list.slice(placeIndex + 1).find(isShown) ??
535
+ list.slice(0, placeIndex).reverse().find(isShown) ??
536
+ (replacement != null && isShown(replacement)
537
+ ? replacement
538
+ : undefined));
539
+ pendingFocusRef.current = next ? next.id : 'restore';
500
540
  }
501
- if (dismissedNode == null) {
502
- // No row is mounted for it: it is hidden past `maxVisible`, or it has
503
- // not rendered yet. Nothing animates out, so it leaves the list now.
541
+ // A toast this handler has already pushed out of the visible window still
542
+ // has its row now, but loses it when the batch renders.
543
+ const index = toastsRef.current.findIndex(t => t.id === id);
544
+ const isPushedOut =
545
+ maxVisibleRef.current > 0 &&
546
+ index >= 0 &&
547
+ index < toastsRef.current.length - maxVisibleRef.current;
548
+ if (dismissedNode == null || isPushedOut) {
549
+ // No row is mounted for it, or none will be: it is hidden past
550
+ // `maxVisible`, or it has not rendered yet. Nothing animates out, so it
551
+ // leaves the list now.
504
552
  // Waiting for its row to finish exiting would keep it forever: when it
505
553
  // surfaced, its row would mount already collapsed, with no transition
506
554
  // left to end, and hold a visible slot as an empty row.
507
555
  // It leaves the committed list now too, so a second dismissal before
508
- // the next render finds nothing to report.
556
+ // the next render finds nothing to report (nor does a toast a repeat
557
+ // replaced in this batch).
509
558
  exitingIdsRef.current.delete(id);
510
559
  pendingEntriesRef.current.delete(id);
560
+ replacedRef.current.delete(id);
511
561
  toastsRef.current = toastsRef.current.filter(t => t.id !== id);
512
562
  setToasts(prev => prev.filter(t => t.id !== id));
513
563
  return;
@@ -371,9 +371,11 @@ presentation.
371
371
  once the confirmation has closed, since a message sent while the
372
372
  confirmation covers the sheet is not heard — `spec:AST-086/FR14`), the
373
373
  search's busy start, and how each search
374
- ended — the result count for a non-empty query, or the empty state's text
375
- when it shows — are announced once per occasion from inside the sheet
376
- through `useAnnounce`, which `spec:AST-086/FR14` routes into the persistent
374
+ ended — the result count for a non-empty query, or the empty state's text when
375
+ it shows, both from the search's own `useAnnounce({hasReplace: true})`
376
+ instance (`spec:AST-086/FR9`, FR13), so a newer one replaces one not yet
377
+ spoken — are announced once per occasion from inside the sheet through
378
+ `useAnnounce`, which `spec:AST-086/FR14` routes into the persistent
377
379
  polite container of the sheet's own dialog while it is open, because the
378
380
  shared page-body channel is not exposed behind a native modal; the
379
381
  container follows FR3, FR5, and FR6 of that record (WCAG 2.2 SC 4.1.3
@@ -723,6 +723,54 @@ describe('Tokenizer', () => {
723
723
  bootstrap: () => [],
724
724
  };
725
725
 
726
+ it('labels the inline create row from the catalog, following the locale', async () => {
727
+ const overrides = {
728
+ de: {'@astryx.tokenizer.create': '„{value}“ erstellen'},
729
+ };
730
+ const {rerender} = render(
731
+ <InternationalizationProvider locale="de" overrides={overrides}>
732
+ <Tokenizer
733
+ label="Obst"
734
+ searchSource={emptySource}
735
+ value={[]}
736
+ onChange={() => {}}
737
+ hasCreate
738
+ debounceMs={0}
739
+ />
740
+ </InternationalizationProvider>,
741
+ );
742
+ await act(async () => {
743
+ fireEvent.change(screen.getByRole('combobox'), {
744
+ target: {value: 'Kiwi'},
745
+ });
746
+ });
747
+ await act(async () => {
748
+ await new Promise(r => setTimeout(r, 50));
749
+ });
750
+ expect(screen.getByText('„Kiwi“ erstellen')).toBeInTheDocument();
751
+ expect(screen.queryByText('Create "Kiwi"')).not.toBeInTheDocument();
752
+
753
+ rerender(
754
+ <InternationalizationProvider locale="en" overrides={overrides}>
755
+ <Tokenizer
756
+ label="Obst"
757
+ searchSource={emptySource}
758
+ value={[]}
759
+ onChange={() => {}}
760
+ hasCreate
761
+ debounceMs={0}
762
+ />
763
+ </InternationalizationProvider>,
764
+ );
765
+ // The row is derived per query, so the next query takes the new locale.
766
+ await act(async () => {
767
+ fireEvent.change(screen.getByRole('combobox'), {
768
+ target: {value: 'Kiwis'},
769
+ });
770
+ });
771
+ expect(await screen.findByText('Create "Kiwis"')).toBeInTheDocument();
772
+ });
773
+
726
774
  it('still offers Create below minQueryLength, and does not search', async () => {
727
775
  // The threshold exists to avoid a fetch too broad to be worth making.
728
776
  // Creating costs no fetch, so a field that can create `QA` should not
@@ -2119,3 +2167,23 @@ describe('Tokenizer consumer key handler order', () => {
2119
2167
  expect(currentTarget).toBe(screen.getByRole('combobox'));
2120
2168
  });
2121
2169
  });
2170
+
2171
+ describe('Tokenizer caller aria-labelledby', () => {
2172
+ it('names the input by its label and a caller aria-labelledby', () => {
2173
+ render(
2174
+ <>
2175
+ <span id="billing-heading">Billing</span>
2176
+ <Tokenizer
2177
+ label="Members"
2178
+ searchSource={userSource}
2179
+ value={[]}
2180
+ onChange={() => {}}
2181
+ aria-labelledby="billing-heading"
2182
+ />
2183
+ </>,
2184
+ );
2185
+ expect(
2186
+ screen.getByRole('combobox', {name: 'Members Billing'}),
2187
+ ).toBeInTheDocument();
2188
+ });
2189
+ });
@@ -85,6 +85,7 @@ import {
85
85
  } from '../theme/tokens.stylex';
86
86
  import type {SearchableItem, SearchSource} from '../Typeahead/types';
87
87
  import {composeEventHandlers, mergeProps} from '../utils';
88
+ import {joinAriaIDs} from '../utils/inputAria';
88
89
  import {themeProps} from '../utils/themeProps';
89
90
  import {useTranslator} from '../i18n';
90
91
  import type {TokenizerBottomSheetProps} from './TokenizerBottomSheet';
@@ -754,6 +755,7 @@ export function Tokenizer<T extends SearchableItem>({
754
755
  experimental,
755
756
  id: idProp,
756
757
  'aria-describedby': ariaDescribedByProp,
758
+ 'aria-labelledby': ariaLabelledByProp,
757
759
  ...restProps
758
760
  }: TokenizerWithPresentationProps<T>): React.JSX.Element {
759
761
  const {rest, container} = useFieldInputStyling('Tokenizer', restProps, {
@@ -768,8 +770,15 @@ export function Tokenizer<T extends SearchableItem>({
768
770
  const size = useSize(sizeProp, 'md');
769
771
  const generatedInputId = useId();
770
772
  const inputId = idProp ?? generatedInputId;
773
+ const fieldLabelId = useId();
771
774
  const descriptionId = useId();
772
775
  const statusMessageId = useId();
776
+ // A caller aria-labelledby names the input together with the field's own
777
+ // label, which it would otherwise replace.
778
+ const fieldLabelledBy =
779
+ ariaLabelledByProp != null
780
+ ? joinAriaIDs(fieldLabelId, ariaLabelledByProp)
781
+ : undefined;
773
782
  const inputRef = useRef<HTMLInputElement>(null);
774
783
  const wrapperRef = useRef<HTMLDivElement>(null);
775
784
 
@@ -1111,12 +1120,12 @@ export function Tokenizer<T extends SearchableItem>({
1111
1120
  return [
1112
1121
  {
1113
1122
  id: `${CREATABLE_ID_PREFIX}${trimmed}`,
1114
- label: `Create "${trimmed}"`,
1123
+ label: t('@astryx.tokenizer.create', {value: trimmed}),
1115
1124
  auxiliaryData: {__createdValue: trimmed},
1116
1125
  } as unknown as T,
1117
1126
  ];
1118
1127
  },
1119
- [hasCreate, selectedIds, selectedLabels],
1128
+ [hasCreate, selectedIds, selectedLabels, t],
1120
1129
  );
1121
1130
 
1122
1131
  const emptySource: SearchSource<T> = useMemo(
@@ -1391,6 +1400,7 @@ export function Tokenizer<T extends SearchableItem>({
1391
1400
  isFocusableDisabled={showsDisabledMessage}
1392
1401
  hasAutoFocus={hasAutoFocus}
1393
1402
  inputId={inputId}
1403
+ ariaLabelledBy={fieldLabelledBy}
1394
1404
  ariaDescribedBy={
1395
1405
  [ariaDescribedByProp, ariaDescribedBy]
1396
1406
  .filter(Boolean)
@@ -1457,6 +1467,7 @@ export function Tokenizer<T extends SearchableItem>({
1457
1467
  ref={triggerMergedRef}
1458
1468
  type="button"
1459
1469
  id={inputId}
1470
+ aria-labelledby={fieldLabelledBy}
1460
1471
  aria-haspopup="dialog"
1461
1472
  aria-expanded={isSheetOpen}
1462
1473
  aria-disabled={isDisabled || undefined}
@@ -1707,6 +1718,7 @@ export function Tokenizer<T extends SearchableItem>({
1707
1718
  isLabelHidden={isLabelHidden}
1708
1719
  description={description}
1709
1720
  inputID={inputId}
1721
+ labelID={ariaLabelledByProp != null ? fieldLabelId : undefined}
1710
1722
  descriptionID={description ? descriptionId : undefined}
1711
1723
  isOptional={isOptional}
1712
1724
  isRequired={isRequired}
@@ -75,6 +75,7 @@ function Harness({
75
75
  maxEntries?: number;
76
76
  isDisabled?: boolean;
77
77
  disabledMessage?: string;
78
+ 'aria-labelledby'?: string;
78
79
  debounceMs?: number;
79
80
  onFocus?: (event: React.FocusEvent) => void;
80
81
  onBlur?: (event: React.FocusEvent) => void;
@@ -238,6 +239,16 @@ describe('Tokenizer experimental.presentation (FR16)', () => {
238
239
  });
239
240
 
240
241
  describe('Tokenizer sheet presentation (single-sheet touch flow)', () => {
242
+ it('names the sheet trigger by its label, then a caller aria-labelledby', () => {
243
+ render(
244
+ <>
245
+ <span id="billing-heading">Billing</span>
246
+ <Harness aria-labelledby="billing-heading" />
247
+ </>,
248
+ );
249
+ expect(screen.getByRole('button', {name: 'Tags Billing'})).toBeTruthy();
250
+ });
251
+
241
252
  it('keeps rows stable while open and applies selected-first order on reopen (FR19)', async () => {
242
253
  const spy = vi.fn();
243
254
  render(<Harness spy={spy} />);
@@ -1787,7 +1798,7 @@ describe('Tokenizer sheet presentation (single-sheet touch flow)', () => {
1787
1798
  // changes until the sheet closes.
1788
1799
  compact = false;
1789
1800
  act(() => {
1790
- for (const cb of listeners) cb();
1801
+ for (const cb of listeners) {cb();}
1791
1802
  });
1792
1803
  expect(screen.getByRole('dialog', {name: 'Tags'})).toBe(dialog);
1793
1804
  expect(trigger).toBeInTheDocument();
@@ -2531,7 +2542,7 @@ describe('Tokenizer sheet presentation (single-sheet touch flow)', () => {
2531
2542
  // the hand-off inside the field reports neither a blur nor a focus.
2532
2543
  compact = false;
2533
2544
  act(() => {
2534
- for (const cb of listeners) cb();
2545
+ for (const cb of listeners) {cb();}
2535
2546
  });
2536
2547
  expect(screen.getByRole('combobox')).toHaveFocus();
2537
2548
  expect(onFocus).toHaveBeenCalledTimes(1);
@@ -2539,14 +2550,14 @@ describe('Tokenizer sheet presentation (single-sheet touch flow)', () => {
2539
2550
  // And back: the input focused, the screen becomes compact again.
2540
2551
  compact = true;
2541
2552
  act(() => {
2542
- for (const cb of listeners) cb();
2553
+ for (const cb of listeners) {cb();}
2543
2554
  });
2544
2555
  expect(openTrigger()).toHaveFocus();
2545
2556
  // Unfocused, a change moves nothing.
2546
2557
  openTrigger().blur();
2547
2558
  compact = false;
2548
2559
  act(() => {
2549
- for (const cb of listeners) cb();
2560
+ for (const cb of listeners) {cb();}
2550
2561
  });
2551
2562
  expect(document.body).toHaveFocus();
2552
2563
  });
@@ -41,15 +41,15 @@ import {BottomSheet} from '../BottomSheet/BottomSheet';
41
41
  import {Button} from '../Button/Button';
42
42
  import {EmptyState} from '../EmptyState/EmptyState';
43
43
  import {PanelSearchInput} from '../Field/PanelSearchInput';
44
- import {Icon} from '../Icon';
44
+ import {Icon} from '../Icon/Icon';
45
45
  import {CheckboxIndicator} from '../Indicator/CheckboxIndicator';
46
- import {Spinner} from '../Spinner';
46
+ import {Spinner} from '../Spinner/Spinner';
47
47
  import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
48
48
  import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
49
49
  import {useAnnounce} from '../hooks/useAnnounce';
50
50
  import {useAnnounceRenderedText} from '../hooks/useAnnounceRenderedText';
51
51
  import {Text} from '../Text/Text';
52
- import {useTranslator} from '../i18n';
52
+ import {useTranslator} from '../i18n/useTranslator';
53
53
  import {warnOnce} from '../utils/devWarning';
54
54
  import type {SearchableItem, SearchSource} from '../Typeahead/types';
55
55
  import {colorVars, radiusVars, spacingVars} from '../theme/tokens.stylex';
@@ -80,10 +80,9 @@ Consumer migration instructions belong in consumer docs and release notes.
80
80
  that does not hold the stop, the stop stays on the earlier control, so
81
81
  Shift+Tab from the clicked control lands on that earlier control inside the
82
82
  toolbar before leaving it.
83
- - **Links and some fields keep their own tab stops.** A link (such as a Button
84
- with `href`), `textarea`, or `select` in the row is outside the roving stop:
85
- it keeps its own tab stop and the arrow keys skip it. The APG toolbar pattern
86
- makes the toolbar a single tab stop.
83
+ - **Some fields keep their own tab stops.** A `textarea` or `select` in the
84
+ row is outside the roving stop: it keeps its own tab stop and the arrow keys
85
+ skip it. The APG toolbar pattern makes the toolbar a single tab stop.
87
86
 
88
87
  ## Ownership boundary
89
88
 
@@ -115,7 +114,7 @@ owns the shared structural-region boundary.
115
114
  | FR1 | The render contains Section-owned Outer chrome around one Toolbar carrying `role="toolbar"`, `aria-label` from `label`, `aria-orientation`, and the `toolbar` target with `size`. Caller `role`, `aria-label`, and `aria-orientation` are dropped; other DOM props reach the toolbar row; `ref`, `className`, `style`, and `xstyle` reach the Section. | Current source, docs, tests, and family |
116
115
  | FR2 | Without center content the Toolbar is a flex line of the present start and end lanes, a row when horizontal and a column of lanes when vertical; with center content it is a three-lane grid whose lanes sit side by side in either orientation. Each lane lays its controls out in a row. No lane clips its content, and the center lane can shrink below its content width. `gap` spaces controls within and between lanes. | Current source and focused structure tests |
117
116
  | FR3 | Variant (default `transparent`), selected divider edges, and block padding by size remain delegated to Section, while Toolbar retains toolbar semantics, keyboard behavior, and internal layout. | Current source, docs, and family contract |
118
- | FR4 | The row MUST be a single tab stop. ArrowRight and ArrowLeft (horizontal) or ArrowDown and ArrowUp (vertical) move focus between its controls, wrapping at the ends and skipping disabled controls; Home and End move to the first and last. Implementation gap: only buttons, inputs, and elements with `tabindex` share the stop, so links, `textarea`, and `select` keep their own tab stops, and a pointer click does not move the stop (Current-state impact). Arrows do not move focus out of a text input whose caret is mid-line, and inside an open composed menu arrow keys belong to the menu. The toolbar keeps its tab stop after a composed menu is dismissed from outside, and keeps its own arrow movement when rendered inside a popover. | APG toolbar pattern; current source and tests |
117
+ | FR4 | The row MUST be a single tab stop. ArrowRight and ArrowLeft (horizontal) or ArrowDown and ArrowUp (vertical) move focus between its controls, wrapping at the ends and skipping disabled controls; Home and End move to the first and last. Implementation gap: only buttons, inputs, links, and elements with `tabindex` share the stop, so `textarea` and `select` carry their own tab stops, and a pointer click does not move the stop (Current-state impact). Arrows do not move focus out of a text input whose caret is mid-line, and inside an open composed menu arrow keys belong to the menu. The toolbar keeps its tab stop after a composed menu is dismissed from outside, and keeps its own arrow movement when rendered inside a popover. | APG toolbar pattern; current source and tests |
119
118
  | FR5 | Caller `onKeyDown`, `onFocus`, and `onBlur` run first; when the caller cancels the event, the built-in handling is skipped. | Current source and tests |
120
119
  | FR6 | Keyboard focus entering the row can show a brief, self-dismissing hint for its arrow-key axis. | Current source |
121
120
  | FR7 | `size` (default `md`) is provided to descendant controls as their default size; Toolbar starts its own size scope and does not inherit an outer size provider (`architecture:component-size-cascade` INV7). | Current source and tests |
@@ -204,7 +203,7 @@ Read from source, with no focused test yet:
204
203
  - FR2: the flex and grid layouts themselves, and `gap` spacing.
205
204
  - FR3: divider edges.
206
205
  - FR5: caller `onFocus` and `onBlur` running first and their cancellation.
207
- - FR4: links, `textarea`, and `select` keeping their own tab stops, and the stop
206
+ - FR4: `textarea` and `select` keeping their own tab stops, and the stop
208
207
  staying put after a pointer click.
209
208
  - FR6.
210
209
  - FR7: descendant controls taking `size` as their default, and the fresh size
@@ -370,6 +370,36 @@ describe('Toolbar', () => {
370
370
  expect(buttons[2]).toHaveAttribute('tabindex', '-1');
371
371
  });
372
372
 
373
+ it('keeps a link member in the arrow order and off the tab order', async () => {
374
+ const user = userEvent.setup();
375
+ render(
376
+ <>
377
+ <Toolbar
378
+ label="Actions"
379
+ startContent={
380
+ <>
381
+ <button type="button">Cut</button>
382
+ <a href="https://example.com">Docs</a>
383
+ <button type="button">Paste</button>
384
+ </>
385
+ }
386
+ />
387
+ <button type="button">After</button>
388
+ </>,
389
+ );
390
+
391
+ const cut = screen.getByRole('button', {name: 'Cut'});
392
+ cut.focus();
393
+ await user.tab();
394
+ expect(screen.getByRole('button', {name: 'After'})).toHaveFocus();
395
+
396
+ cut.focus();
397
+ await user.keyboard('{ArrowRight}');
398
+ expect(screen.getByRole('link', {name: 'Docs'})).toHaveFocus();
399
+ await user.keyboard('{ArrowRight}');
400
+ expect(screen.getByRole('button', {name: 'Paste'})).toHaveFocus();
401
+ });
402
+
373
403
  it('keeps its tab stop after outside dismissal of MoreMenu', async () => {
374
404
  const user = userEvent.setup();
375
405
  render(
@@ -248,7 +248,8 @@ export function Toolbar({
248
248
  const gapVar = spacingVars[spacingStepToVar[gap]] as string;
249
249
 
250
250
  const {listRef, handleKeyDown, handleFocus} = useListFocus<HTMLDivElement>({
251
- itemSelector: 'button, input, [tabindex]',
251
+ // `a[href]` catches a Button rendered with `href`.
252
+ itemSelector: 'button, input, a[href], [tabindex]',
252
253
  boundarySelector: '[role="toolbar"], [popover]',
253
254
  orientation,
254
255
  hasRovingTabIndex: true,
@@ -93,15 +93,27 @@ async function runState(
93
93
  });
94
94
  }
95
95
 
96
- test('every toolbar expectation is exercised by a Toolbar state', async ({
97
- page,
98
- }) => {
99
- const cdp = await page.context().newCDPSession(page);
96
+ // Checking coverage runs every state again, which on a loaded runner nears
97
+ // the per-test timeout as one test. It runs as one serial part per state,
98
+ // sharing their results, then one check over all of them.
99
+ test.describe('every toolbar expectation is exercised by a Toolbar state', () => {
100
+ test.describe.configure({mode: 'serial'});
100
101
  const results: BindingResult[] = [];
102
+
101
103
  for (const state of TOOLBAR_BINDING_STATES) {
102
- results.push(await runState(page, cdp, state));
104
+ test(`runs ${state.id}`, async ({page}) => {
105
+ const cdp = await page.context().newCDPSession(page);
106
+ results.push(await runState(page, cdp, state));
107
+ });
103
108
  }
104
- expect(neverExercised(results)).toEqual([]);
109
+
110
+ test('no expectation is left unexercised', () => {
111
+ // Every part ran in this worker; this test alone proves nothing.
112
+ expect(results.map(result => result.state).sort()).toEqual(
113
+ TOOLBAR_BINDING_STATES.map(state => state.id).sort(),
114
+ );
115
+ expect(neverExercised(results)).toEqual([]);
116
+ });
105
117
  });
106
118
 
107
119
  /**
@@ -104,16 +104,27 @@ async function runState(
104
104
  });
105
105
  }
106
106
 
107
- test('every tooltip expectation is exercised by a Tooltip state', async ({
108
- page,
109
- }) => {
110
- test.setTimeout(3 * 60 * 1000);
111
- const cdp = await page.context().newCDPSession(page);
107
+ // Checking coverage runs every state again, which on a loaded runner nears
108
+ // the per-test timeout as one test. It runs as one serial part per state,
109
+ // sharing their results, then one check over all of them.
110
+ test.describe('every tooltip expectation is exercised by a Tooltip state', () => {
111
+ test.describe.configure({mode: 'serial'});
112
112
  const results: BindingResult[] = [];
113
+
113
114
  for (const state of TOOLTIP_BINDING_STATES) {
114
- results.push(await runState(page, cdp, state));
115
+ test(`runs ${state.id}`, async ({page}) => {
116
+ const cdp = await page.context().newCDPSession(page);
117
+ results.push(await runState(page, cdp, state));
118
+ });
115
119
  }
116
- expect(neverExercised(results)).toEqual([]);
120
+
121
+ test('no expectation is left unexercised', () => {
122
+ // Every part ran in this worker; this test alone proves nothing.
123
+ expect(results.map(result => result.state).sort()).toEqual(
124
+ TOOLTIP_BINDING_STATES.map(state => state.id).sort(),
125
+ );
126
+ expect(neverExercised(results)).toEqual([]);
127
+ });
117
128
  });
118
129
 
119
130
  for (const state of TOOLTIP_BINDING_STATES) {