@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
@@ -87,23 +87,30 @@ async function runState(
87
87
  });
88
88
  }
89
89
 
90
- test('every window-splitter expectation is exercised by a ResizeHandle state', async ({
91
- page,
92
- }) => {
93
- // One test mounts and checks every binding state in turn: about 20s on an
94
- // idle machine, three times that when the contract run shares a runner's
95
- // cores, so the default 60s ran out mid-loop on whichever state it reached.
96
- // Budget it like the other whole-inventory sweeps (Button, Checkbox).
97
- test.setTimeout(3 * 60 * 1000);
98
- const cdp = await page.context().newCDPSession(page);
90
+ // Checking coverage runs every state again, which on a loaded runner nears
91
+ // the per-test timeout as one test. It runs as one serial part per state,
92
+ // sharing their results, then one check over all of them.
93
+ test.describe('every window-splitter expectation is exercised by a ResizeHandle state', () => {
94
+ test.describe.configure({mode: 'serial'});
99
95
  const results: BindingResult[] = [];
96
+
100
97
  for (const state of RESIZE_HANDLE_BINDING_STATES) {
101
- results.push(await runState(page, cdp, state));
98
+ test(`runs ${state.id}`, async ({page}) => {
99
+ const cdp = await page.context().newCDPSession(page);
100
+ results.push(await runState(page, cdp, state));
101
+ });
102
102
  }
103
- expect(neverExercised(results)).toEqual([]);
104
- expect(unmatchedKnownFailures(RESIZE_HANDLE_KNOWN_FAILURES, results)).toEqual(
105
- [],
106
- );
103
+
104
+ test('no expectation is left unexercised', () => {
105
+ // Every part ran in this worker; this test alone proves nothing.
106
+ expect(results.map(result => result.state).sort()).toEqual(
107
+ RESIZE_HANDLE_BINDING_STATES.map(state => state.id).sort(),
108
+ );
109
+ expect(neverExercised(results)).toEqual([]);
110
+ expect(
111
+ unmatchedKnownFailures(RESIZE_HANDLE_KNOWN_FAILURES, results),
112
+ ).toEqual([]);
113
+ });
107
114
  });
108
115
 
109
116
  for (const state of RESIZE_HANDLE_BINDING_STATES) {
@@ -1680,3 +1680,46 @@ describe('ResizableProps source compatibility (AST-010 API5)', () => {
1680
1680
  expect(prePr._onResizeCancel).toBeUndefined();
1681
1681
  });
1682
1682
  });
1683
+
1684
+ describe('useResizable window reads after a commit', () => {
1685
+ // React re-reads a useSyncExternalStore snapshot after the commit whenever
1686
+ // getSnapshot is a new function, and a window size read after the DOM has
1687
+ // changed forces a layout of the whole page. A re-render with unchanged
1688
+ // inputs reads the window during render only.
1689
+ function readsAfterCommit(direction: 'horizontal' | 'vertical') {
1690
+ const reads = {count: 0};
1691
+ const width = vi.spyOn(window, 'innerWidth', 'get');
1692
+ const height = vi.spyOn(window, 'innerHeight', 'get');
1693
+ width.mockImplementation(() => {
1694
+ reads.count += 1;
1695
+ return 1024;
1696
+ });
1697
+ height.mockImplementation(() => {
1698
+ reads.count += 1;
1699
+ return 768;
1700
+ });
1701
+ function Region({tick}: {tick: number}) {
1702
+ useResizable({defaultSize: 240, direction});
1703
+ // Runs in the commit, before passive effects: every read counted
1704
+ // after it happened after the DOM changed.
1705
+ useLayoutEffect(() => {
1706
+ reads.count = 0;
1707
+ });
1708
+ return createElement('div', {'data-tick': tick});
1709
+ }
1710
+ const {rerender} = render(createElement(Region, {tick: 0}));
1711
+ act(() => rerender(createElement(Region, {tick: 1})));
1712
+ const count = reads.count;
1713
+ width.mockRestore();
1714
+ height.mockRestore();
1715
+ return count;
1716
+ }
1717
+
1718
+ it('reads the window width again only during render, not after the commit', () => {
1719
+ expect(readsAfterCommit('horizontal')).toBe(0);
1720
+ });
1721
+
1722
+ it('reads the window height again only during render, not after the commit', () => {
1723
+ expect(readsAfterCommit('vertical')).toBe(0);
1724
+ });
1725
+ });
@@ -306,6 +306,34 @@ const SERVER_BASIS = 1200;
306
306
 
307
307
  /** A store that never changes on its own; the snapshot is re-read on render. */
308
308
  const noopSubscribe = () => () => {};
309
+
310
+ // The window snapshots are module functions, so their identity is stable:
311
+ // React re-reads a snapshot after the commit whenever getSnapshot is a new
312
+ // function, and a window size read after the DOM changed forces a layout of
313
+ // the whole page. With these, the window is read during render only.
314
+
315
+ /** The viewport width, the basis without a container. */
316
+ function readViewportBasis(): number {
317
+ return typeof window === 'undefined' ? SERVER_BASIS : window.innerWidth;
318
+ }
319
+
320
+ function readServerBasis(): number {
321
+ return SERVER_BASIS;
322
+ }
323
+
324
+ /** The window's extent along a horizontal hook's axis. */
325
+ function readWindowWidth(): number | null {
326
+ return typeof window === 'undefined' ? null : window.innerWidth;
327
+ }
328
+
329
+ /** The window's extent along a vertical hook's axis. */
330
+ function readWindowHeight(): number | null {
331
+ return typeof window === 'undefined' ? null : window.innerHeight;
332
+ }
333
+
334
+ function readNoWindowExtent(): null {
335
+ return null;
336
+ }
309
337
  const DEFAULT_SIZE_FALLBACK = 250;
310
338
  const RESIZABLE_SIZE_GUIDANCE =
311
339
  'Use a non-negative number of pixels, an exact "Npx" string, an exact ' +
@@ -698,8 +726,8 @@ function useSingleResizable(config: UseResizableSingleConfig): ResizableRegion {
698
726
  );
699
727
  const viewportBasis = useSyncExternalStore(
700
728
  subscribeToViewport,
701
- () => (typeof window === 'undefined' ? SERVER_BASIS : window.innerWidth),
702
- () => SERVER_BASIS,
729
+ readViewportBasis,
730
+ readServerBasis,
703
731
  );
704
732
 
705
733
  // The window's extent along the hook's axis, for the REPORTED maximum of an
@@ -710,13 +738,8 @@ function useSingleResizable(config: UseResizableSingleConfig): ResizableRegion {
710
738
  // (component:Resizable Performance and resources).
711
739
  const windowExtent = useSyncExternalStore(
712
740
  noopSubscribe,
713
- () =>
714
- typeof window === 'undefined'
715
- ? null
716
- : direction === 'vertical'
717
- ? window.innerHeight
718
- : window.innerWidth,
719
- () => null,
741
+ direction === 'vertical' ? readWindowHeight : readWindowWidth,
742
+ readNoWindowExtent,
720
743
  );
721
744
 
722
745
  // With a container, its content box is the basis; without one, the released
@@ -562,6 +562,90 @@ describe('SegmentedControl disabled state', () => {
562
562
  expect(handleChange).not.toHaveBeenCalled();
563
563
  });
564
564
 
565
+ it("does not run a caller's onClick on a disabled segment", async () => {
566
+ const user = userEvent.setup({pointerEventsCheck: 0});
567
+ const handleClick = vi.fn();
568
+ const handleChange = vi.fn();
569
+
570
+ render(
571
+ <SegmentedControl value="grid" onChange={handleChange} label="View mode">
572
+ <SegmentedControlItem value="grid" label="Grid" />
573
+ <SegmentedControlItem
574
+ value="list"
575
+ label="List"
576
+ isDisabled
577
+ onClick={handleClick}
578
+ />
579
+ </SegmentedControl>,
580
+ );
581
+
582
+ await user.click(screen.getByRole('radio', {name: 'List'}));
583
+ expect(handleClick).not.toHaveBeenCalled();
584
+ expect(handleChange).not.toHaveBeenCalled();
585
+ });
586
+
587
+ it("does not run a caller's onClick when the whole group is disabled", async () => {
588
+ const user = userEvent.setup({pointerEventsCheck: 0});
589
+ const handleClick = vi.fn();
590
+
591
+ render(
592
+ <SegmentedControl
593
+ value="grid"
594
+ onChange={() => {}}
595
+ label="View mode"
596
+ isDisabled>
597
+ <SegmentedControlItem value="grid" label="Grid" />
598
+ <SegmentedControlItem value="list" label="List" onClick={handleClick} />
599
+ </SegmentedControl>,
600
+ );
601
+
602
+ await user.click(screen.getByRole('radio', {name: 'List'}));
603
+ expect(handleClick).not.toHaveBeenCalled();
604
+ });
605
+
606
+ it("does not run a caller's onClick on a focusable disabled segment", async () => {
607
+ // A group disabled with a reason keeps its selected segment in the tab
608
+ // order, so Enter and Space can reach it and synthesize a click.
609
+ const user = userEvent.setup({pointerEventsCheck: 0});
610
+ const handleClick = vi.fn();
611
+
612
+ render(
613
+ <SegmentedControl
614
+ value="grid"
615
+ onChange={() => {}}
616
+ label="View mode"
617
+ isDisabled
618
+ disabledMessage="Choose a project to switch views">
619
+ <SegmentedControlItem value="grid" label="Grid" onClick={handleClick} />
620
+ <SegmentedControlItem value="list" label="List" />
621
+ </SegmentedControl>,
622
+ );
623
+
624
+ const selected = screen.getByRole('radio', {name: 'Grid'});
625
+ selected.focus();
626
+ expect(selected).toHaveFocus();
627
+ await user.keyboard('{Enter}');
628
+ await user.keyboard(' ');
629
+ expect(handleClick).not.toHaveBeenCalled();
630
+ });
631
+
632
+ it("still runs a caller's onClick on an enabled segment", async () => {
633
+ const user = userEvent.setup({pointerEventsCheck: 0});
634
+ const handleClick = vi.fn();
635
+ const handleChange = vi.fn();
636
+
637
+ render(
638
+ <SegmentedControl value="grid" onChange={handleChange} label="View mode">
639
+ <SegmentedControlItem value="grid" label="Grid" />
640
+ <SegmentedControlItem value="list" label="List" onClick={handleClick} />
641
+ </SegmentedControl>,
642
+ );
643
+
644
+ await user.click(screen.getByRole('radio', {name: 'List'}));
645
+ expect(handleClick).toHaveBeenCalledTimes(1);
646
+ expect(handleChange).toHaveBeenCalledWith('list');
647
+ });
648
+
565
649
  it('skips disabled items during keyboard navigation', async () => {
566
650
  const user = userEvent.setup();
567
651
  const handleChange = vi.fn();
@@ -210,11 +210,17 @@ export function SegmentedControlItem({
210
210
 
211
211
  // Consumer-first: a consumer onClick can call preventDefault() to opt out of
212
212
  // selection; otherwise selection proceeds when enabled and not selected.
213
- const handleClick = composeEventHandlers(onClickProp, () => {
214
- if (!isItemDisabled && !isSelected) {
215
- ctx.onChange(value);
216
- }
217
- });
213
+ // A disabled segment invokes no caller callback (spec:AST-085 FR2), so the
214
+ // consumer onClick is left out while it is disabled, including the selected
215
+ // segment of a group disabled with a reason, which stays focusable.
216
+ const handleClick = composeEventHandlers(
217
+ isItemDisabled ? undefined : onClickProp,
218
+ () => {
219
+ if (!isItemDisabled && !isSelected) {
220
+ ctx.onChange(value);
221
+ }
222
+ },
223
+ );
218
224
 
219
225
  const iconElement = icon ? (
220
226
  <span {...stylex.props(styles.icon, iconSizeStyles[size])}>{icon}</span>
@@ -9,7 +9,15 @@
9
9
  * SYNC: When Selector.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 {readFileSync} from 'node:fs';
14
22
  import {
15
23
  act,
@@ -61,6 +69,15 @@ function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
61
69
  }
62
70
 
63
71
  // Mock showPopover and hidePopover methods since they're not implemented in jsdom
72
+ // The bottom-sheet presentation renders BottomSheet through React.lazy. Its
73
+ // first import transforms the BottomSheet module tree, which on a loaded
74
+ // runner takes longer than a query's 1 s wait, so the first sheet test timed
75
+ // out before the dialog existed. Load the module once up front: lazy() then
76
+ // resolves from the module cache and every sheet test waits only on the open.
77
+ beforeAll(async () => {
78
+ await import('../BottomSheet/BottomSheet');
79
+ });
80
+
64
81
  beforeEach(() => {
65
82
  // The live regions are a document-level singleton; start each test clean.
66
83
  __resetLiveRegionsForTest();
@@ -276,6 +293,13 @@ function expectSilentBusySpinner() {
276
293
  expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
277
294
  }
278
295
 
296
+ // The bottom sheet is loaded on demand (SelectorBottomSheet's lazy import).
297
+ // Loading its module is not part of an open, so the file waits for it once
298
+ // here, and each test's open waits only for the open.
299
+ beforeAll(async () => {
300
+ await import('../BottomSheet/BottomSheet');
301
+ });
302
+
279
303
  describe('Selector', () => {
280
304
  it('uses a bottom sheet and closes after a selection when requested', async () => {
281
305
  const user = userEvent.setup();
@@ -4271,6 +4295,71 @@ describe('Selector popup theme target', () => {
4271
4295
  });
4272
4296
  });
4273
4297
 
4298
+ describe('Selector light dismiss focus (spec:Popover AR4)', () => {
4299
+ function closeByLightDismiss() {
4300
+ const popover = document.querySelector('[popover]') as HTMLElement;
4301
+ act(() => {
4302
+ popover.dispatchEvent(
4303
+ Object.assign(new Event('toggle'), {
4304
+ oldState: 'open',
4305
+ newState: 'closed',
4306
+ }),
4307
+ );
4308
+ });
4309
+ }
4310
+
4311
+ it('leaves focus on the field a press moved it to', async () => {
4312
+ const user = userEvent.setup();
4313
+ render(
4314
+ <>
4315
+ <Selector
4316
+ label="Fruit"
4317
+ options={OPTIONS}
4318
+ value="Banana"
4319
+ onChange={() => {}}
4320
+ placement="below"
4321
+ />
4322
+ <input aria-label="Notes" />
4323
+ </>,
4324
+ );
4325
+ const trigger = screen.getByRole('combobox');
4326
+ await user.click(trigger);
4327
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
4328
+
4329
+ // The press on another field focuses it before the browser's light
4330
+ // dismiss closes the list.
4331
+ const notes = screen.getByRole('textbox', {name: 'Notes'});
4332
+ act(() => {
4333
+ notes.focus();
4334
+ });
4335
+ closeByLightDismiss();
4336
+
4337
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
4338
+ expect(notes).toHaveFocus();
4339
+ });
4340
+
4341
+ it('returns focus to the trigger when the press lands on nothing focusable', async () => {
4342
+ const user = userEvent.setup();
4343
+ render(
4344
+ <Selector
4345
+ label="Fruit"
4346
+ options={OPTIONS}
4347
+ value="Banana"
4348
+ onChange={() => {}}
4349
+ placement="below"
4350
+ />,
4351
+ );
4352
+ const trigger = screen.getByRole('combobox');
4353
+ await user.click(trigger);
4354
+ act(() => {
4355
+ (document.activeElement as HTMLElement | null)?.blur();
4356
+ });
4357
+ closeByLightDismiss();
4358
+
4359
+ expect(trigger).toHaveFocus();
4360
+ });
4361
+ });
4362
+
4274
4363
  describe('Selector option-row theme target', () => {
4275
4364
  const ROW_OPTIONS = ['Apple', 'Banana', 'Orange'];
4276
4365
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  import {cleanup, render, screen, waitFor, within} from '@testing-library/react';
6
6
  import userEvent from '@testing-library/user-event';
7
- import {describe, expect, it} from 'vitest';
7
+ import {beforeAll, describe, expect, it} from 'vitest';
8
8
  import {
9
9
  COMBOBOX_PATTERN,
10
10
  expectAccessibilitySpec,
@@ -20,6 +20,13 @@ import {SELECTOR_COMBOBOX_KNOWN_FAILURES} from './Combobox.a11y.known-failures';
20
20
 
21
21
  installListboxDialogStubs();
22
22
 
23
+ // The bottom sheet is loaded on demand (SelectorBottomSheet's lazy import).
24
+ // Loading its module is not part of an open, so the file waits for it once
25
+ // here, and each scenario's open waits only for the open.
26
+ beforeAll(async () => {
27
+ await import('../../BottomSheet/BottomSheet');
28
+ });
29
+
23
30
  const hidden = {hidden: true} as const;
24
31
 
25
32
  function subjectFor(scenario: SelectorComboboxScenario): HTMLElement {
@@ -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,
@@ -20,6 +20,13 @@ import {installListboxDialogStubs} from './Listbox.a11y.dom';
20
20
 
21
21
  installListboxDialogStubs();
22
22
 
23
+ // The bottom sheet is loaded on demand (SelectorBottomSheet's lazy import).
24
+ // Loading its module is not part of an open, so the file waits for it once
25
+ // here, and each scenario's open waits only for the open.
26
+ beforeAll(async () => {
27
+ await import('../../BottomSheet/BottomSheet');
28
+ });
29
+
23
30
  const scenarios = LISTBOX_SCENARIOS.filter(
24
31
  scenario => scenario.component === 'Selector',
25
32
  );
@@ -34,6 +34,13 @@ interface UseSelectorPresentationOptions {
34
34
  onShow: () => void;
35
35
  popoverOptions: Omit<UsePopoverOptions, 'onHide' | 'onShow'>;
36
36
  triggerRef: RefObject<HTMLElement | null>;
37
+ /**
38
+ * Return focus to the trigger on every close, even when a press already
39
+ * moved focus to another element. A caller-rendered trigger needs this
40
+ * (spec:AST-055 AR3); the built-in trigger leaves focus where the press put
41
+ * it (spec:Popover AR4).
42
+ */
43
+ isFocusReturnAlways?: boolean;
37
44
  }
38
45
 
39
46
  interface SelectorPresentationController {
@@ -55,6 +62,7 @@ export function useSelectorPresentation({
55
62
  onShow,
56
63
  popoverOptions,
57
64
  triggerRef,
65
+ isFocusReturnAlways = false,
58
66
  }: UseSelectorPresentationOptions): SelectorPresentationController {
59
67
  const resolvedPresentation = useAdaptivePresentation(presentation);
60
68
  const activePresentationRef =
@@ -68,6 +76,9 @@ export function useSelectorPresentation({
68
76
  const onHideRef = useRef(onHide);
69
77
  const onShowRef = useRef(onShow);
70
78
  const didShowPopoverRef = useRef(false);
79
+ // The open list's container, read when the popover hides to tell focus that
80
+ // is still inside the list from focus that moved to another element.
81
+ const popoverContentRef = useRef<HTMLDivElement | null>(null);
71
82
  const {
72
83
  isFocusRingSuppressed,
73
84
  onFocusReturnTargetFocus,
@@ -78,10 +89,26 @@ export function useSelectorPresentation({
78
89
  onShowRef.current = onShow;
79
90
 
80
91
  const handlePopoverHide = useCallback(() => {
81
- prepareFocusReturn();
82
92
  onHideRef.current();
83
- triggerRef.current?.focus();
84
- }, [prepareFocusReturn, triggerRef]);
93
+ const trigger = triggerRef.current;
94
+ // A light dismiss caused by pressing another focusable element has already
95
+ // moved focus there; taking it back would leave the next key on this
96
+ // closed Selector (spec:Popover AR4 returns focus only when it would
97
+ // otherwise be lost). Escape, a choice in the list, or a press on nothing
98
+ // focusable still returns focus to the trigger.
99
+ const active = document.activeElement;
100
+ const focusLandedElsewhere =
101
+ !isFocusReturnAlways &&
102
+ active instanceof HTMLElement &&
103
+ active !== document.body &&
104
+ active !== trigger &&
105
+ !popoverContentRef.current?.contains(active);
106
+ if (focusLandedElsewhere) {
107
+ return;
108
+ }
109
+ prepareFocusReturn();
110
+ trigger?.focus();
111
+ }, [isFocusReturnAlways, prepareFocusReturn, triggerRef]);
85
112
  const handlePopoverShow = useCallback(() => {
86
113
  didShowPopoverRef.current = true;
87
114
  resetFocusReturn();
@@ -92,6 +119,7 @@ export function useSelectorPresentation({
92
119
  onHide: handlePopoverHide,
93
120
  onShow: handlePopoverShow,
94
121
  });
122
+ popoverContentRef.current = popover.contentRef.current;
95
123
  const {hide: hidePopover, show: showPopover} = popover;
96
124
 
97
125
  const show = useCallback((): boolean => {
@@ -492,6 +492,33 @@ describe('SideNavHeading', () => {
492
492
  );
493
493
  });
494
494
 
495
+ it('moves focus one item per arrow key in a NavHeadingMenu (APG menu)', async () => {
496
+ __resetMenuPressForTest();
497
+ const user = userEvent.setup();
498
+ render(
499
+ <SideNavHeading
500
+ heading="My App"
501
+ menu={
502
+ <NavHeadingMenu>
503
+ <NavHeadingMenuItem label="One" onClick={() => {}} />
504
+ <NavHeadingMenuItem label="Two" onClick={() => {}} />
505
+ <NavHeadingMenuItem label="Three" onClick={() => {}} />
506
+ </NavHeadingMenu>
507
+ }
508
+ />,
509
+ );
510
+ await user.click(screen.getByRole('button', {name: 'Open menu'}));
511
+ const item = (name: string) =>
512
+ screen.getByRole('menuitem', {name, hidden: true});
513
+ item('One').focus();
514
+ fireEvent.keyDown(item('One'), {key: 'ArrowDown'});
515
+ expect(item('Two')).toHaveFocus();
516
+ fireEvent.keyDown(item('Two'), {key: 'ArrowDown'});
517
+ expect(item('Three')).toHaveFocus();
518
+ fireEvent.keyDown(item('Three'), {key: 'ArrowUp'});
519
+ expect(item('Two')).toHaveFocus();
520
+ });
521
+
495
522
  it('closes when a mouse pressed on a NavHeadingMenu item is released outside the menu (useMenuPress FR3)', async () => {
496
523
  __resetMenuPressForTest();
497
524
  const user = userEvent.setup();