@astryxdesign/core 0.7.5 → 0.7.6-canary.bb18c81

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 (353) 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/BottomSheet/BottomSheet.js +1 -1
  7. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetPanel.js +2 -1
  9. package/dist/BottomSheet/useSheetGestures.d.ts +12 -0
  10. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  11. package/dist/BottomSheet/useSheetGestures.js +30 -3
  12. package/dist/Button/Button.js +4 -4
  13. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  14. package/dist/CheckboxInput/CheckboxInput.js +6 -5
  15. package/dist/DateInput/DateInput.d.ts.map +1 -1
  16. package/dist/DateInput/DateInput.js +4 -1
  17. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  18. package/dist/DateInput/MonthScroller.js +101 -15
  19. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  20. package/dist/DateRangeInput/DateRangeInput.js +1 -1
  21. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  22. package/dist/DateTimeInput/DateTimeInput.js +4 -1
  23. package/dist/Dialog/Dialog.d.ts.map +1 -1
  24. package/dist/Dialog/Dialog.js +34 -9
  25. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  26. package/dist/Dialog/DialogHeader.js +5 -4
  27. package/dist/Divider/Divider.d.ts.map +1 -1
  28. package/dist/Divider/Divider.js +2 -1
  29. package/dist/Drawer/Drawer.d.ts.map +1 -1
  30. package/dist/Drawer/Drawer.js +5 -0
  31. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  32. package/dist/DropdownMenu/DropdownMenu.js +4 -1
  33. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
  34. package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
  35. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +10 -1
  36. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
  37. package/dist/DropdownMenu/lazyMenuBottomSheet.js +16 -2
  38. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  39. package/dist/DropdownMenu/renderDropdownItems.js +21 -16
  40. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  41. package/dist/EmptyState/EmptyState.js +2 -1
  42. package/dist/Field/FieldLabel.d.ts +1 -1
  43. package/dist/Field/FieldLabel.d.ts.map +1 -1
  44. package/dist/Field/FieldLabel.js +4 -4
  45. package/dist/Field/InputClearButton.js +3 -3
  46. package/dist/Field/PanelSearchInput.js +2 -2
  47. package/dist/FieldStatus/FieldStatus.js +2 -2
  48. package/dist/FileInput/FileInput.d.ts.map +1 -1
  49. package/dist/FileInput/FileInput.js +7 -1
  50. package/dist/Heading/Heading.js +1 -1
  51. package/dist/Icon/Icon.js +1 -1
  52. package/dist/Indicator/CheckIndicator.d.ts.map +1 -1
  53. package/dist/Indicator/CheckIndicator.js +2 -1
  54. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
  55. package/dist/Indicator/CheckboxIndicator.js +3 -1
  56. package/dist/Indicator/RadioIndicator.d.ts.map +1 -1
  57. package/dist/Indicator/RadioIndicator.js +3 -1
  58. package/dist/Item/Item.d.ts.map +1 -1
  59. package/dist/Item/Item.js +2 -1
  60. package/dist/Item/ItemSwipeLayer.js +1 -1
  61. package/dist/Layout/Layout.js +1 -1
  62. package/dist/Layout/LayoutContent.js +1 -1
  63. package/dist/Layout/LayoutFooter.js +1 -1
  64. package/dist/Layout/LayoutHeader.js +1 -1
  65. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  66. package/dist/Lightbox/Lightbox.js +5 -0
  67. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +5 -4
  68. package/dist/List/List.js +1 -1
  69. package/dist/List/ListItem.js +3 -3
  70. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  71. package/dist/MobileNav/MobileNav.js +7 -2
  72. package/dist/MultiSelector/MultiSelector.d.ts +1 -1
  73. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  74. package/dist/MultiSelector/MultiSelector.js +5 -2
  75. package/dist/NumberInput/NumberInput.d.ts +3 -1
  76. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  77. package/dist/NumberInput/NumberInput.js +3 -3
  78. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  79. package/dist/ProgressBar/ProgressBar.js +16 -12
  80. package/dist/Resizable/useResizable.d.ts.map +1 -1
  81. package/dist/Resizable/useResizable.js +28 -2
  82. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  83. package/dist/SegmentedControl/SegmentedControlItem.js +4 -1
  84. package/dist/Selector/useSelectorPresentation.d.ts +8 -1
  85. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  86. package/dist/Selector/useSelectorPresentation.js +20 -4
  87. package/dist/Slider/Slider.d.ts.map +1 -1
  88. package/dist/Slider/Slider.js +7 -1
  89. package/dist/Spinner/Spinner.js +2 -2
  90. package/dist/Stack/Stack.js +1 -1
  91. package/dist/Stack/StackItem.js +1 -1
  92. package/dist/TabList/TabList.d.ts.map +1 -1
  93. package/dist/TabList/TabList.js +16 -1
  94. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  95. package/dist/Table/plugins/columnResize/useTableColumnResize.js +9 -1
  96. package/dist/Text/Text.js +1 -1
  97. package/dist/Text/useTruncation.d.ts.map +1 -1
  98. package/dist/Text/useTruncation.js +23 -5
  99. package/dist/TextArea/TextArea.d.ts +1 -1
  100. package/dist/TextArea/TextArea.d.ts.map +1 -1
  101. package/dist/TextArea/TextArea.js +13 -2
  102. package/dist/TextInput/TextInput.d.ts.map +1 -1
  103. package/dist/TextInput/TextInput.js +3 -3
  104. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  105. package/dist/Timestamp/Timestamp.js +2 -2
  106. package/dist/Timestamp/TimestampHoverCard.js +4 -4
  107. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  108. package/dist/Toast/ToastViewport.js +82 -51
  109. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  110. package/dist/Tokenizer/Tokenizer.js +13 -2
  111. package/dist/Tokenizer/TokenizerBottomSheet.js +3 -3
  112. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  113. package/dist/Toolbar/Toolbar.js +2 -1
  114. package/dist/Typeahead/Typeahead.d.ts +1 -1
  115. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  116. package/dist/Typeahead/Typeahead.js +9 -1
  117. package/dist/astryx.css +14 -1
  118. package/dist/gestures/PinchGestureRecognizer.d.ts +30 -0
  119. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
  120. package/dist/gestures/PinchGestureRecognizer.js +125 -0
  121. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +70 -0
  122. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
  123. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +220 -0
  124. package/dist/gestures/SwipeGestureRecognizer.d.ts +36 -0
  125. package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
  126. package/dist/gestures/SwipeGestureRecognizer.js +145 -0
  127. package/dist/gestures/VelocityTracker.d.ts +6 -2
  128. package/dist/gestures/VelocityTracker.d.ts.map +1 -1
  129. package/dist/gestures/VelocityTracker.js +7 -3
  130. package/dist/gestures/types.d.ts +11 -7
  131. package/dist/gestures/types.d.ts.map +1 -1
  132. package/dist/gestures/types.js +22 -2
  133. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
  134. package/dist/hooks/useAdaptivePresentation.js +4 -1
  135. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  136. package/dist/hooks/useMenuHover.js +12 -1
  137. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  138. package/dist/i18n/generated-locales/en.generated.js +3 -0
  139. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  140. package/dist/i18n/generated-locales/pseudo.generated.js +3 -0
  141. package/dist/motion/animator.d.ts +7 -1
  142. package/dist/motion/animator.d.ts.map +1 -1
  143. package/dist/motion/animator.js +119 -27
  144. package/dist/theme/defineTheme.js +1 -1
  145. package/dist/theme/domainTokens/domainTokenDefaults.d.ts +3 -0
  146. package/dist/theme/domainTokens/domainTokenDefaults.d.ts.map +1 -0
  147. package/dist/theme/domainTokens/domainTokenDefaults.js +19 -0
  148. package/dist/theme/domainTokens/index.d.ts +5 -4
  149. package/dist/theme/domainTokens/index.d.ts.map +1 -1
  150. package/dist/theme/domainTokens/index.js +5 -9
  151. package/dist/utils/focusReturn.d.ts +17 -1
  152. package/dist/utils/focusReturn.d.ts.map +1 -1
  153. package/dist/utils/focusReturn.js +11 -4
  154. package/dist/utils/getNodeText.d.ts +8 -0
  155. package/dist/utils/getNodeText.d.ts.map +1 -0
  156. package/dist/utils/getNodeText.js +37 -0
  157. package/dist/utils/interactionModality.d.ts +11 -2
  158. package/dist/utils/interactionModality.d.ts.map +1 -1
  159. package/dist/utils/interactionModality.js +58 -5
  160. package/dist/utils/presentation.d.ts +50 -0
  161. package/dist/utils/presentation.d.ts.map +1 -0
  162. package/dist/utils/presentation.js +125 -0
  163. package/dist/utils/touchPresentation.d.ts.map +1 -1
  164. package/dist/utils/touchPresentation.js +5 -3
  165. package/dist/utils/touchPresentation.stylex.d.ts +15 -0
  166. package/dist/utils/touchPresentation.stylex.d.ts.map +1 -0
  167. package/dist/utils/touchPresentation.stylex.js +33 -0
  168. package/locales/en.json +13 -1
  169. package/locales/pseudo.json +9 -0
  170. package/package.json +5 -4
  171. package/src/AlertDialog/AlertDialog.tsx +6 -5
  172. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +18 -6
  173. package/src/Avatar/Avatar.spec.md +22 -22
  174. package/src/Avatar/Avatar.tsx +18 -1
  175. package/src/Avatar/__tests__/AvatarForcedColors.a11y.chromium.spec.ts +126 -0
  176. package/src/BottomSheet/BottomSheet.test.tsx +46 -0
  177. package/src/BottomSheet/BottomSheet.tsx +1 -1
  178. package/src/BottomSheet/BottomSheetPanel.tsx +2 -1
  179. package/src/BottomSheet/useSheetGestures.test.ts +52 -0
  180. package/src/BottomSheet/useSheetGestures.ts +37 -6
  181. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +18 -6
  182. package/src/Button/Button.tsx +4 -4
  183. package/src/Calendar/Calendar.spec.md +87 -48
  184. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +21 -7
  185. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +21 -7
  186. package/src/CheckboxInput/CheckboxInput.tsx +6 -5
  187. package/src/Collapsible/Collapsible.spec.md +2 -0
  188. package/src/Collapsible/CollapsibleGroup.spec.md +57 -29
  189. package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +18 -7
  190. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +18 -6
  191. package/src/ContextMenu/ContextMenu.test.tsx +35 -0
  192. package/src/DateInput/DateInput.test.tsx +53 -2
  193. package/src/DateInput/DateInput.tsx +15 -11
  194. package/src/DateInput/MonthScroller.tsx +106 -20
  195. package/src/DateInput/MonthScrollerSettle.test.tsx +165 -0
  196. package/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts +312 -0
  197. package/src/DateRangeInput/DateRangeInput.test.tsx +46 -3
  198. package/src/DateRangeInput/DateRangeInput.tsx +16 -12
  199. package/src/DateTimeInput/DateTimeInput.spec.md +71 -48
  200. package/src/DateTimeInput/DateTimeInput.test.tsx +61 -0
  201. package/src/DateTimeInput/DateTimeInput.tsx +15 -11
  202. package/src/Dialog/Dialog.test.tsx +216 -2
  203. package/src/Dialog/Dialog.tsx +42 -9
  204. package/src/Dialog/DialogHeader.tsx +5 -4
  205. package/src/Dialog/__tests__/DialogSecondDocument.a11y.browser.spec.ts +202 -0
  206. package/src/Divider/Divider.tsx +2 -1
  207. package/src/Drawer/Drawer.spec.md +111 -80
  208. package/src/Drawer/Drawer.test.tsx +22 -0
  209. package/src/Drawer/Drawer.tsx +5 -0
  210. package/src/Drawer/DrawerHeader.spec.md +181 -0
  211. package/src/DropdownMenu/DropdownMenu.test.tsx +41 -0
  212. package/src/DropdownMenu/DropdownMenu.tsx +7 -1
  213. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +34 -0
  214. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -5
  215. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +19 -7
  216. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +21 -9
  217. package/src/DropdownMenu/lazyMenuBottomSheet.test.ts +53 -0
  218. package/src/DropdownMenu/lazyMenuBottomSheet.ts +23 -6
  219. package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
  220. package/src/EmptyState/EmptyState.tsx +2 -1
  221. package/src/Field/FieldLabel.tsx +5 -4
  222. package/src/Field/InputClearButton.tsx +3 -3
  223. package/src/Field/PanelSearchInput.tsx +2 -2
  224. package/src/FieldStatus/FieldStatus.tsx +2 -2
  225. package/src/FileInput/FileInput.test.tsx +21 -0
  226. package/src/FileInput/FileInput.tsx +4 -1
  227. package/src/FormLayout/FormLayout.spec.md +7 -8
  228. package/src/Heading/Heading.tsx +1 -1
  229. package/src/Icon/Icon.tsx +1 -1
  230. package/src/Indicator/CheckIndicator.spec.md +47 -42
  231. package/src/Indicator/CheckIndicator.tsx +2 -1
  232. package/src/Indicator/CheckboxIndicator.spec.md +40 -37
  233. package/src/Indicator/CheckboxIndicator.tsx +3 -1
  234. package/src/Indicator/Indicator.spec.md +188 -0
  235. package/src/Indicator/RadioIndicator.tsx +3 -1
  236. package/src/Item/Item.tsx +2 -1
  237. package/src/Item/ItemSwipeLayer.tsx +1 -1
  238. package/src/Layout/Layout.spec.md +127 -131
  239. package/src/Layout/Layout.tsx +1 -1
  240. package/src/Layout/LayoutContent.spec.md +205 -0
  241. package/src/Layout/LayoutContent.tsx +1 -1
  242. package/src/Layout/LayoutFooter.spec.md +190 -0
  243. package/src/Layout/LayoutFooter.tsx +1 -1
  244. package/src/Layout/LayoutHeader.spec.md +193 -0
  245. package/src/Layout/LayoutHeader.tsx +1 -1
  246. package/src/Layout/LayoutPanel.spec.md +194 -0
  247. package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +18 -6
  248. package/src/Lightbox/Lightbox.test.tsx +18 -0
  249. package/src/Lightbox/Lightbox.tsx +5 -0
  250. package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +8 -4
  251. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +18 -8
  252. package/src/List/List.tsx +1 -1
  253. package/src/List/ListItem.tsx +3 -3
  254. package/src/MobileNav/MobileNav.test.tsx +15 -0
  255. package/src/MobileNav/MobileNav.tsx +9 -1
  256. package/src/MobileNav/__tests__/MobileNavHeader.a11y.chromium.spec.ts +89 -0
  257. package/src/MoreMenu/MoreMenu.test.tsx +11 -0
  258. package/src/MultiSelector/MultiSelector.spec.md +7 -4
  259. package/src/MultiSelector/MultiSelector.test.tsx +118 -1
  260. package/src/MultiSelector/MultiSelector.tsx +6 -2
  261. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +8 -1
  262. package/src/NavMenu/NavHeadingMenu.spec.md +1 -5
  263. package/src/NumberInput/NumberInput.doc.mjs +13 -7
  264. package/src/NumberInput/NumberInput.spec.md +59 -37
  265. package/src/NumberInput/NumberInput.tsx +13 -3
  266. package/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts +102 -0
  267. package/src/ProgressBar/ProgressBar.test.tsx +108 -32
  268. package/src/ProgressBar/ProgressBar.tsx +58 -10
  269. package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +21 -9
  270. package/src/ProgressBar/__tests__/ProgressBarMotion.a11y.chromium.spec.ts +831 -0
  271. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +21 -14
  272. package/src/Resizable/useResizable.test.ts +43 -0
  273. package/src/Resizable/useResizable.ts +32 -9
  274. package/src/SegmentedControl/SegmentedControl.test.tsx +84 -0
  275. package/src/SegmentedControl/SegmentedControlItem.tsx +11 -5
  276. package/src/Selector/Selector.test.tsx +90 -1
  277. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +8 -1
  278. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +8 -1
  279. package/src/Selector/useSelectorPresentation.ts +31 -3
  280. package/src/SideNav/SideNav.test.tsx +27 -0
  281. package/src/Slider/Slider.test.tsx +12 -0
  282. package/src/Slider/Slider.tsx +4 -1
  283. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +19 -7
  284. package/src/Spinner/Spinner.tsx +2 -2
  285. package/src/Stack/Stack.tsx +1 -1
  286. package/src/Stack/StackItem.tsx +1 -1
  287. package/src/TabList/TabList.test.tsx +52 -1
  288. package/src/TabList/TabList.tsx +16 -1
  289. package/src/TabList/__tests__/TabPressPointer.interaction.browser.spec.ts +163 -0
  290. package/src/Table/__tests__/Table.a11y.chromium.spec.ts +18 -6
  291. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +22 -0
  292. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
  293. package/src/Text/Text.tsx +1 -1
  294. package/src/Text/useTruncation.test.tsx +128 -1
  295. package/src/Text/useTruncation.ts +23 -5
  296. package/src/TextArea/TextArea.spec.md +10 -13
  297. package/src/TextArea/TextArea.test.tsx +60 -0
  298. package/src/TextArea/TextArea.tsx +15 -1
  299. package/src/TextInput/TextInput.test.tsx +21 -0
  300. package/src/TextInput/TextInput.tsx +3 -1
  301. package/src/Timestamp/Timestamp.spec.md +6 -12
  302. package/src/Timestamp/Timestamp.test.tsx +30 -5
  303. package/src/Timestamp/Timestamp.tsx +6 -2
  304. package/src/Timestamp/TimestampHoverCard.tsx +4 -4
  305. package/src/Toast/Toast.spec.md +7 -10
  306. package/src/Toast/ToastViewport.test.tsx +575 -1
  307. package/src/Toast/ToastViewport.tsx +100 -50
  308. package/src/Tokenizer/Tokenizer.spec.md +5 -3
  309. package/src/Tokenizer/Tokenizer.test.tsx +68 -0
  310. package/src/Tokenizer/Tokenizer.tsx +14 -2
  311. package/src/Tokenizer/TokenizerBottomSheet.test.tsx +15 -4
  312. package/src/Tokenizer/TokenizerBottomSheet.tsx +3 -3
  313. package/src/Toolbar/Toolbar.spec.md +5 -6
  314. package/src/Toolbar/Toolbar.test.tsx +30 -0
  315. package/src/Toolbar/Toolbar.tsx +2 -1
  316. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +18 -6
  317. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +18 -7
  318. package/src/TopNav/TopNav.test.tsx +27 -0
  319. package/src/Typeahead/Typeahead.test.tsx +40 -0
  320. package/src/Typeahead/Typeahead.tsx +12 -1
  321. package/src/gestures/PinchGestureRecognizer.ts +144 -0
  322. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +252 -0
  323. package/src/gestures/SwipeGestureRecognizer.ts +193 -0
  324. package/src/gestures/VelocityTracker.ts +10 -3
  325. package/src/gestures/__tests__/PanGestureRecognizer.test.ts +4 -4
  326. package/src/gestures/__tests__/PinchGestureRecognizer.test.ts +74 -0
  327. package/src/gestures/__tests__/ScreenEdgePanGestureRecognizer.test.ts +498 -0
  328. package/src/gestures/__tests__/SwipeGestureRecognizer.test.ts +108 -0
  329. package/src/gestures/__tests__/arenaPinchSwipeEdge.test.ts +188 -0
  330. package/src/gestures/__tests__/harness.ts +21 -2
  331. package/src/gestures/types.ts +23 -9
  332. package/src/hooks/useAdaptivePresentation.ts +4 -1
  333. package/src/hooks/useMenuHover.ts +15 -1
  334. package/src/i18n/generated-locales/en.generated.ts +4 -0
  335. package/src/i18n/generated-locales/pseudo.generated.ts +4 -0
  336. package/src/motion/README.md +11 -1
  337. package/src/motion/animator.test.ts +151 -3
  338. package/src/motion/animator.ts +149 -33
  339. package/src/theme/defineTheme.ts +1 -1
  340. package/src/theme/derivedVarRegistry.test.ts +4 -0
  341. package/src/theme/domainTokens/domainTokenDefaults.ts +19 -0
  342. package/src/theme/domainTokens/index.ts +6 -10
  343. package/src/utils/focusReturn.ts +17 -6
  344. package/src/utils/getNodeText.test.ts +24 -0
  345. package/src/utils/getNodeText.ts +37 -0
  346. package/src/utils/interactionModality.test.ts +45 -1
  347. package/src/utils/interactionModality.ts +64 -5
  348. package/src/utils/presentation.test.ts +380 -0
  349. package/src/utils/presentation.ts +180 -0
  350. package/src/utils/pressPaintPriority.test.ts +80 -0
  351. package/src/utils/touchPresentation.stylex.ts +43 -0
  352. package/src/utils/touchPresentation.test.ts +140 -1
  353. package/src/utils/touchPresentation.ts +5 -3
@@ -0,0 +1,89 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file MobileNavHeader.a11y.chromium.spec.ts
5
+ * @input Built MobileNav stories in a phone context in real Chromium
6
+ * @output Browser evidence that the header row is at least one bar high and
7
+ * grows with taller header content: a stacked title and search field stay
8
+ * inside the row, and the first navigation item starts below it
9
+ * @position Chromium proof for MobileNav's header row (`styles.header` in
10
+ * MobileNav.tsx, `minHeight`); jsdom has no layout to measure it
11
+ */
12
+
13
+ import {expect, test, type Page} from '@playwright/test';
14
+ import {
15
+ DEFAULT_STORYBOOK_DIR,
16
+ serveStorybook,
17
+ type StaticServer,
18
+ } from '@astryxdesign/a11y-spec/storybook';
19
+
20
+ let storybook: StaticServer;
21
+ test.beforeAll(async () => {
22
+ storybook = await serveStorybook(
23
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
24
+ );
25
+ });
26
+ test.afterAll(async () => {
27
+ await storybook?.close();
28
+ });
29
+
30
+ test.use({
31
+ hasTouch: true,
32
+ isMobile: true,
33
+ viewport: {width: 390, height: 844},
34
+ deviceScaleFactor: 1,
35
+ });
36
+
37
+ /** Opens a story's drawer and returns the boxes of its header row and first item. */
38
+ async function openAndMeasure(page: Page, story: string, firstItem: string) {
39
+ await page.goto(
40
+ `${storybook.origin}/iframe.html?id=core-mobilenav--${story}&viewMode=story`,
41
+ );
42
+ await page.getByRole('button', {name: 'Open Navigation'}).click();
43
+ const nav = page.getByRole('dialog');
44
+ await expect(nav).toBeVisible();
45
+ const close = nav.getByRole('button', {name: 'Close navigation'});
46
+ // The Close button sits in the header row.
47
+ const header = close.locator('xpath=..');
48
+ const headerBox = await header.boundingBox();
49
+ const itemBox = await nav
50
+ .getByRole('link', {name: firstItem})
51
+ .first()
52
+ .boundingBox();
53
+ if (headerBox == null || itemBox == null) {
54
+ throw new Error('the drawer did not render');
55
+ }
56
+ return {nav, header, headerBox, itemBox};
57
+ }
58
+
59
+ test('a one-line header keeps the one-bar row', async ({page}) => {
60
+ const {headerBox, itemBox} = await openAndMeasure(
61
+ page,
62
+ 'default',
63
+ 'Dashboard',
64
+ );
65
+ // --spacing-12 at the default root size, as before.
66
+ expect(headerBox.height).toBeCloseTo(48, 0);
67
+ expect(itemBox.y).toBeGreaterThanOrEqual(headerBox.y + headerBox.height);
68
+ });
69
+
70
+ test('a taller header grows its row, and the first item starts below it', async ({
71
+ page,
72
+ }) => {
73
+ const {nav, headerBox, itemBox} = await openAndMeasure(
74
+ page,
75
+ 'tall-header',
76
+ 'Dashboard',
77
+ );
78
+ expect(headerBox.height).toBeGreaterThan(48);
79
+ const search = await nav.getByRole('textbox', {name: 'Search'}).boundingBox();
80
+ if (search == null) {
81
+ throw new Error('the search field did not render');
82
+ }
83
+ // The search field is inside the row, and nothing overlaps the first item.
84
+ expect(search.y).toBeGreaterThanOrEqual(headerBox.y);
85
+ expect(search.y + search.height).toBeLessThanOrEqual(
86
+ headerBox.y + headerBox.height,
87
+ );
88
+ expect(itemBox.y).toBeGreaterThanOrEqual(headerBox.y + headerBox.height);
89
+ });
@@ -9,6 +9,7 @@
9
9
  * SYNC: When MoreMenu.tsx changes, update tests to match new behavior
10
10
  */
11
11
 
12
+ import {createRef} from 'react';
12
13
  import {describe, it, expect, vi, beforeAll, beforeEach} from 'vitest';
13
14
  import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
15
  import userEvent from '@testing-library/user-event';
@@ -256,6 +257,16 @@ describe('MoreMenu', () => {
256
257
  expect(handleEdit).not.toHaveBeenCalled();
257
258
  });
258
259
 
260
+ it('forwards its ref to the bottom-sheet trigger', () => {
261
+ const ref = createRef<HTMLButtonElement>();
262
+ render(
263
+ <MoreMenu items={defaultItems} ref={ref} presentation="bottom-sheet" />,
264
+ );
265
+ expect(ref.current).toBe(
266
+ screen.getByRole('button', {name: 'More options'}),
267
+ );
268
+ });
269
+
259
270
  it('supports forwardRef', () => {
260
271
  const ref = vi.fn();
261
272
  render(<MoreMenu items={defaultItems} ref={ref} />);
@@ -182,10 +182,13 @@ and `aria-controls` for the listbox, the listbox carries `aria-label` from
182
182
  focused element that owns arrow, Enter, Escape, and typeahead keys while open,
183
183
  so an anchor that cannot take focus still leaves a keyboard path.
184
184
 
185
- The create row is a `role="option"` with `aria-selected="false"` and no
186
- checkbox indicator. When it is the only result, the polite live region
187
- announces its label rather than the empty-search message, so what is announced
188
- matches what is on screen; picking it announces the creation.
185
+ The create row is a `role="option"` with `aria-selected="false"` and no checkbox
186
+ indicator. When it is the only result, the polite live region announces its
187
+ label rather than the empty-search message, so what is announced matches what is
188
+ on screen; picking it announces the creation. The result count and the create
189
+ row's label come from the search's own `useAnnounce({hasReplace: true})`
190
+ instance, so a newer one replaces one not yet spoken (`spec:AST-086` FR9), and
191
+ selection messages keep their own instance.
189
192
 
190
193
  ## Design relationships
191
194
 
@@ -9,7 +9,15 @@
9
9
  * SYNC: When MultiSelector.tsx API changes, update these tests.
10
10
  */
11
11
 
12
- import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
12
+ import {
13
+ describe,
14
+ it,
15
+ expect,
16
+ vi,
17
+ beforeAll,
18
+ beforeEach,
19
+ afterEach,
20
+ } from 'vitest';
13
21
  import {
14
22
  act,
15
23
  render,
@@ -65,6 +73,15 @@ function politeRegion(): HTMLElement | null {
65
73
  }
66
74
 
67
75
  // Mock showPopover and hidePopover methods since they're not implemented in jsdom
76
+ // The bottom-sheet presentation renders BottomSheet through React.lazy. Its
77
+ // first import transforms the BottomSheet module tree, which on a loaded
78
+ // runner takes longer than a query's 1 s wait, so the first sheet test timed
79
+ // out before the dialog existed. Load the module once up front: lazy() then
80
+ // resolves from the module cache and every sheet test waits only on the open.
81
+ beforeAll(async () => {
82
+ await import('../BottomSheet/BottomSheet');
83
+ });
84
+
68
85
  beforeEach(() => {
69
86
  __resetInteractionModalityForTest();
70
87
  HTMLDialogElement.prototype.showModal = vi.fn(function (
@@ -128,6 +145,13 @@ function expectSilentBusySpinner() {
128
145
  expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
129
146
  }
130
147
 
148
+ // The bottom sheet is loaded on demand (SelectorBottomSheet's lazy import).
149
+ // Loading its module is not part of an open, so the file waits for it once
150
+ // here, and each test's open waits only for the open.
151
+ beforeAll(async () => {
152
+ await import('../BottomSheet/BottomSheet');
153
+ });
154
+
131
155
  describe('MultiSelector', () => {
132
156
  const defaultOptions = ['Apple', 'Banana', 'Orange'];
133
157
 
@@ -1388,6 +1412,58 @@ describe('MultiSelector', () => {
1388
1412
  expect(within(listbox).getByText('Add a fruit first')).toBeInTheDocument();
1389
1413
  });
1390
1414
 
1415
+ describe('count trigger text', () => {
1416
+ it('shows the default count in the catalog message', () => {
1417
+ render(
1418
+ <MultiSelector
1419
+ label="Fruit"
1420
+ options={defaultOptions}
1421
+ value={['Apple', 'Banana']}
1422
+ onChange={() => {}}
1423
+ triggerDisplay="count"
1424
+ />,
1425
+ );
1426
+ expect(screen.getByText('2 selected')).toBeInTheDocument();
1427
+ });
1428
+
1429
+ it('translates the default count, with plural forms, under another locale', () => {
1430
+ render(
1431
+ <InternationalizationProvider
1432
+ locale="fr"
1433
+ overrides={{
1434
+ fr: {
1435
+ '@astryx.multiSelector.selectedCount':
1436
+ '{count, number} {count, plural, one {sélectionné} other {sélectionnés}}',
1437
+ },
1438
+ }}>
1439
+ <MultiSelector
1440
+ label="Fruit"
1441
+ options={defaultOptions}
1442
+ value={['Apple', 'Banana']}
1443
+ onChange={() => {}}
1444
+ triggerDisplay="count"
1445
+ />
1446
+ </InternationalizationProvider>,
1447
+ );
1448
+ expect(screen.getByText('2 sélectionnés')).toBeInTheDocument();
1449
+ expect(screen.queryByText('2 selected')).not.toBeInTheDocument();
1450
+ });
1451
+
1452
+ it('still lets formatValue replace the count text', () => {
1453
+ render(
1454
+ <MultiSelector
1455
+ label="Fruit"
1456
+ options={defaultOptions}
1457
+ value={['Apple', 'Banana']}
1458
+ onChange={() => {}}
1459
+ triggerDisplay="count"
1460
+ formatValue={items => `${items.length} fruits`}
1461
+ />,
1462
+ );
1463
+ expect(screen.getByText('2 fruits')).toBeInTheDocument();
1464
+ });
1465
+ });
1466
+
1391
1467
  describe('result announcements', () => {
1392
1468
  it('announces the match count politely while searching', async () => {
1393
1469
  const user = userEvent.setup();
@@ -3436,6 +3512,47 @@ describe('MultiSelector trigger render prop', () => {
3436
3512
  expect(screen.queryByText('Labels')).toBeNull();
3437
3513
  });
3438
3514
 
3515
+ // spec:AST-055 AR3: a light dismiss returns focus to the caller's control,
3516
+ // even when the press that closed the list focused another field.
3517
+ it('returns focus to the caller control after a light dismiss onto another field', async () => {
3518
+ const user = userEvent.setup();
3519
+ render(
3520
+ <>
3521
+ <MultiSelector
3522
+ label="Labels"
3523
+ options={OPTIONS}
3524
+ value={[]}
3525
+ onChange={() => {}}
3526
+ renderTrigger={props => (
3527
+ <button type="button" {...props}>
3528
+ Pick labels
3529
+ </button>
3530
+ )}
3531
+ />
3532
+ <input aria-label="Notes" />
3533
+ </>,
3534
+ );
3535
+ const opener = screen.getByRole('button', {name: 'Pick labels'});
3536
+ await user.click(opener);
3537
+ expect(opener).toHaveAttribute('aria-expanded', 'true');
3538
+
3539
+ act(() => {
3540
+ screen.getByRole('textbox', {name: 'Notes'}).focus();
3541
+ });
3542
+ const popover = document.querySelector('[popover]') as HTMLElement;
3543
+ act(() => {
3544
+ popover.dispatchEvent(
3545
+ Object.assign(new Event('toggle'), {
3546
+ oldState: 'open',
3547
+ newState: 'closed',
3548
+ }),
3549
+ );
3550
+ });
3551
+
3552
+ expect(opener).toHaveAttribute('aria-expanded', 'false');
3553
+ expect(opener).toHaveFocus();
3554
+ });
3555
+
3439
3556
  it('opens against the caller control and names the listbox by label', async () => {
3440
3557
  const user = userEvent.setup();
3441
3558
  render(
@@ -905,7 +905,7 @@ export interface MultiSelectorProps<
905
905
  * Not called when nothing is selected — the placeholder shows instead — and
906
906
  * not called for triggerDisplay 'badges', which renders Badge elements
907
907
  * rather than text.
908
- * @default items => `${items.length} selected` for 'count', "A, B, C, +N" for 'labels'
908
+ * @default the translated `@astryx.multiSelector.selectedCount` message ("2 selected") for 'count', "A, B, C, +N" for 'labels'
909
909
  */
910
910
  formatValue?: (items: MultiSelectorSelectedItem[]) => string;
911
911
 
@@ -1450,6 +1450,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1450
1450
  onHide: handleLayerHide,
1451
1451
  onShow: handleLayerShow,
1452
1452
  triggerRef,
1453
+ isFocusReturnAlways: hasExternalTrigger,
1453
1454
  popoverOptions: {
1454
1455
  hasLightDismiss: true,
1455
1456
  hasCloseButton: false,
@@ -1890,7 +1891,9 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1890
1891
  return (
1891
1892
  <span {...stylex.props(styles.triggerText)}>
1892
1893
  {formatValue?.(selectedItems) ??
1893
- `${optimisticValue.length} selected`}
1894
+ t('@astryx.multiSelector.selectedCount', {
1895
+ count: optimisticValue.length,
1896
+ })}
1894
1897
  </span>
1895
1898
  );
1896
1899
 
@@ -1933,6 +1936,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1933
1936
  placeholder,
1934
1937
  formatValue,
1935
1938
  maxBadges,
1939
+ t,
1936
1940
  ]);
1937
1941
 
1938
1942
  // Render search input
@@ -9,7 +9,7 @@
9
9
 
10
10
  import {cleanup, render, screen, within} from '@testing-library/react';
11
11
  import userEvent from '@testing-library/user-event';
12
- import {describe, expect, it} from 'vitest';
12
+ import {beforeAll, describe, expect, it} from 'vitest';
13
13
  import {
14
14
  expectAccessibilitySpec,
15
15
  LISTBOX_PATTERN,
@@ -23,6 +23,13 @@ import {installListboxDialogStubs} from '../../Selector/__tests__/Listbox.a11y.d
23
23
 
24
24
  installListboxDialogStubs();
25
25
 
26
+ // The bottom sheet is loaded on demand (SelectorBottomSheet's lazy import).
27
+ // Loading its module is not part of an open, so the file waits for it once
28
+ // here, and each scenario's open waits only for the open.
29
+ beforeAll(async () => {
30
+ await import('../../BottomSheet/BottomSheet');
31
+ });
32
+
26
33
  const scenarios = LISTBOX_SCENARIOS.filter(
27
34
  scenario => scenario.component === 'MultiSelector',
28
35
  );
@@ -44,7 +44,7 @@ system_specs: [spec:AST-005/DEC-1, spec:AST-020, spec:AST-085]
44
44
  | Behavior | A `role="menu"` list of `menuitem` rows inside a heading popover, with arrow, Home, End, typeahead, Enter, Space, and Escape support. Selecting an enabled item runs it and, where the host heading supplies a close action, closes the heading menu. |
45
45
  | End-user impact | Users switch products or accounts from the heading menu by pointer or keyboard; disabled rows are skipped by keyboard and typeahead. |
46
46
  | Builder impact | None. |
47
- | Compatibility/readiness | Records shipped behavior; no public change. Six implementation gaps (see Current-state impact). |
47
+ | Compatibility/readiness | Records shipped behavior; no public change. Five implementation gaps (see Current-state impact). |
48
48
  | Review checks | Reject losing `menu`/`menuitem` roles to caller props, keyboard focus landing on a disabled item, or a caller `onKeyDown` that silently disables the keyboard model. |
49
49
  | Governing rules | `family:navigation-destinations` FR7; `architecture:interaction-modality`; `architecture:component-theming-surface`; `architecture:layer-runtime`. |
50
50
 
@@ -81,10 +81,6 @@ Consumer migration instructions belong in consumer docs and release notes.
81
81
  `role="menu"` sits directly inside the heading's own `role="menu"` wrapper.
82
82
  WAI-ARIA 1.2 allows a `menu` to own only `menuitem`-family items and groups,
83
83
  so assistive technology meets a menu nested in a menu.
84
- - **Inside a heading, arrow keys move twice.** Inside SideNavHeading or
85
- TopNavHeading, the heading's own list handler also moves focus over every
86
- `menuitem`, so one ArrowDown moves two items, and in a two-item menu ArrowDown
87
- never moves focus. The APG menu pattern moves focus one item per arrow press.
88
84
  - **The menu has no name of its own.** NavHeadingMenu's `menu` carries no
89
85
  accessible name unless the caller passes `aria-label`; inside a heading only
90
86
  the heading's wrapper `menu` is named. The APG menu pattern, which
@@ -160,7 +160,7 @@ export const docs = {
160
160
  name: 'hasNumberSteppers',
161
161
  type: 'boolean',
162
162
  description:
163
- 'Shows increment and decrement buttons at the end of the input.',
163
+ 'Shows increment and decrement buttons at the end of the input on fine pointers. Hidden on coarse pointers (such as touch screens); text entry and keyboard stepping remain available.',
164
164
  default: 'false',
165
165
  },
166
166
  {
@@ -226,12 +226,14 @@ export const docs = {
226
226
  {
227
227
  name: 'containerClassName',
228
228
  type: 'string',
229
- description: 'Class name for the field container. Same targeting as `containerXstyle`.',
229
+ description:
230
+ 'Class name for the field container. Same targeting as `containerXstyle`.',
230
231
  },
231
232
  {
232
233
  name: 'containerStyle',
233
234
  type: 'CSSProperties',
234
- description: 'Inline styles for the field container. Same targeting as `containerXstyle`.',
235
+ description:
236
+ 'Inline styles for the field container. Same targeting as `containerXstyle`.',
235
237
  },
236
238
  {
237
239
  name: 'controlXstyle',
@@ -474,7 +476,8 @@ export const docsZh = {
474
476
  {
475
477
  name: 'hasNumberSteppers',
476
478
  type: 'boolean',
477
- description: '是否在输入框末尾显示递增和递减按钮。',
479
+ description:
480
+ '在精细指针设备上于输入框末尾显示递增和递减按钮。粗指针设备(例如触摸屏)上隐藏按钮;仍可直接输入数值或使用键盘步进。',
478
481
  default: 'false',
479
482
  },
480
483
  {
@@ -720,10 +723,12 @@ export const docsDense = {
720
723
  ],
721
724
  },
722
725
  propDescriptions: {
723
- containerXstyle: 'StyleX styles for the field container; no effect in InputGroup or horizontal-labels FormLayout. No root xstyle/className/style.',
726
+ containerXstyle:
727
+ 'StyleX styles for the field container; no effect in InputGroup or horizontal-labels FormLayout. No root xstyle/className/style.',
724
728
  containerClassName: 'Class name for the field container.',
725
729
  containerStyle: 'Inline styles for the field container.',
726
- controlXstyle: 'StyleX styles for the visual control box (border, background, icons); not the native input.',
730
+ controlXstyle:
731
+ 'StyleX styles for the visual control box (border, background, icons); not the native input.',
727
732
  controlClassName: 'Class name for the visual control box.',
728
733
  controlStyle: 'Inline styles for the visual control box.',
729
734
  label: 'Label text (always rendered for accessibility).',
@@ -754,7 +759,8 @@ export const docsDense = {
754
759
  'Formats committed value at rest; raw number is shown while editing.',
755
760
  isWheelEnabled:
756
761
  'Allow focused wheel gestures to step value. Defaults to true.',
757
- hasNumberSteppers: 'Show increment + decrement buttons. Defaults to false.',
762
+ hasNumberSteppers:
763
+ 'Show increment + decrement buttons on fine pointers; hidden on coarse pointers. Defaults to false.',
758
764
  units: 'Units suffix (e.g. "%" or "GB").',
759
765
  isIntegerOnly: 'Only allow integer values.',
760
766
  hasClear:
@@ -6,8 +6,8 @@ id: component:NumberInput
6
6
  authority: current
7
7
  archive_reason: null
8
8
  superseded_by: null
9
- approved_by: cixzhang
10
- approved_at: 2026-10-26
9
+ approved_by: imdreamrunner
10
+ approved_at: 2026-10-29
11
11
  owners: [cixzhang]
12
12
  review_triggers: [public-api, behavior, layout, theming, accessibility]
13
13
  verified_by:
@@ -19,6 +19,7 @@ verified_by:
19
19
  packages/core/src/NumberInput/numberParser.docblock.test.ts,
20
20
  packages/core/src/NumberInput/__tests__/NumberInput.a11y.test.tsx,
21
21
  packages/core/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts,
22
+ packages/core/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts,
22
23
  packages/core/src/InputGroup/InputGroup.test.tsx,
23
24
  ]
24
25
  modules: []
@@ -40,15 +41,15 @@ system_specs: [spec:AST-085, spec:AST-086, spec:AST-102]
40
41
 
41
42
  ## Contract at a glance
42
43
 
43
- | Area | Contract |
44
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
- | Public contract | None changes. NumberInput is a controlled numeric field with Field props, size, status, `min`, `max`, `step`, `isIntegerOnly`, `formatValue`, `units`, steppers, wheel stepping, clear, disabled and read-only states, and the input-family part props. |
46
- | Behavior | A text-backed `spinbutton`. Typing edits a draft that commits only on blur or Enter, parsed for the locale and clamped to the range. Arrow keys, the wheel, and the steppers step on a grid and commit at once. |
47
- | End-user impact | People can type or paste numbers the way their locale and spreadsheets write them, and get told at once when an entry is invalid. Keyboard and screen-reader users can reach a disabled field. Read-only is not announced. |
48
- | Builder impact | None. |
49
- | Compatibility/readiness | Records shipped behavior; no API, default, or DOM change. The read-only state is not exposed to assistive technology, and the clear button scales under reduced motion; both are listed under Current-state impact. |
50
- | Review checks | Reject an `onChange` for every keystroke, a commit of an unparsable draft, a step off the grid or past a bound, a disabled field that leaves the tab order or submits, or arrows that step during IME composition. |
51
- | Governing rules | `family:input-fields` FR3, FR4, FR8–FR11; `spec:AST-085` FR1–FR3, FR6; `spec:AST-086` FR3, FR4; `architecture:internationalization`; `design:reduced-motion` DR1. |
44
+ | Area | Contract |
45
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | Public contract | NumberInput is a controlled numeric field with Field props, size, status, `min`, `max`, `step`, `isIntegerOnly`, `formatValue`, `units`, steppers, wheel stepping, clear, disabled and read-only states, and input-family part props. |
47
+ | Behavior | A text-backed `spinbutton`; a typed draft commits on blur or Enter, parsed for the locale and clamped to the range. Arrow keys, the wheel, and available non-coarse-pointer steppers step on a grid and commit at once. |
48
+ | End-user impact | Coarse-pointer users enter numbers directly instead of using compact steppers. Keyboard stepping remains available. Invalid entries are announced; disabled fields remain reachable; read-only is not announced. |
49
+ | Builder impact | Public prop types and numeric callbacks remain unchanged; existing NumberInput calls need no source or configuration migration. |
50
+ | Compatibility/readiness | Coarse-pointer adaptation is non-breaking: numeric values, validation, direct entry, and keyboard stepping remain supported. The read-only exposure and reduced-motion clear-button gaps remain listed under Current-state impact. |
51
+ | Review checks | Reject coarse-pointer steppers, loss of non-coarse-pointer steppers when requested, per-keystroke `onChange`, an unparsable commit, an off-grid or out-of-range step, a disabled field that leaves the tab order or submits, or stepping during IME composition. |
52
+ | Governing rules | FR5 and AR4 own pointer-conditioned stepper availability; `family:input-fields` FR3, FR4, FR8–FR11; `spec:AST-085` FR1–FR3, FR6; `spec:AST-086` FR3, FR4; `spec:AST-017` FR1, FR3, FR7; `architecture:internationalization`; `design:reduced-motion` DR1. |
52
53
 
53
54
  This table is a review projection; the body below is authoritative.
54
55
 
@@ -63,12 +64,16 @@ Consumer usage lives in `NumberInput.doc.mjs`.
63
64
 
64
65
  - Released default preserved: `yes`; `step={1}`, wheel stepping on, no
65
66
  steppers, no clear, `size="md"`, `statusVariant="attached"`
66
- - Compatibility class: records shipped behavior; no public prop, default,
67
- export, or DOM change
67
+ - Compatibility class: non-breaking presentation adaptation; public prop
68
+ names, types, exports, numeric values and callbacks, validation, direct entry,
69
+ and keyboard stepping remain unchanged under `spec:AST-017` FR3's numeric-stepper
70
+ presentation rule
68
71
  - Controlled/uncontrolled behavior: controlled only; `value` (`number`, `null`,
69
72
  or `undefined`) is the committed value, and NumberInput keeps only an
70
73
  uncommitted text draft of its own
71
- - Migration decision: none
74
+ - Migration decision: no caller source or configuration migration; numeric
75
+ input functionality remains available through direct entry and keyboard
76
+ stepping while compact visual stepper aids adapt to pointer precision
72
77
 
73
78
  Consumer migration instructions belong in consumer docs and release notes.
74
79
 
@@ -118,7 +123,7 @@ Consumer migration instructions belong in consumer docs and release notes.
118
123
  | FR2 | Typing changes only a draft; `onChange` is not called while typing. A draft is valid when it parses for the active locale (grouping, decimal separator, digit script, and sign) and, for validity display, lies within the range and meets integer-only. An invalid draft colors the text secondary, sets `aria-invalid`, and fills a persistent assertive region with the localized invalid-number message. | `spec:AST-086` FR3; `architecture:internationalization`; current tests |
119
124
  | FR3 | On blur the draft resolves once and ends: an empty draft clears (`onChange(null)` when clearable and a value exists) or reverts; an unparsable or wrong-kind draft reverts; otherwise the number clamps to the range and commits with `onChange(number)` when it differs from `value`. Enter resolves a valid draft the same way and clears an empty draft when clearable. Enter commits nothing for an empty draft without `hasClear`, which stays in the field and is not marked invalid, or for an unparsable or wrong-kind draft, which stays in the field and stays invalid. Enter ends the draft only when its commit clamped. In a field that is not disabled, every Enter outside composition then calls `onEnter`, whatever it committed. | Current tests |
120
125
  | FR4 | ArrowUp and ArrowDown without modifiers, the wheel over the focused input (when `isWheelEnabled`), and the steppers step by one grid position and commit at once. The grid starts at `min` (when it suits integer-only) or 0. From an empty field or an emptied draft, up goes to `min` or 0 and down to `max` or 0. Results round to the finer of the step's and the grid start's precision (at most 12 decimal places), clamp to the range, and never leave integer-only; a stepper that cannot move does nothing, while an arrow or wheel step that cannot move still ends the draft, committing a valid draft's number and dropping an unparsable one. A caller `onKeyDown` that prevents an arrow cancels that step. A wheel step stops the page from scrolling; otherwise the page scrolls. | Current tests |
121
- | FR5 | With `hasNumberSteppers`, increment and decrement buttons render at the end of the control. They are outside the tab order, are named with the localized increment and decrement phrases and the label, keep focus on the input when pressed, and expose `aria-disabled` when stepping that way cannot move or the field is disabled or read-only. | `spec:AST-085` FR1, FR2; current tests |
126
+ | FR5 | With `hasNumberSteppers`, increment and decrement buttons render at the end of the control unless the primary pointer is coarse. On coarse primary pointers the buttons are hidden from layout and the accessibility tree at every viewport width, with no opt-out; the control restores its normal themed end padding. Direct numeric entry and keyboard stepping remain available. When shown, the buttons are outside the tab order, are named with the localized increment and decrement phrases and the label, keep focus on the input when pressed, and expose `aria-disabled` when stepping that way cannot move or the field is disabled or read-only. | `spec:AST-085` FR1, FR2; current tests |
122
127
  | FR6 | With `formatValue`, the input shows the formatted committed value at rest and the plain locale-formatted number while focused. The form value is then submitted from a hidden input carrying the raw number, and the visible input has no `name`; without `formatValue` the visible input carries `name`. | Current tests |
123
128
  | FR7 | `units` renders as secondary text after the input and is part of the input's description. | Current tests |
124
129
  | FR8 | With `hasClear`, a clear button shows while `value` is not `null` and the field is editable. Activating it calls `onChange(null)`, drops the draft, and focuses the input as TextInput's clear does. The clear button is a ghost small `component:Button`, so its press scale MUST NOT apply under reduced motion (`design:reduced-motion` DR1). Implementation gap: it still scales under reduced motion. | `family:input-fields` FR11; current tests |
@@ -168,19 +173,20 @@ Consumer migration instructions belong in consumer docs and release notes.
168
173
  - **AR2 — Invalid entry.** FR2's assertive region announces an invalid draft,
169
174
  since an invalid draft reverts silently on blur (WCAG 3.3.1).
170
175
  - **AR3 — Disabled.** FR9 satisfies `spec:AST-085` FR1, FR5, and FR6.
171
- - **AR4 — Steppers.** Pointer-only aids; the arrow keys give keyboard users the
172
- same stepping (FR4, FR5).
176
+ - **AR4 — Steppers.** Compact pointer-only aids are hidden on coarse primary
177
+ pointers at every viewport width. Direct numeric entry remains available, and
178
+ arrow keys provide keyboard stepping on every pointer type (FR4, FR5).
173
179
  - **AR5 — Read-only exposure.** A read-only NumberInput MUST expose its
174
180
  read-only state to assistive technology (WCAG 2.2 4.1.2). Implementation gap:
175
181
  the spinbutton exposes no read-only state.
176
182
 
177
183
  ## Design relationships
178
184
 
179
- | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
180
- | ---------------- | --------------------------------------------------------- | ------------------------ | -------------- | ------------------ |
181
- | Input | Numeric text; secondary color while the draft is invalid. | Current source and tests | Prominent | FR2 |
182
- | Units | Secondary text after the number. | Current source and tests | Supporting | FR7 |
183
- | Steppers | Stacked increment and decrement at the end. | Current source and tests | Supporting | FR5, FR13 |
185
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
186
+ | ---------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------ | -------------- | ------------------ |
187
+ | Input | Numeric text; secondary color while the draft is invalid. | Current source and tests | Prominent | FR2 |
188
+ | Units | Secondary text after the number. | Current source and tests | Supporting | FR7 |
189
+ | Steppers | Stacked at the end on non-coarse pointers; hidden on coarse primary pointers, with normal themed end padding. | FR5; DEC-1 | Supporting | FR5, FR13 |
184
190
 
185
191
  `design:reduced-motion` governs the clear button's press scale (FR8); no other
186
192
  design record governs NumberInput. The control box carries `number-input`;
@@ -201,19 +207,20 @@ anatomy-theming map.
201
207
 
202
208
  ## Verification map
203
209
 
204
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
205
- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
206
- | FR1, AR1 | `NumberInput.test.tsx` spinbutton-attribute suite; `NumberInput.a11y.chromium.spec.ts` | Bounded, unbounded, formatted | A missing role or ARIA value fails. | `audit:NumberInput/accessibility` |
207
- | FR2, AR2 | `numberParser.test.ts`; `numberParser.docblock.test.ts`; `NumberInput.test.tsx` invalid-feedback suite | Locales, grouping, invalid text | A misparse, a commit of a guess, or a missing announcement fails. | `audit:NumberInput/behavior` |
208
- | FR3, ORD1 | `numberInputCommit.test.ts`; `NumberInput.test.tsx` validation, commit-policy, clamping, keyboard-clearing suites | Blur, Enter, empty, clamp, inverted range | A per-keystroke `onChange`, an unclamped commit, or a wrong clear fails. | `audit:NumberInput/behavior` |
209
- | FR4, FR5, ORD2, AR4, PR1 | `NumberInput.test.tsx` stepping and steppers suites; wheel-listener cases | Keys, wheel, steppers; bounds; empty; precision | A step off the grid, past a bound, or with modifiers fails. Evidence gap: no test steps from an emptied draft over a value. | `audit:NumberInput/behavior` |
210
- | FR6, FR7 | `NumberInput.test.tsx` formatted-display, form, and units suites | Rest and focus; form submission | A formatted value submitted, or units not described, fails. | `audit:NumberInput/behavior` |
211
- | FR8 | `NumberInput.test.tsx` hasClear suite | Clear; disabled; read-only | A clear on a disabled field, or no `null`, fails. Evidence gap: no test presses the clear button under reduced motion. | `audit:NumberInput/behavior` |
212
- | FR9, FR10, ORD3, AR3 | `NumberInput.test.tsx` disabled, disabledMessage, read-only, and form suites; `InputGroup.test.tsx` disabled-propagation cases | Disabled, reason, read-only | A lost tab stop, a submitted disabled value, or stepping while read-only fails. | `audit:NumberInput/accessibility` |
213
- | FR12 | `NumberInput.fr10.test.tsx`; `NumberInput.test.tsx` status and click-to-focus suites | Standalone and grouped | A base prop off the input, or styling on the wrong part, fails. | `audit:NumberInput/api` |
214
- | FR13 | `NumberInput.test.tsx` theme-state and stepper-padding suites | Each padding spelling | Steppers that ignore the theme padding fail. | `audit:NumberInput/theming` |
215
- | AR5 | `NumberInput.a11y.chromium.spec.ts` with its recorded known failure | Read-only | The known failure stops matching when the gap closes. | `audit:NumberInput/accessibility` |
216
- | FR11 | `NumberInput.test.tsx` "does not step or commit on a composing keydown (IME)" and "waits for IME composition to finish before committing" | Composing arrows and Enter | A step or commit during composition fails. | `audit:NumberInput/behavior` |
210
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
211
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
212
+ | FR1, AR1 | `NumberInput.test.tsx` spinbutton-attribute suite; `NumberInput.a11y.chromium.spec.ts` | Bounded, unbounded, formatted | A missing role or ARIA value fails. | `audit:NumberInput/accessibility` |
213
+ | FR2, AR2 | `numberParser.test.ts`; `numberParser.docblock.test.ts`; `NumberInput.test.tsx` invalid-feedback suite | Locales, grouping, invalid text | A misparse, a commit of a guess, or a missing announcement fails. | `audit:NumberInput/behavior` |
214
+ | FR3, ORD1 | `numberInputCommit.test.ts`; `NumberInput.test.tsx` validation, commit-policy, clamping, keyboard-clearing suites | Blur, Enter, empty, clamp, inverted range | A per-keystroke `onChange`, an unclamped commit, or a wrong clear fails. | `audit:NumberInput/behavior` |
215
+ | FR4, FR5, ORD2, AR4, PR1 | `NumberInput.test.tsx` stepping and steppers suites; wheel-listener cases | Keys, wheel, steppers; bounds; empty; precision | A step off the grid, past a bound, or with modifiers fails. Evidence gap: no test steps from an emptied draft over a value. | `audit:NumberInput/behavior` |
216
+ | FR6, FR7 | `NumberInput.test.tsx` formatted-display, form, and units suites | Rest and focus; form submission | A formatted value submitted, or units not described, fails. | `audit:NumberInput/behavior` |
217
+ | FR8 | `NumberInput.test.tsx` hasClear suite | Clear; disabled; read-only | A clear on a disabled field, or no `null`, fails. Evidence gap: no test presses the clear button under reduced motion. | `audit:NumberInput/behavior` |
218
+ | FR9, FR10, ORD3, AR3 | `NumberInput.test.tsx` disabled, disabledMessage, read-only, and form suites; `InputGroup.test.tsx` disabled-propagation cases | Disabled, reason, read-only | A lost tab stop, a submitted disabled value, or stepping while read-only fails. | `audit:NumberInput/accessibility` |
219
+ | FR12 | `NumberInput.fr10.test.tsx`; `NumberInput.test.tsx` status and click-to-focus suites | Standalone and grouped | A base prop off the input, or styling on the wrong part, fails. | `audit:NumberInput/api` |
220
+ | FR13 | `NumberInput.test.tsx` theme-state and stepper-padding suites | Each padding spelling | Steppers that ignore the theme padding fail. | `audit:NumberInput/theming` |
221
+ | AR5 | `NumberInput.a11y.chromium.spec.ts` with its recorded known failure | Read-only | The known failure stops matching when the gap closes. | `audit:NumberInput/accessibility` |
222
+ | FR11 | `NumberInput.test.tsx` "does not step or commit on a composing keydown (IME)" and "waits for IME composition to finish before committing" | Composing arrows and Enter | A step or commit during composition fails. | `audit:NumberInput/behavior` |
223
+ | FR5, FR13, AR4 | `NumberInput.coarse-pointer.a11y.chromium.spec.ts` | Coarse/non-coarse pointers at 390 and 1280 px, LTR/RTL, non-default themed end padding | Visible coarse-pointer steppers, hidden fine-pointer steppers, wrong end padding, or lost direct/keyboard entry fail. | `audit:NumberInput/behavior` |
217
224
 
218
225
  ## Current-state impact
219
226
 
@@ -235,8 +242,23 @@ anatomy-theming map.
235
242
 
236
243
  ## Decision log
237
244
 
238
- No component-local decision. NumberInput applies `family:input-fields` and
239
- `spec:AST-085`.
245
+ NumberInput applies `family:input-fields` and `spec:AST-085`.
246
+
247
+ ### DEC-1 — Compact steppers follow primary-pointer precision
248
+
249
+ **Reference:** `component:NumberInput/DEC-1`
250
+ **Decider:** `imdreamrunner`, `2026-10-29`
251
+
252
+ Coarse-primary-pointer presentation uses direct numeric entry rather than
253
+ compact steppers, independently of viewport width and without an opt-out.
254
+ Non-coarse-pointer steppers and keyboard stepping retain their contracts.
255
+ The compact buttons are presentation aids: the numeric value, validation,
256
+ callback, direct-entry, and keyboard-stepping contracts do not depend on their
257
+ visibility. Adapting those aids requires no caller source or configuration
258
+ migration.
259
+
260
+ Rejected: width-based activation, because pointer precision governs this
261
+ control affordance rather than the available layout width.
240
262
 
241
263
  ## Open questions
242
264