@astryxdesign/core 0.7.5 → 0.7.6-canary.00507ba

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 (595) 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/AppShell/AppShell.d.ts.map +1 -1
  5. package/dist/AppShell/AppShell.js +183 -5
  6. package/dist/AppShell/observeMainOverflow.d.ts +9 -0
  7. package/dist/AppShell/observeMainOverflow.d.ts.map +1 -0
  8. package/dist/AppShell/observeMainOverflow.js +125 -0
  9. package/dist/Avatar/Avatar.d.ts.map +1 -1
  10. package/dist/Avatar/Avatar.js +12 -3
  11. package/dist/Badge/Badge.d.ts.map +1 -1
  12. package/dist/Badge/Badge.js +7 -1
  13. package/dist/BottomSheet/BottomSheet.js +1 -1
  14. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  15. package/dist/BottomSheet/BottomSheetPanel.js +2 -1
  16. package/dist/BottomSheet/useSheetGestures.d.ts +12 -0
  17. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  18. package/dist/BottomSheet/useSheetGestures.js +30 -3
  19. package/dist/Button/Button.js +4 -4
  20. package/dist/Carousel/Carousel.d.ts +3 -1
  21. package/dist/Carousel/Carousel.d.ts.map +1 -1
  22. package/dist/Carousel/Carousel.js +12 -3
  23. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  24. package/dist/CheckboxInput/CheckboxInput.js +6 -5
  25. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  26. package/dist/ClickableCard/ClickableCard.js +37 -4
  27. package/dist/DateInput/DateInput.d.ts.map +1 -1
  28. package/dist/DateInput/DateInput.js +12 -4
  29. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  30. package/dist/DateInput/MonthScroller.js +101 -15
  31. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  32. package/dist/DateInput/TouchDateField.js +8 -3
  33. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  34. package/dist/DateRangeInput/DateRangeInput.js +1 -1
  35. package/dist/DateTimeInput/DateTimeInput.d.ts +5 -2
  36. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  37. package/dist/DateTimeInput/DateTimeInput.js +23 -12
  38. package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
  39. package/dist/DateTimeInput/NativeTimeSegment.js +5 -4
  40. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  41. package/dist/DateTimeInput/TouchDateTimeField.js +4 -3
  42. package/dist/Dialog/Dialog.d.ts.map +1 -1
  43. package/dist/Dialog/Dialog.js +34 -9
  44. package/dist/Dialog/DialogHeader.d.ts.map +1 -1
  45. package/dist/Dialog/DialogHeader.js +5 -4
  46. package/dist/Divider/Divider.d.ts.map +1 -1
  47. package/dist/Divider/Divider.js +2 -1
  48. package/dist/Drawer/Drawer.d.ts +1 -0
  49. package/dist/Drawer/Drawer.d.ts.map +1 -1
  50. package/dist/Drawer/Drawer.js +7 -2
  51. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  52. package/dist/DropdownMenu/DropdownMenu.js +7 -3
  53. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -1
  54. package/dist/DropdownMenu/MenuBottomSheetActionList.js +6 -5
  55. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +20 -2
  56. package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -1
  57. package/dist/DropdownMenu/lazyMenuBottomSheet.js +31 -3
  58. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -1
  59. package/dist/DropdownMenu/renderDropdownItems.js +21 -16
  60. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  61. package/dist/EmptyState/EmptyState.js +2 -1
  62. package/dist/Field/FieldLabel.d.ts +1 -1
  63. package/dist/Field/FieldLabel.d.ts.map +1 -1
  64. package/dist/Field/FieldLabel.js +4 -4
  65. package/dist/Field/InputClearButton.js +3 -3
  66. package/dist/Field/PanelSearchInput.js +2 -2
  67. package/dist/FieldStatus/FieldStatus.js +2 -2
  68. package/dist/FileInput/FileInput.d.ts.map +1 -1
  69. package/dist/FileInput/FileInput.js +7 -1
  70. package/dist/Heading/Heading.js +1 -1
  71. package/dist/Icon/Icon.js +1 -1
  72. package/dist/Indicator/CheckIndicator.d.ts.map +1 -1
  73. package/dist/Indicator/CheckIndicator.js +2 -1
  74. package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
  75. package/dist/Indicator/CheckboxIndicator.js +3 -1
  76. package/dist/Indicator/RadioIndicator.d.ts.map +1 -1
  77. package/dist/Indicator/RadioIndicator.js +3 -1
  78. package/dist/Item/Item.d.ts.map +1 -1
  79. package/dist/Item/Item.js +2 -1
  80. package/dist/Item/ItemSwipeLayer.js +1 -1
  81. package/dist/Layer/popoverOperations.d.ts +27 -0
  82. package/dist/Layer/popoverOperations.d.ts.map +1 -0
  83. package/dist/Layer/popoverOperations.js +159 -0
  84. package/dist/Layer/useLayer.d.ts +1 -1
  85. package/dist/Layer/useLayer.d.ts.map +1 -1
  86. package/dist/Layer/useLayer.js +93 -59
  87. package/dist/Layout/Layout.js +1 -1
  88. package/dist/Layout/LayoutContent.js +1 -1
  89. package/dist/Layout/LayoutFooter.js +1 -1
  90. package/dist/Layout/LayoutHeader.js +1 -1
  91. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  92. package/dist/Lightbox/Lightbox.js +5 -0
  93. package/dist/Lightbox/LightboxViewport.a11y.browser.spec.js +5 -4
  94. package/dist/List/List.js +1 -1
  95. package/dist/List/ListItem.js +3 -3
  96. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  97. package/dist/MobileNav/MobileNav.js +7 -2
  98. package/dist/MultiSelector/CreateRow.a11y.chromium.spec.js +7 -6
  99. package/dist/MultiSelector/MultiSelector.d.ts +1 -1
  100. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  101. package/dist/MultiSelector/MultiSelector.js +26 -10
  102. package/dist/NumberInput/NumberInput.d.ts +3 -1
  103. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  104. package/dist/NumberInput/NumberInput.js +3 -3
  105. package/dist/NumberInput/numberParser.d.ts +3 -0
  106. package/dist/NumberInput/numberParser.d.ts.map +1 -1
  107. package/dist/NumberInput/numberParser.js +7 -44
  108. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  109. package/dist/ProgressBar/ProgressBar.js +16 -12
  110. package/dist/Resizable/useResizable.d.ts.map +1 -1
  111. package/dist/Resizable/useResizable.js +28 -2
  112. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  113. package/dist/SegmentedControl/SegmentedControlItem.js +4 -1
  114. package/dist/Selector/useSelectorPresentation.d.ts +8 -1
  115. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -1
  116. package/dist/Selector/useSelectorPresentation.js +20 -4
  117. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  118. package/dist/Skeleton/Skeleton.js +1 -1
  119. package/dist/Slider/Slider.d.ts.map +1 -1
  120. package/dist/Slider/Slider.js +10 -2
  121. package/dist/Spinner/Spinner.d.ts.map +1 -1
  122. package/dist/Spinner/Spinner.js +3 -3
  123. package/dist/Stack/Stack.js +1 -1
  124. package/dist/Stack/StackItem.js +1 -1
  125. package/dist/TabList/TabList.d.ts.map +1 -1
  126. package/dist/TabList/TabList.js +16 -1
  127. package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
  128. package/dist/Table/plugins/columnResize/useTableColumnResize.js +9 -1
  129. package/dist/Text/Text.js +1 -1
  130. package/dist/Text/useTruncation.d.ts.map +1 -1
  131. package/dist/Text/useTruncation.js +23 -5
  132. package/dist/TextArea/TextArea.d.ts +1 -1
  133. package/dist/TextArea/TextArea.d.ts.map +1 -1
  134. package/dist/TextArea/TextArea.js +13 -2
  135. package/dist/TextInput/TextInput.d.ts.map +1 -1
  136. package/dist/TextInput/TextInput.js +3 -3
  137. package/dist/TimeInput/TimeInput.d.ts +6 -2
  138. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  139. package/dist/TimeInput/TimeInput.js +21 -14
  140. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  141. package/dist/TimeInput/TouchTimeField.js +5 -4
  142. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  143. package/dist/Timestamp/Timestamp.js +2 -2
  144. package/dist/Timestamp/TimestampHoverCard.js +4 -4
  145. package/dist/Toast/Toast.d.ts.map +1 -1
  146. package/dist/Toast/Toast.js +60 -20
  147. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  148. package/dist/Toast/ToastViewport.js +82 -51
  149. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  150. package/dist/Tokenizer/Tokenizer.js +13 -2
  151. package/dist/Tokenizer/TokenizerBottomSheet.js +3 -3
  152. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  153. package/dist/Toolbar/Toolbar.js +2 -1
  154. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  155. package/dist/TopNav/TopNavMegaMenu.js +3 -0
  156. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  157. package/dist/TopNav/TopNavMenu.js +1 -0
  158. package/dist/Typeahead/Typeahead.d.ts +1 -1
  159. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  160. package/dist/Typeahead/Typeahead.js +9 -1
  161. package/dist/astryx.css +21 -4
  162. package/dist/gestures/GestureArena.d.ts +6 -0
  163. package/dist/gestures/GestureArena.d.ts.map +1 -1
  164. package/dist/gestures/GestureArena.js +39 -0
  165. package/dist/gestures/GestureHost.d.ts +352 -0
  166. package/dist/gestures/GestureHost.d.ts.map +1 -0
  167. package/dist/gestures/GestureHost.js +1773 -0
  168. package/dist/gestures/GestureRecognizer.d.ts +10 -0
  169. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  170. package/dist/gestures/GestureRecognizer.js +16 -0
  171. package/dist/gestures/LongPressGestureRecognizer.d.ts +1 -0
  172. package/dist/gestures/LongPressGestureRecognizer.d.ts.map +1 -1
  173. package/dist/gestures/LongPressGestureRecognizer.js +3 -0
  174. package/dist/gestures/PanGestureRecognizer.d.ts +1 -0
  175. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  176. package/dist/gestures/PanGestureRecognizer.js +3 -0
  177. package/dist/gestures/PinchGestureRecognizer.d.ts +31 -0
  178. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -0
  179. package/dist/gestures/PinchGestureRecognizer.js +128 -0
  180. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +71 -0
  181. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -0
  182. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +223 -0
  183. package/dist/gestures/SwipeGestureRecognizer.d.ts +37 -0
  184. package/dist/gestures/SwipeGestureRecognizer.d.ts.map +1 -0
  185. package/dist/gestures/SwipeGestureRecognizer.js +148 -0
  186. package/dist/gestures/TapGestureRecognizer.d.ts +1 -0
  187. package/dist/gestures/TapGestureRecognizer.d.ts.map +1 -1
  188. package/dist/gestures/TapGestureRecognizer.js +3 -0
  189. package/dist/gestures/VelocityTracker.d.ts +6 -2
  190. package/dist/gestures/VelocityTracker.d.ts.map +1 -1
  191. package/dist/gestures/VelocityTracker.js +7 -3
  192. package/dist/gestures/haptics.d.ts +65 -0
  193. package/dist/gestures/haptics.d.ts.map +1 -0
  194. package/dist/gestures/haptics.js +182 -0
  195. package/dist/gestures/nativeScroll.d.ts +42 -0
  196. package/dist/gestures/nativeScroll.d.ts.map +1 -0
  197. package/dist/gestures/nativeScroll.js +228 -0
  198. package/dist/gestures/touchAction.d.ts +60 -0
  199. package/dist/gestures/touchAction.d.ts.map +1 -0
  200. package/dist/gestures/touchAction.js +157 -0
  201. package/dist/gestures/touchActionRule.d.ts +34 -0
  202. package/dist/gestures/touchActionRule.d.ts.map +1 -0
  203. package/dist/gestures/touchActionRule.js +140 -0
  204. package/dist/gestures/types.d.ts +18 -7
  205. package/dist/gestures/types.d.ts.map +1 -1
  206. package/dist/gestures/types.js +22 -2
  207. package/dist/hooks/index.d.ts +1 -1
  208. package/dist/hooks/index.d.ts.map +1 -1
  209. package/dist/hooks/modalAnnounce.d.ts.map +1 -1
  210. package/dist/hooks/modalAnnounce.js +107 -61
  211. package/dist/hooks/scrollKeyboardDelegation.d.ts.map +1 -1
  212. package/dist/hooks/scrollKeyboardDelegation.js +7 -3
  213. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -1
  214. package/dist/hooks/useAdaptivePresentation.js +4 -1
  215. package/dist/hooks/useAnnounce.d.ts +101 -42
  216. package/dist/hooks/useAnnounce.d.ts.map +1 -1
  217. package/dist/hooks/useAnnounce.js +161 -75
  218. package/dist/hooks/useAnnounceRenderedText.d.ts +11 -3
  219. package/dist/hooks/useAnnounceRenderedText.d.ts.map +1 -1
  220. package/dist/hooks/useAnnounceRenderedText.js +5 -3
  221. package/dist/hooks/useBusyAnnouncement.d.ts.map +1 -1
  222. package/dist/hooks/useBusyAnnouncement.js +6 -12
  223. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  224. package/dist/hooks/useMenuHover.js +12 -1
  225. package/dist/hooks/useScrollOverflow.d.ts +2 -1
  226. package/dist/hooks/useScrollOverflow.d.ts.map +1 -1
  227. package/dist/hooks/useScrollOverflow.js +56 -10
  228. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  229. package/dist/i18n/generated-locales/en.generated.js +6 -0
  230. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  231. package/dist/i18n/generated-locales/pseudo.generated.js +6 -0
  232. package/dist/motion/animator.d.ts +7 -1
  233. package/dist/motion/animator.d.ts.map +1 -1
  234. package/dist/motion/animator.js +119 -27
  235. package/dist/theme/defineTheme.d.ts +19 -5
  236. package/dist/theme/defineTheme.d.ts.map +1 -1
  237. package/dist/theme/defineTheme.js +64 -4
  238. package/dist/theme/domainTokens/domainTokenDefaults.d.ts +3 -0
  239. package/dist/theme/domainTokens/domainTokenDefaults.d.ts.map +1 -0
  240. package/dist/theme/domainTokens/domainTokenDefaults.js +19 -0
  241. package/dist/theme/domainTokens/index.d.ts +5 -4
  242. package/dist/theme/domainTokens/index.d.ts.map +1 -1
  243. package/dist/theme/domainTokens/index.js +5 -9
  244. package/dist/theme/index.d.ts +1 -0
  245. package/dist/theme/index.d.ts.map +1 -1
  246. package/dist/theme/motionKnobs.d.ts +56 -0
  247. package/dist/theme/motionKnobs.d.ts.map +1 -0
  248. package/dist/theme/motionKnobs.js +48 -0
  249. package/dist/theme/resolveThemeValues.d.ts +11 -4
  250. package/dist/theme/resolveThemeValues.d.ts.map +1 -1
  251. package/dist/theme/resolveThemeValues.js +40 -4
  252. package/dist/theme/themeAdaptations.d.ts +7 -3
  253. package/dist/theme/themeAdaptations.d.ts.map +1 -1
  254. package/dist/theme/themeAdaptations.js +36 -7
  255. package/dist/theme/themeMotion.d.ts +96 -0
  256. package/dist/theme/themeMotion.d.ts.map +1 -0
  257. package/dist/theme/themeMotion.js +177 -0
  258. package/dist/theme/tokenReferences.d.ts +31 -0
  259. package/dist/theme/tokenReferences.d.ts.map +1 -0
  260. package/dist/theme/tokenReferences.js +132 -0
  261. package/dist/theme/tokens.d.ts +1 -0
  262. package/dist/theme/tokens.d.ts.map +1 -1
  263. package/dist/theme/tokens.js +9 -129
  264. package/dist/theme/tokens.stylex.d.ts +12 -0
  265. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  266. package/dist/theme/tokens.stylex.js +12 -0
  267. package/dist/utils/focusReturn.d.ts +17 -1
  268. package/dist/utils/focusReturn.d.ts.map +1 -1
  269. package/dist/utils/focusReturn.js +11 -4
  270. package/dist/utils/getNodeText.d.ts +8 -0
  271. package/dist/utils/getNodeText.d.ts.map +1 -0
  272. package/dist/utils/getNodeText.js +37 -0
  273. package/dist/utils/interactionModality.d.ts +11 -2
  274. package/dist/utils/interactionModality.d.ts.map +1 -1
  275. package/dist/utils/interactionModality.js +58 -5
  276. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  277. package/dist/utils/interactionOverlay.stylex.js +17 -8
  278. package/dist/utils/plainDate.d.ts +2 -0
  279. package/dist/utils/plainDate.d.ts.map +1 -1
  280. package/dist/utils/plainDate.js +46 -3
  281. package/dist/utils/presentation.d.ts +50 -0
  282. package/dist/utils/presentation.d.ts.map +1 -0
  283. package/dist/utils/presentation.js +125 -0
  284. package/dist/utils/pressFeedback.d.ts.map +1 -1
  285. package/dist/utils/pressFeedback.js +20 -1
  286. package/dist/utils/primaryClick.d.ts +11 -0
  287. package/dist/utils/primaryClick.d.ts.map +1 -0
  288. package/dist/utils/primaryClick.js +25 -0
  289. package/dist/utils/sharedMutationObserver.d.ts +19 -11
  290. package/dist/utils/sharedMutationObserver.d.ts.map +1 -1
  291. package/dist/utils/sharedMutationObserver.js +66 -26
  292. package/dist/utils/timeFormat.d.ts +47 -0
  293. package/dist/utils/timeFormat.d.ts.map +1 -0
  294. package/dist/utils/timeFormat.js +240 -0
  295. package/dist/utils/touchPresentation.d.ts.map +1 -1
  296. package/dist/utils/touchPresentation.js +5 -3
  297. package/dist/utils/touchPresentation.stylex.d.ts +15 -0
  298. package/dist/utils/touchPresentation.stylex.d.ts.map +1 -0
  299. package/dist/utils/touchPresentation.stylex.js +33 -0
  300. package/dist/utils/unicodeDigits.d.ts +20 -0
  301. package/dist/utils/unicodeDigits.d.ts.map +1 -0
  302. package/dist/utils/unicodeDigits.js +70 -0
  303. package/locales/en.json +27 -3
  304. package/locales/pseudo.json +18 -0
  305. package/package.json +5 -4
  306. package/src/AlertDialog/AlertDialog.tsx +6 -5
  307. package/src/AlertDialog/__tests__/AlertDialog.a11y.chromium.spec.ts +18 -6
  308. package/src/AppShell/AppShell.doc.mjs +1 -1
  309. package/src/AppShell/AppShell.spec.md +9 -12
  310. package/src/AppShell/AppShell.test.tsx +164 -2
  311. package/src/AppShell/AppShell.tsx +225 -5
  312. package/src/AppShell/__tests__/AppShellMainKeyboard.a11y.browser.spec.ts +534 -0
  313. package/src/AppShell/observeMainOverflow.test.ts +173 -0
  314. package/src/AppShell/observeMainOverflow.ts +143 -0
  315. package/src/Avatar/Avatar.spec.md +22 -22
  316. package/src/Avatar/Avatar.tsx +18 -1
  317. package/src/Avatar/__tests__/AvatarForcedColors.a11y.chromium.spec.ts +126 -0
  318. package/src/Badge/Badge.doc.mjs +1 -1
  319. package/src/Badge/Badge.tsx +19 -2
  320. package/src/Badge/__tests__/BadgeLargerText.a11y.chromium.spec.ts +260 -0
  321. package/src/BottomSheet/BottomSheet.test.tsx +46 -0
  322. package/src/BottomSheet/BottomSheet.tsx +1 -1
  323. package/src/BottomSheet/BottomSheetPanel.tsx +2 -1
  324. package/src/BottomSheet/useSheetGestures.test.ts +52 -0
  325. package/src/BottomSheet/useSheetGestures.ts +37 -6
  326. package/src/Breadcrumbs/__tests__/Breadcrumbs.a11y.chromium.spec.ts +18 -6
  327. package/src/Button/Button.tsx +4 -4
  328. package/src/Calendar/Calendar.spec.md +87 -48
  329. package/src/Calendar/__tests__/Calendar.a11y.chromium.spec.ts +21 -7
  330. package/src/Carousel/Carousel.doc.mjs +1 -1
  331. package/src/Carousel/Carousel.spec.md +9 -9
  332. package/src/Carousel/Carousel.test.tsx +124 -3
  333. package/src/Carousel/Carousel.tsx +10 -2
  334. package/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts +21 -7
  335. package/src/Carousel/__tests__/Carousel.a11y.known-failures.ts +1 -16
  336. package/src/Chat/ChatComposerTokenElement.spec.md +45 -33
  337. package/src/CheckboxInput/CheckboxInput.tsx +6 -5
  338. package/src/ClickableCard/ClickableCard.test.tsx +96 -0
  339. package/src/ClickableCard/ClickableCard.tsx +48 -4
  340. package/src/Collapsible/Collapsible.spec.md +2 -0
  341. package/src/Collapsible/CollapsibleGroup.spec.md +57 -29
  342. package/src/Collapsible/__tests__/Collapsible.a11y.chromium.spec.ts +18 -7
  343. package/src/Collapsible/__tests__/CollapsibleGroupAccordion.a11y.chromium.spec.ts +18 -6
  344. package/src/ComplexSelector/ComplexSelector.test.tsx +37 -0
  345. package/src/ContextMenu/ContextMenu.test.tsx +35 -0
  346. package/src/DateInput/DateInput.test.tsx +115 -3
  347. package/src/DateInput/DateInput.tsx +29 -14
  348. package/src/DateInput/DateInputTouch.test.tsx +76 -0
  349. package/src/DateInput/MonthScroller.tsx +106 -20
  350. package/src/DateInput/MonthScrollerSettle.test.tsx +165 -0
  351. package/src/DateInput/TouchDateField.tsx +14 -3
  352. package/src/DateInput/__tests__/MonthScrollerSteps.interaction.browser.spec.ts +312 -0
  353. package/src/DateRangeInput/DateRangeInput.test.tsx +46 -3
  354. package/src/DateRangeInput/DateRangeInput.tsx +16 -12
  355. package/src/DateTimeInput/DateTimeInput.doc.mjs +10 -8
  356. package/src/DateTimeInput/DateTimeInput.spec.md +75 -51
  357. package/src/DateTimeInput/DateTimeInput.test.tsx +81 -0
  358. package/src/DateTimeInput/DateTimeInput.tsx +46 -24
  359. package/src/DateTimeInput/NativeTimeSegment.tsx +4 -6
  360. package/src/DateTimeInput/TouchDateTimeField.tsx +3 -7
  361. package/src/Dialog/Dialog.test.tsx +216 -2
  362. package/src/Dialog/Dialog.tsx +42 -9
  363. package/src/Dialog/DialogHeader.tsx +5 -4
  364. package/src/Dialog/__tests__/DialogSecondDocument.a11y.browser.spec.ts +202 -0
  365. package/src/Divider/Divider.tsx +2 -1
  366. package/src/Drawer/Drawer.spec.md +111 -80
  367. package/src/Drawer/Drawer.test.tsx +35 -0
  368. package/src/Drawer/Drawer.tsx +13 -2
  369. package/src/Drawer/DrawerHeader.spec.md +181 -0
  370. package/src/Drawer/__tests__/DrawerNonModalPageScroll.a11y.chromium.spec.ts +267 -0
  371. package/src/DropdownMenu/DropdownMenu.test.tsx +41 -0
  372. package/src/DropdownMenu/DropdownMenu.tsx +10 -3
  373. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +34 -0
  374. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +6 -5
  375. package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +19 -7
  376. package/src/DropdownMenu/__tests__/MenuButton.a11y.chromium.spec.ts +21 -9
  377. package/src/DropdownMenu/lazyMenuBottomSheet.test.ts +60 -0
  378. package/src/DropdownMenu/lazyMenuBottomSheet.ts +36 -7
  379. package/src/DropdownMenu/renderDropdownItems.tsx +24 -19
  380. package/src/EmptyState/EmptyState.tsx +2 -1
  381. package/src/Field/FieldLabel.tsx +5 -4
  382. package/src/Field/InputClearButton.tsx +3 -3
  383. package/src/Field/PanelSearchInput.tsx +2 -2
  384. package/src/FieldStatus/FieldStatus.tsx +2 -2
  385. package/src/FileInput/FileInput.test.tsx +21 -0
  386. package/src/FileInput/FileInput.tsx +4 -1
  387. package/src/FormLayout/FormLayout.spec.md +7 -8
  388. package/src/Heading/Heading.tsx +1 -1
  389. package/src/Icon/Icon.tsx +1 -1
  390. package/src/Indicator/CheckIndicator.spec.md +47 -42
  391. package/src/Indicator/CheckIndicator.tsx +2 -1
  392. package/src/Indicator/CheckboxIndicator.spec.md +40 -37
  393. package/src/Indicator/CheckboxIndicator.tsx +3 -1
  394. package/src/Indicator/Indicator.spec.md +188 -0
  395. package/src/Indicator/RadioIndicator.tsx +3 -1
  396. package/src/Item/Item.tsx +2 -1
  397. package/src/Item/ItemSwipeLayer.tsx +1 -1
  398. package/src/Kbd/Kbd.spec.md +81 -38
  399. package/src/Layer/__tests__/LayerBrowserHide.a11y.chromium.spec.ts +126 -0
  400. package/src/Layer/popoverOperations.test.ts +205 -0
  401. package/src/Layer/popoverOperations.ts +172 -0
  402. package/src/Layer/useLayer.test.tsx +348 -1
  403. package/src/Layer/useLayer.tsx +118 -71
  404. package/src/Layout/Layout.spec.md +127 -131
  405. package/src/Layout/Layout.tsx +1 -1
  406. package/src/Layout/LayoutContent.spec.md +205 -0
  407. package/src/Layout/LayoutContent.tsx +1 -1
  408. package/src/Layout/LayoutFooter.spec.md +190 -0
  409. package/src/Layout/LayoutFooter.tsx +1 -1
  410. package/src/Layout/LayoutHeader.spec.md +193 -0
  411. package/src/Layout/LayoutHeader.tsx +1 -1
  412. package/src/Layout/LayoutPanel.spec.md +194 -0
  413. package/src/Layout/__tests__/Layout.a11y.chromium.spec.ts +18 -6
  414. package/src/Lightbox/Lightbox.test.tsx +18 -0
  415. package/src/Lightbox/Lightbox.tsx +5 -0
  416. package/src/Lightbox/LightboxViewport.a11y.browser.spec.ts +8 -4
  417. package/src/Link/__tests__/Link.a11y.chromium.spec.ts +18 -8
  418. package/src/List/List.tsx +1 -1
  419. package/src/List/ListItem.spec.md +205 -0
  420. package/src/List/ListItem.tsx +3 -3
  421. package/src/MetadataList/MetadataList.spec.md +187 -0
  422. package/src/MetadataList/MetadataListItem.spec.md +165 -0
  423. package/src/MobileNav/MobileNav.test.tsx +15 -0
  424. package/src/MobileNav/MobileNav.tsx +9 -1
  425. package/src/MobileNav/__tests__/MobileNavHeader.a11y.chromium.spec.ts +89 -0
  426. package/src/MoreMenu/MoreMenu.test.tsx +11 -0
  427. package/src/MultiSelector/CreateRow.a11y.chromium.spec.ts +7 -8
  428. package/src/MultiSelector/MultiSelector.spec.md +7 -4
  429. package/src/MultiSelector/MultiSelector.test.tsx +270 -1
  430. package/src/MultiSelector/MultiSelector.tsx +25 -10
  431. package/src/MultiSelector/__tests__/MultiSelector.listbox.a11y.test.tsx +8 -1
  432. package/src/NavMenu/NavHeadingMenu.spec.md +1 -5
  433. package/src/NumberInput/NumberInput.doc.mjs +13 -7
  434. package/src/NumberInput/NumberInput.spec.md +59 -37
  435. package/src/NumberInput/NumberInput.tsx +13 -3
  436. package/src/NumberInput/__tests__/NumberInput.coarse-pointer.a11y.chromium.spec.ts +102 -0
  437. package/src/NumberInput/numberParser.ts +7 -45
  438. package/src/ProgressBar/ProgressBar.test.tsx +108 -32
  439. package/src/ProgressBar/ProgressBar.tsx +58 -10
  440. package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +21 -9
  441. package/src/ProgressBar/__tests__/ProgressBarMotion.a11y.chromium.spec.ts +831 -0
  442. package/src/Resizable/__tests__/ResizeHandle.a11y.chromium.spec.ts +21 -14
  443. package/src/Resizable/useResizable.test.ts +43 -0
  444. package/src/Resizable/useResizable.ts +32 -9
  445. package/src/SegmentedControl/SegmentedControl.test.tsx +84 -0
  446. package/src/SegmentedControl/SegmentedControlItem.tsx +11 -5
  447. package/src/Selector/Selector.test.tsx +90 -1
  448. package/src/Selector/__tests__/Selector.combobox.a11y.test.tsx +8 -1
  449. package/src/Selector/__tests__/Selector.listbox.a11y.test.tsx +8 -1
  450. package/src/Selector/useSelectorPresentation.ts +31 -3
  451. package/src/SideNav/SideNav.test.tsx +27 -0
  452. package/src/SideNav/__tests__/SideNavRailMotion.interaction.browser.spec.ts +176 -51
  453. package/src/Skeleton/Skeleton.tsx +3 -1
  454. package/src/Slider/Slider.test.tsx +31 -0
  455. package/src/Slider/Slider.tsx +8 -3
  456. package/src/Slider/__tests__/Slider.a11y.chromium.spec.ts +19 -7
  457. package/src/Spinner/Spinner.tsx +5 -3
  458. package/src/Stack/Stack.tsx +1 -1
  459. package/src/Stack/StackItem.tsx +1 -1
  460. package/src/TabList/TabList.test.tsx +58 -4
  461. package/src/TabList/TabList.tsx +16 -1
  462. package/src/TabList/__tests__/TabPressPointer.interaction.browser.spec.ts +163 -0
  463. package/src/Table/__tests__/Table.a11y.chromium.spec.ts +18 -6
  464. package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +22 -0
  465. package/src/Table/plugins/columnResize/useTableColumnResize.tsx +8 -4
  466. package/src/Text/Text.tsx +1 -1
  467. package/src/Text/useTruncation.test.tsx +128 -1
  468. package/src/Text/useTruncation.ts +23 -5
  469. package/src/TextArea/TextArea.spec.md +10 -13
  470. package/src/TextArea/TextArea.test.tsx +60 -0
  471. package/src/TextArea/TextArea.tsx +15 -1
  472. package/src/TextInput/TextInput.test.tsx +21 -0
  473. package/src/TextInput/TextInput.tsx +3 -1
  474. package/src/TimeInput/NativeTimeInput.test.tsx +17 -0
  475. package/src/TimeInput/Presentation.test.tsx +21 -0
  476. package/src/TimeInput/TimeInput.doc.mjs +8 -8
  477. package/src/TimeInput/TimeInput.spec.md +45 -48
  478. package/src/TimeInput/TimeInput.test.tsx +76 -0
  479. package/src/TimeInput/TimeInput.tsx +36 -18
  480. package/src/TimeInput/TouchTimeField.tsx +4 -6
  481. package/src/Timestamp/Timestamp.spec.md +6 -12
  482. package/src/Timestamp/Timestamp.test.tsx +30 -5
  483. package/src/Timestamp/Timestamp.tsx +6 -2
  484. package/src/Timestamp/TimestampHoverCard.tsx +4 -4
  485. package/src/Toast/Toast.spec.md +14 -20
  486. package/src/Toast/Toast.tsx +104 -36
  487. package/src/Toast/ToastViewport.test.tsx +719 -1
  488. package/src/Toast/ToastViewport.tsx +100 -50
  489. package/src/Tokenizer/Tokenizer.spec.md +5 -3
  490. package/src/Tokenizer/Tokenizer.test.tsx +68 -0
  491. package/src/Tokenizer/Tokenizer.tsx +14 -2
  492. package/src/Tokenizer/TokenizerBottomSheet.test.tsx +15 -4
  493. package/src/Tokenizer/TokenizerBottomSheet.tsx +3 -3
  494. package/src/Toolbar/Toolbar.spec.md +5 -6
  495. package/src/Toolbar/Toolbar.test.tsx +30 -0
  496. package/src/Toolbar/Toolbar.tsx +2 -1
  497. package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +18 -6
  498. package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +18 -7
  499. package/src/TopNav/TopNav.test.tsx +27 -0
  500. package/src/TopNav/TopNavMegaMenu.test.tsx +21 -0
  501. package/src/TopNav/TopNavMegaMenu.tsx +4 -1
  502. package/src/TopNav/TopNavMenu.test.tsx +17 -0
  503. package/src/TopNav/TopNavMenu.tsx +1 -0
  504. package/src/Typeahead/Typeahead.test.tsx +40 -0
  505. package/src/Typeahead/Typeahead.tsx +12 -1
  506. package/src/__tests__/pressState.ts +8 -4
  507. package/src/gestures/GestureArena.ts +35 -0
  508. package/src/gestures/GestureHost.ts +2065 -0
  509. package/src/gestures/GestureRecognizer.ts +18 -0
  510. package/src/gestures/LongPressGestureRecognizer.ts +4 -0
  511. package/src/gestures/PanGestureRecognizer.ts +4 -0
  512. package/src/gestures/PinchGestureRecognizer.ts +148 -0
  513. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +256 -0
  514. package/src/gestures/SwipeGestureRecognizer.ts +197 -0
  515. package/src/gestures/TapGestureRecognizer.ts +4 -0
  516. package/src/gestures/VelocityTracker.ts +10 -3
  517. package/src/gestures/__tests__/GestureHost.test.ts +2087 -0
  518. package/src/gestures/__tests__/PanGestureRecognizer.test.ts +4 -4
  519. package/src/gestures/__tests__/PinchGestureRecognizer.test.ts +74 -0
  520. package/src/gestures/__tests__/ScreenEdgePanGestureRecognizer.test.ts +498 -0
  521. package/src/gestures/__tests__/SwipeGestureRecognizer.test.ts +108 -0
  522. package/src/gestures/__tests__/arena.test.ts +5 -0
  523. package/src/gestures/__tests__/arenaPinchSwipeEdge.test.ts +193 -0
  524. package/src/gestures/__tests__/browser/build-fixture.mjs +46 -0
  525. package/src/gestures/__tests__/browser/fixture.ts +307 -0
  526. package/src/gestures/__tests__/browser/gestures.interaction.browser.spec.ts +1137 -0
  527. package/src/gestures/__tests__/browserNegotiation.test.ts +293 -0
  528. package/src/gestures/__tests__/haptics.test.ts +187 -0
  529. package/src/gestures/__tests__/harness.ts +21 -2
  530. package/src/gestures/__tests__/recognizerKind.test.ts +78 -0
  531. package/src/gestures/__tests__/touchActionRule.test.ts +54 -0
  532. package/src/gestures/haptics.ts +203 -0
  533. package/src/gestures/nativeScroll.ts +277 -0
  534. package/src/gestures/touchAction.ts +191 -0
  535. package/src/gestures/touchActionRule.ts +157 -0
  536. package/src/gestures/types.ts +30 -9
  537. package/src/hooks/index.ts +5 -1
  538. package/src/hooks/modalAnnounce.ts +129 -72
  539. package/src/hooks/scrollKeyboardDelegation.test.ts +33 -0
  540. package/src/hooks/scrollKeyboardDelegation.ts +9 -3
  541. package/src/hooks/useAdaptivePresentation.ts +4 -1
  542. package/src/hooks/useAnnounce.doc.mjs +35 -3
  543. package/src/hooks/useAnnounce.replace.test.tsx +516 -0
  544. package/src/hooks/useAnnounce.ts +209 -102
  545. package/src/hooks/useAnnounceRenderedText.adopters.test.tsx +10 -1
  546. package/src/hooks/useAnnounceRenderedText.ts +17 -3
  547. package/src/hooks/useBusyAnnouncement.ts +7 -10
  548. package/src/hooks/useMenuHover.ts +15 -1
  549. package/src/hooks/useScrollOverflow.doc.mjs +1 -1
  550. package/src/hooks/useScrollOverflow.test.tsx +102 -1
  551. package/src/hooks/useScrollOverflow.ts +59 -10
  552. package/src/i18n/generated-locales/en.generated.ts +7 -0
  553. package/src/i18n/generated-locales/pseudo.generated.ts +7 -0
  554. package/src/motion/README.md +11 -1
  555. package/src/motion/animator.test.ts +151 -3
  556. package/src/motion/animator.ts +149 -33
  557. package/src/theme/Theme.doc.mjs +1 -1
  558. package/src/theme/defineTheme.ts +117 -8
  559. package/src/theme/derivedVarRegistry.test.ts +4 -0
  560. package/src/theme/domainTokens/domainTokenDefaults.ts +19 -0
  561. package/src/theme/domainTokens/index.ts +6 -10
  562. package/src/theme/expandMotionScale.test.ts +1 -1
  563. package/src/theme/index.ts +5 -0
  564. package/src/theme/motionKnobs.ts +82 -0
  565. package/src/theme/resolveThemeValues.ts +68 -6
  566. package/src/theme/themeAdaptations.ts +52 -12
  567. package/src/theme/themeMotion.test.ts +620 -0
  568. package/src/theme/themeMotion.ts +221 -0
  569. package/src/theme/tokenReferences.ts +149 -0
  570. package/src/theme/tokens.stylex.ts +10 -0
  571. package/src/theme/tokens.ts +15 -158
  572. package/src/utils/focusReturn.ts +17 -6
  573. package/src/utils/getNodeText.test.ts +24 -0
  574. package/src/utils/getNodeText.ts +37 -0
  575. package/src/utils/interactionModality.test.ts +45 -1
  576. package/src/utils/interactionModality.ts +64 -5
  577. package/src/utils/interactionOverlay.stylex.ts +12 -3
  578. package/src/utils/plainDate.test.ts +90 -0
  579. package/src/utils/plainDate.ts +58 -3
  580. package/src/utils/presentation.test.ts +380 -0
  581. package/src/utils/presentation.ts +180 -0
  582. package/src/utils/pressFeedback.test.ts +138 -0
  583. package/src/utils/pressFeedback.ts +34 -11
  584. package/src/utils/pressPaintPriority.test.ts +80 -0
  585. package/src/utils/primaryClick.test.ts +38 -0
  586. package/src/utils/primaryClick.ts +28 -0
  587. package/src/utils/sharedMutationObserver.test.ts +54 -1
  588. package/src/utils/sharedMutationObserver.ts +87 -29
  589. package/src/utils/timeFormat.test.ts +204 -0
  590. package/src/utils/timeFormat.ts +314 -0
  591. package/src/utils/touchPresentation.stylex.ts +43 -0
  592. package/src/utils/touchPresentation.test.ts +140 -1
  593. package/src/utils/touchPresentation.ts +5 -3
  594. package/src/utils/unicodeDigits.test.ts +44 -0
  595. package/src/utils/unicodeDigits.ts +71 -0
@@ -0,0 +1,1137 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file gestures.interaction.browser.spec.ts
5
+ * @input The gesture fixture bundled with esbuild, a phone-sized touch
6
+ * screen, and real engine touch input: Chromium through CDP with explicit
7
+ * timestamps, WebKit through its own input command (real-time pacing)
8
+ * @output Browser proof of the negotiation the unit tests cannot see:
9
+ * touch-action per element, touchmove cancelled exactly while a recognizer
10
+ * owns the finger, native scrolling untouched otherwise, click suppression
11
+ * with no ghost click, a drag that began as a scroll staying the browser's, pinch-zoom
12
+ * left to the browser, a second finger that never breaks the first, and
13
+ * timing taken from event timestamps under a blocked main thread
14
+ * @position Browser tests of packages/core/src/gestures, run by
15
+ * `pnpm test:interaction` (playwright.interaction.config.ts, a phone in
16
+ * Chromium and WebKit) and in CI's interaction proofs. WebKit's Linux build
17
+ * cannot scroll, fling or pinch-zoom from injected touches, and synthesises
18
+ * a click only from its tap gesture; the checks that need those run in
19
+ * Chromium here and in Mobile Safari on the iOS simulator.
20
+ */
21
+
22
+ import path from 'node:path';
23
+ import {fileURLToPath} from 'node:url';
24
+ import {build} from 'esbuild';
25
+ import {expect, test, type Page} from '@playwright/test';
26
+
27
+ interface Entry {
28
+ name: string;
29
+ state: string;
30
+ time: number;
31
+ x?: number;
32
+ y?: number;
33
+ scale?: number;
34
+ beganAt?: number | null;
35
+ startedAt?: number;
36
+ }
37
+
38
+ let bundle = '';
39
+
40
+ test.beforeAll(async () => {
41
+ const result = await build({
42
+ entryPoints: [
43
+ path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixture.ts'),
44
+ ],
45
+ bundle: true,
46
+ write: false,
47
+ format: 'iife',
48
+ platform: 'browser',
49
+ target: 'es2022',
50
+ });
51
+ bundle = result.outputFiles[0].text;
52
+ });
53
+
54
+ type Point = {x: number; y: number};
55
+ type Finger = Point & {id: number};
56
+
57
+ /** The fingers down after a step, and when (ms after the gesture's start). */
58
+ interface Step {
59
+ t: number;
60
+ down: Finger[];
61
+ }
62
+
63
+ interface Driver {
64
+ readonly engine: 'chromium' | 'webkit';
65
+ /** Plays the steps as real touch input; returns the page time of t = 0. */
66
+ play(steps: Step[]): Promise<number>;
67
+ /** A tap through the engine's tap path (WebKit synthesises click only from it). */
68
+ tap(at: Point): Promise<void>;
69
+ }
70
+
71
+ const sleep = async (ms: number) =>
72
+ new Promise(resolve => setTimeout(resolve, ms));
73
+
74
+ /** Each step's change against the previous one: who landed, lifted, moved. */
75
+ function diff(previous: Finger[], next: Finger[]) {
76
+ const before = new Set(previous.map(finger => finger.id));
77
+ const after = new Set(next.map(finger => finger.id));
78
+ return {
79
+ landed: next.filter(finger => !before.has(finger.id)),
80
+ lifted: previous.filter(finger => !after.has(finger.id)),
81
+ };
82
+ }
83
+
84
+ async function chromiumDriver(page: Page): Promise<Driver> {
85
+ const cdp = await page.context().newCDPSession(page);
86
+ const origin = await page.evaluate(() => performance.timeOrigin);
87
+ // The fingers down, across calls: a gesture may span several.
88
+ let down: Finger[] = [];
89
+ const send = async (type: string, points: Finger[], at: number) =>
90
+ cdp.send('Input.dispatchTouchEvent', {
91
+ type: type as 'touchStart',
92
+ touchPoints: points.map(point => ({
93
+ x: point.x,
94
+ y: point.y,
95
+ id: point.id,
96
+ })),
97
+ timestamp: (origin + at) / 1000,
98
+ });
99
+ return {
100
+ engine: 'chromium',
101
+ async play(steps) {
102
+ // CDP stamps every event with the scripted time, so the page sees the
103
+ // script's timing exactly, whatever the dispatch latency.
104
+ const base = Math.ceil(await page.evaluate(() => performance.now())) + 50;
105
+ for (const step of steps) {
106
+ const {landed} = diff(down, step.down);
107
+ // CDP diffs active points against the previous event: a new point
108
+ // is pressed, a missing one released; touchEnd carries none.
109
+ const type =
110
+ landed.length > 0
111
+ ? 'touchStart'
112
+ : step.down.length === 0
113
+ ? 'touchEnd'
114
+ : 'touchMove';
115
+ await send(type, step.down, base + step.t);
116
+ down = step.down;
117
+ }
118
+ return base;
119
+ },
120
+ async tap(at) {
121
+ const base = Math.ceil(await page.evaluate(() => performance.now())) + 50;
122
+ await send('touchStart', [{...at, id: 1}], base);
123
+ await send('touchEnd', [], base + 60);
124
+ },
125
+ };
126
+ }
127
+
128
+ interface WebKitSession {
129
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>;
130
+ }
131
+
132
+ /** Playwright's server-side WebKit page session (a locally launched browser). */
133
+ function webkitSession(page: Page): WebKitSession {
134
+ const connection = (
135
+ page as unknown as {_connection?: {toImpl?: (o: unknown) => unknown}}
136
+ )._connection;
137
+ const impl = connection?.toImpl?.(page) as
138
+ {delegate?: {_pageProxySession?: WebKitSession}} | undefined;
139
+ const session = impl?.delegate?._pageProxySession;
140
+ if (session == null) {
141
+ throw new Error('WebKit touch input needs a locally launched browser');
142
+ }
143
+ return session;
144
+ }
145
+
146
+ function webkitDriver(page: Page): Driver {
147
+ const session = webkitSession(page);
148
+ // WebKit's command applies the event's phase to every point it lists: a
149
+ // start lists only the fingers that land, an end only those that lift, a
150
+ // move every finger down. Integer CSS px; the page sees real time.
151
+ const points = (fingers: Finger[]) =>
152
+ fingers.map(f => ({x: Math.round(f.x), y: Math.round(f.y), id: f.id}));
153
+ // The fingers down, across calls: a gesture may span several.
154
+ let down: Finger[] = [];
155
+ return {
156
+ engine: 'webkit',
157
+ async play(steps) {
158
+ const start = Date.now();
159
+ const base = await page.evaluate(() => performance.now());
160
+ for (const step of steps) {
161
+ const wait = start + step.t - Date.now();
162
+ if (wait > 0) {
163
+ await sleep(wait);
164
+ }
165
+ const {landed, lifted} = diff(down, step.down);
166
+ if (landed.length > 0) {
167
+ await session.send('Input.dispatchTouchEvent', {
168
+ type: 'touchStart',
169
+ touchPoints: points(landed),
170
+ });
171
+ } else if (lifted.length > 0) {
172
+ await session.send('Input.dispatchTouchEvent', {
173
+ type: 'touchEnd',
174
+ touchPoints: points(lifted),
175
+ });
176
+ } else {
177
+ await session.send('Input.dispatchTouchEvent', {
178
+ type: 'touchMove',
179
+ touchPoints: points(step.down),
180
+ });
181
+ }
182
+ down = step.down;
183
+ }
184
+ return base;
185
+ },
186
+ async tap(at) {
187
+ await page.touchscreen.tap(Math.round(at.x), Math.round(at.y));
188
+ },
189
+ };
190
+ }
191
+
192
+ /** One finger from `from` to `to` in `count` frames of 16 ms, then a lift. */
193
+ function drag(from: Point, to: Point, count = 12, lift = true): Step[] {
194
+ const steps: Step[] = [{t: 0, down: [{...from, id: 1}]}];
195
+ for (let i = 1; i <= count; i++) {
196
+ const k = i / count;
197
+ steps.push({
198
+ t: i * 16,
199
+ down: [
200
+ {
201
+ x: from.x + (to.x - from.x) * k,
202
+ y: from.y + (to.y - from.y) * k,
203
+ id: 1,
204
+ },
205
+ ],
206
+ });
207
+ }
208
+ if (lift) {
209
+ steps.push({t: (count + 1) * 16, down: []});
210
+ }
211
+ return steps;
212
+ }
213
+
214
+ /** Down at `from`, through each offset in turn 16 ms apart, then up. */
215
+ function travel(from: Point, offsets: ReadonlyArray<Point>): Step[] {
216
+ const steps: Step[] = [{t: 0, down: [{...from, id: 1}]}];
217
+ offsets.forEach((offset, index) => {
218
+ steps.push({
219
+ t: (index + 1) * 16,
220
+ down: [{x: from.x + offset.x, y: from.y + offset.y, id: 1}],
221
+ });
222
+ });
223
+ steps.push({t: (offsets.length + 2) * 16, down: []});
224
+ return steps;
225
+ }
226
+
227
+ /** Offsets along x, one per step, ending `distance` px from touch-down. */
228
+ const alongX = (...xs: number[]): Point[] => xs.map(x => ({x, y: 0}));
229
+
230
+ async function open(page: Page, engine: string): Promise<Driver> {
231
+ // A real navigation first: touch emulation exposes the Touch Events API
232
+ // to documents it creates.
233
+ await page.goto('about:blank');
234
+ await page.setContent(
235
+ '<!doctype html><meta name="viewport" content="width=device-width, initial-scale=1"><body></body>',
236
+ );
237
+ await page.addScriptTag({content: bundle});
238
+ return engine === 'webkit' ? webkitDriver(page) : chromiumDriver(page);
239
+ }
240
+
241
+ async function center(page: Page, id: string): Promise<Point> {
242
+ const box = await page.locator(`#${id}`).boundingBox();
243
+ if (box == null) {
244
+ throw new Error(`#${id} has no box`);
245
+ }
246
+ return {
247
+ x: Math.round(box.x + box.width / 2),
248
+ y: Math.round(box.y + box.height / 2),
249
+ };
250
+ }
251
+
252
+ async function log(page: Page): Promise<Entry[]> {
253
+ await page.waitForTimeout(50);
254
+ return page.evaluate(() => window.__gestures);
255
+ }
256
+
257
+ async function clearLog(page: Page): Promise<void> {
258
+ await page.evaluate(() => {
259
+ window.__gestures.length = 0;
260
+ });
261
+ }
262
+
263
+ async function scrollTop(page: Page, id: string): Promise<number> {
264
+ return page.evaluate(
265
+ target => document.getElementById(target)?.scrollTop ?? -1,
266
+ id,
267
+ );
268
+ }
269
+
270
+ /** Record whether each touchmove reached the page cancelled. */
271
+ async function logTouchMoves(page: Page): Promise<void> {
272
+ await page.evaluate(() => {
273
+ const moves: boolean[] = [];
274
+ (window as unknown as {__moves: boolean[]}).__moves = moves;
275
+ window.addEventListener(
276
+ 'touchmove',
277
+ event => moves.push(event.defaultPrevented),
278
+ {passive: true},
279
+ );
280
+ });
281
+ }
282
+
283
+ async function readTouchMoves(page: Page): Promise<boolean[]> {
284
+ return page.evaluate(
285
+ () => (window as unknown as {__moves: boolean[]}).__moves,
286
+ );
287
+ }
288
+
289
+ const states = (entries: Entry[], name: string) =>
290
+ entries.filter(entry => entry.name === name).map(entry => entry.state);
291
+
292
+ test('derives touch-action per element from its recognizers', async ({
293
+ page,
294
+ browserName,
295
+ }) => {
296
+ await open(page, browserName);
297
+ const actions = await page.evaluate(() =>
298
+ Object.fromEntries(
299
+ ['hpan', 'tapper', 'presser', 'pinchable', 'sheetBody'].map(id => [
300
+ id,
301
+ getComputedStyle(document.getElementById(id) as HTMLElement)
302
+ .touchAction,
303
+ ]),
304
+ ),
305
+ );
306
+ expect(actions).toEqual({
307
+ hpan: 'pan-y pinch-zoom',
308
+ tapper: 'manipulation',
309
+ presser: 'manipulation',
310
+ pinchable: 'none',
311
+ sheetBody: 'auto',
312
+ });
313
+ });
314
+
315
+ test('only elements whose recognizers claim a moving touch carry a blocking touchmove listener', async ({
316
+ page,
317
+ browserName,
318
+ }) => {
319
+ await open(page, browserName);
320
+ const blocking = await page.evaluate(() =>
321
+ window.__listeners
322
+ .filter(entry => entry.type === 'touchmove' && entry.passive === false)
323
+ .map(entry => entry.id),
324
+ );
325
+ // Pans (the cards' and the screen-edge pans' too), long presses, a pinch
326
+ // and the sheet's pan can take a moving touch; a tap-only element and a
327
+ // press consumer never wait.
328
+ expect(new Set(blocking)).toEqual(
329
+ new Set([
330
+ 'hpan',
331
+ 'presser',
332
+ 'pinchable',
333
+ 'sheetBody',
334
+ 'card',
335
+ 'authorNone',
336
+ 'authorPanY',
337
+ 'authorPinchZoom',
338
+ 'authorPanXY',
339
+ 'inlinePanXY',
340
+ 'edgeZone',
341
+ 'edgeWide',
342
+ 'edgeZoneRtl',
343
+ 'edgeWideRtl',
344
+ ]),
345
+ );
346
+ });
347
+
348
+ test('a horizontal drag on a pan-y element is the recognizer’s: cancelled moves, no scroll, no click', async ({
349
+ page,
350
+ browserName,
351
+ }) => {
352
+ const driver = await open(page, browserName);
353
+ await logTouchMoves(page);
354
+ const start = await center(page, 'hbutton');
355
+ // A 4:1 drag, the shape WebKit would otherwise pan natively.
356
+ await driver.play(drag(start, {x: start.x + 120, y: start.y + 30}));
357
+ const entries = await log(page);
358
+ const hpan = states(entries, 'hpan');
359
+ expect(hpan[0]).toBe('began');
360
+ expect(hpan.slice(-2)).toEqual(['ended', 'possible']);
361
+ const last = entries
362
+ .filter(entry => entry.name === 'hpan' && entry.state === 'changed')
363
+ .at(-1);
364
+ // As in UIKit, the 10 px hysteresis is not part of the translation.
365
+ expect(last?.x).toBeCloseTo(120 - 10, 0);
366
+ // Every touchmove after the claim was cancelled.
367
+ const moves = await readTouchMoves(page);
368
+ expect(moves.length).toBeGreaterThan(0);
369
+ expect(moves.slice(-3).every(Boolean)).toBe(true);
370
+ expect(await scrollTop(page, 'scroller')).toBe(0);
371
+ expect(entries.some(entry => entry.name.startsWith('click:'))).toBe(false);
372
+ expect(entries.some(entry => entry.name.startsWith('mousedown:'))).toBe(
373
+ false,
374
+ );
375
+ });
376
+
377
+ test('a vertical drag on the same element fails the pan and is never cancelled', async ({
378
+ page,
379
+ browserName,
380
+ }) => {
381
+ const driver = await open(page, browserName);
382
+ await logTouchMoves(page);
383
+ const start = await center(page, 'hpan');
384
+ await driver.play(drag(start, {x: start.x + 4, y: start.y - 120}));
385
+ const entries = await log(page);
386
+ expect(states(entries, 'hpan')).toEqual(['failed', 'possible']);
387
+ expect((await readTouchMoves(page)).some(Boolean)).toBe(false);
388
+ if (driver.engine === 'chromium') {
389
+ // The browser scrolled it natively.
390
+ expect(await scrollTop(page, 'scroller')).toBeGreaterThan(60);
391
+ }
392
+ expect(entries.some(entry => entry.name.startsWith('click:'))).toBe(false);
393
+ });
394
+
395
+ test('a horizontal drag on a passive pan-y control keeps its press: uncancelable moves the browser cannot pan are not a claim', async ({
396
+ page,
397
+ browserName,
398
+ }) => {
399
+ const driver = await open(page, browserName);
400
+ await page.evaluate(() => {
401
+ document.getElementById('passive')?.scrollIntoView({block: 'center'});
402
+ });
403
+ const start = await center(page, 'passive');
404
+ await driver.play(
405
+ drag({x: start.x - 120, y: start.y}, {x: start.x + 120, y: start.y + 6}),
406
+ );
407
+ const consumed = (await log(page))
408
+ .filter(entry => entry.name === 'passive')
409
+ .map(entry => entry.state);
410
+ expect(consumed[0]).toBe('began');
411
+ expect(consumed).not.toContain('cancelled');
412
+ expect(consumed.at(-1)).toBe('ended');
413
+ });
414
+
415
+ test('a vertical drag on a passive pan-y control, over a page with room to scroll, hands the press to the browser (FR20)', async ({
416
+ page,
417
+ browserName,
418
+ }) => {
419
+ test.skip(
420
+ browserName === 'webkit',
421
+ 'needs native scrolling from injected touches (Mobile Safari on the simulator)',
422
+ );
423
+ const driver = await open(page, browserName);
424
+ await page.evaluate(() => {
425
+ document.getElementById('passive')?.scrollIntoView({block: 'center'});
426
+ });
427
+ const before = await page.evaluate(() => window.scrollY);
428
+ const start = await center(page, 'passive');
429
+ // Up, along the axis pan-y leaves the browser, which has room that way.
430
+ await driver.play(drag(start, {x: start.x + 4, y: start.y - 120}));
431
+ const consumed = states(await log(page), 'passive');
432
+ expect(consumed[0]).toBe('began');
433
+ expect(consumed.at(-1)).toBe('cancelled');
434
+ expect(consumed).not.toContain('ended');
435
+ expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(
436
+ before + 20,
437
+ );
438
+ });
439
+
440
+ test('a vertical drag on a passive pan-y control where no scroller has room is not handed over by its moves: the browser\u2019s pointercancel ends the press (FR20)', async ({
441
+ page,
442
+ browserName,
443
+ }) => {
444
+ test.skip(
445
+ browserName === 'webkit',
446
+ 'needs native scrolling from injected touches (Mobile Safari on the simulator)',
447
+ );
448
+ const driver = await open(page, browserName);
449
+ // The page at its end: no room to scroll on.
450
+ await page.evaluate(() => {
451
+ window.scrollTo(0, document.documentElement.scrollHeight);
452
+ });
453
+ const atEnd = await page.evaluate(() => window.scrollY);
454
+ const start = await center(page, 'passiveEnd');
455
+ await driver.play(drag(start, {x: start.x + 4, y: start.y - 120}));
456
+ const entries = (await log(page)).filter(
457
+ entry => entry.name === 'passiveEnd' || entry.name === 'dom:pointercancel',
458
+ );
459
+ const order = entries.map(entry =>
460
+ entry.name === 'passiveEnd' ? entry.state : 'pointercancel',
461
+ );
462
+ expect(order[0]).toBe('began');
463
+ // The press ends cancelled, at the browser's pointercancel and not before:
464
+ // an uncancelable move along an axis no scroller can scroll is no claim.
465
+ const cancelledAt = order.indexOf('cancelled');
466
+ expect(cancelledAt).toBeGreaterThan(0);
467
+ expect(order.slice(0, cancelledAt)).toContain('pointercancel');
468
+ expect(order).not.toContain('ended');
469
+ expect(await page.evaluate(() => window.scrollY)).toBe(atEnd);
470
+ });
471
+
472
+ test('a one-way overlay pan keeps its direction from the page, and leaves the other way to it', async ({
473
+ page,
474
+ browserName,
475
+ }) => {
476
+ test.skip(
477
+ browserName === 'webkit',
478
+ 'needs native scrolling from injected touches (Mobile Safari on the simulator)',
479
+ );
480
+ const driver = await open(page, browserName);
481
+ await page.evaluate(() => {
482
+ document.getElementById('card')?.scrollIntoView({block: 'center'});
483
+ });
484
+ const before = await page.evaluate(() => window.scrollY);
485
+ const start = await center(page, 'card');
486
+ // Up, slowly: the card's pan begins at 10 px and the page never moves.
487
+ await driver.play(drag(start, {x: start.x, y: start.y - 40}, 40));
488
+ expect(await page.evaluate(() => window.scrollY)).toBe(before);
489
+ const up = (await log(page)).filter(entry => entry.name === 'card');
490
+ expect(up[0]?.state).toBe('began');
491
+ expect(up.filter(entry => entry.state === 'changed').at(-1)?.y).toBe(-30);
492
+ // Down: the page's.
493
+ await clearLog(page);
494
+ await driver.play(drag(start, {x: start.x, y: start.y + 60}, 30));
495
+ expect(await page.evaluate(() => window.scrollY)).toBeLessThan(before - 20);
496
+ expect(states(await log(page), 'card')).toEqual(['failed', 'possible']);
497
+ });
498
+
499
+ test('a tap recognizer replaces the click: no ghost click, no compatibility mouse events', async ({
500
+ page,
501
+ browserName,
502
+ }) => {
503
+ const driver = await open(page, browserName);
504
+ await driver.tap(await center(page, 'tapper'));
505
+ let entries = await log(page);
506
+ expect(states(entries, 'tap')).toEqual(['ended', 'possible']);
507
+ expect(entries.filter(entry => entry.state === 'dom')).toEqual([]);
508
+
509
+ // A tap the pan does not take keeps its click (exactly one).
510
+ await clearLog(page);
511
+ await driver.tap(await center(page, 'hbutton'));
512
+ entries = await log(page);
513
+ expect(entries.filter(entry => entry.name === 'click:hbutton')).toHaveLength(
514
+ 1,
515
+ );
516
+ });
517
+
518
+ test('a keyboard activation inside the click window after a claimed drag still clicks: the swallower passes a click whose detail is 0 (FR21)', async ({
519
+ page,
520
+ browserName,
521
+ }) => {
522
+ const driver = await open(page, browserName);
523
+ // A pan claims the drag: its touchend is cancelled, so no click follows it,
524
+ // and the swallower stays armed for the stray one it may still owe.
525
+ const start = await center(page, 'hpan');
526
+ await driver.play(drag(start, {x: start.x + 40, y: start.y}, 8));
527
+ expect(states(await log(page), 'hpan')).toContain('ended');
528
+ await clearLog(page);
529
+ // Enter on a focused button: a click whose detail is 0, which passes.
530
+ await page.focus('#hbutton');
531
+ await page.keyboard.press('Enter');
532
+ const entries = await log(page);
533
+ expect(entries.filter(entry => entry.name === 'click:hbutton')).toHaveLength(
534
+ 1,
535
+ );
536
+ });
537
+
538
+ test('scroll edge: at the top the pull is the sheet’s at once', async ({
539
+ page,
540
+ browserName,
541
+ }) => {
542
+ const driver = await open(page, browserName);
543
+ const start = await center(page, 'sheetBody');
544
+ await driver.play(drag(start, {x: start.x, y: start.y + 96}));
545
+ const entries = await log(page);
546
+ const sheet = entries.filter(entry => entry.name === 'sheet');
547
+ expect(sheet[0]?.state).toBe('began');
548
+ expect(
549
+ sheet.filter(entry => entry.state === 'changed').at(-1)?.y,
550
+ ).toBeCloseTo(96 - 10, 0);
551
+ expect(await scrollTop(page, 'sheetBody')).toBe(0);
552
+ });
553
+
554
+ test('a drag that began as a scroll stays the browser’s (AST-094 FR14): the content scrolls to its top and the sheet never begins', async ({
555
+ page,
556
+ browserName,
557
+ }) => {
558
+ test.skip(
559
+ browserName === 'webkit',
560
+ 'needs native scrolling from injected touches (Mobile Safari on the simulator)',
561
+ );
562
+ const driver = await open(page, browserName);
563
+ await page.evaluate(() => {
564
+ (document.getElementById('sheetBody') as HTMLElement).scrollTop = 60;
565
+ });
566
+ const start = await center(page, 'sheetBody');
567
+ start.y -= 60;
568
+ await driver.play(drag(start, {x: start.x, y: start.y + 200}, 25));
569
+ const entries = await log(page);
570
+ const sheet = entries.filter(entry => entry.name === 'sheet');
571
+ // The content takes the whole drag: it scrolls to its top, and the travel
572
+ // past that point never becomes the sheet's.
573
+ expect(await scrollTop(page, 'sheetBody')).toBe(0);
574
+ expect(sheet.some(entry => entry.state === 'began')).toBe(false);
575
+ });
576
+
577
+ /** Two fingers that land one after the other and spread apart. */
578
+ function spread(at: Point, from: number, by: number, drift = 0): Step[] {
579
+ const steps: Step[] = [
580
+ {t: 0, down: [{x: at.x - from, y: at.y, id: 1}]},
581
+ {
582
+ t: 30,
583
+ down: [
584
+ {x: at.x - from, y: at.y, id: 1},
585
+ {x: at.x + from, y: at.y, id: 2},
586
+ ],
587
+ },
588
+ ];
589
+ for (let i = 1; i <= 12; i++) {
590
+ const half = from + (by * i) / 12;
591
+ steps.push({
592
+ t: 30 + i * 16,
593
+ down: [
594
+ {x: at.x - half + drift * i, y: at.y, id: 1},
595
+ {x: at.x + half + drift * i, y: at.y, id: 2},
596
+ ],
597
+ });
598
+ }
599
+ steps.push({t: 30 + 13 * 16, down: []});
600
+ return steps;
601
+ }
602
+
603
+ test('pinch-zoom stays the browser’s on an element without a pinch recognizer', async ({
604
+ page,
605
+ browserName,
606
+ }) => {
607
+ test.skip(
608
+ browserName === 'webkit',
609
+ 'needs native pinch-zoom from injected touches (Mobile Safari on the simulator)',
610
+ );
611
+ const driver = await open(page, browserName);
612
+ await driver.play(spread(await center(page, 'tapper'), 20, 96));
613
+ await page.waitForTimeout(400);
614
+ expect(
615
+ await page.evaluate(() => window.visualViewport?.scale ?? 1),
616
+ ).toBeGreaterThan(1.2);
617
+ expect(states(await log(page), 'tap')).not.toContain('ended');
618
+ });
619
+
620
+ test('a pinch recognizer takes the two-finger gesture on its element only, alongside its pan', async ({
621
+ page,
622
+ browserName,
623
+ }) => {
624
+ const driver = await open(page, browserName);
625
+ await driver.play(spread(await center(page, 'pinchable'), 20, 40, 1));
626
+ const entries = await log(page);
627
+ expect(states(entries, 'pinch')[0]).toBe('began');
628
+ const scale = entries
629
+ .filter(entry => entry.name === 'pinch' && entry.state === 'changed')
630
+ .at(-1)?.scale;
631
+ // The span grew from 40 to 120; as in UIKit, the scale is measured from
632
+ // the starting span moved by the 8 px hysteresis.
633
+ expect(scale).toBeCloseTo(120 / (40 + 8), 1);
634
+ expect(states(entries, 'pinchPan')).toContain('began');
635
+ expect(await page.evaluate(() => window.visualViewport?.scale ?? 1)).toBe(1);
636
+ });
637
+
638
+ test('a second finger never breaks a claimed one-finger pan', async ({
639
+ page,
640
+ browserName,
641
+ }) => {
642
+ const driver = await open(page, browserName);
643
+ const start = await center(page, 'hbutton');
644
+ const other = await center(page, 'tapper');
645
+ const steps: Step[] = [{t: 0, down: [{...start, id: 1}]}];
646
+ for (let i = 1; i <= 4; i++) {
647
+ steps.push({t: i * 16, down: [{x: start.x + i * 10, y: start.y, id: 1}]});
648
+ }
649
+ const first = {x: start.x + 40, y: start.y, id: 1};
650
+ steps.push({t: 80, down: [first, {...other, id: 2}]});
651
+ for (let i = 1; i <= 6; i++) {
652
+ steps.push({
653
+ t: 80 + i * 16,
654
+ down: [
655
+ {x: first.x + i * 5, y: first.y, id: 1},
656
+ {x: other.x + i * 15, y: other.y + i * 15, id: 2},
657
+ ],
658
+ });
659
+ }
660
+ steps.push({
661
+ t: 200,
662
+ down: [{x: other.x + 90, y: other.y + 90, id: 2}],
663
+ });
664
+ steps.push({t: 216, down: []});
665
+ await driver.play(steps);
666
+ const entries = await log(page);
667
+ const hpan = entries.filter(entry => entry.name === 'hpan');
668
+ expect(hpan.map(entry => entry.state)).not.toContain('cancelled');
669
+ expect(hpan.map(entry => entry.state)).toContain('ended');
670
+ expect(hpan.filter(entry => entry.state === 'changed').at(-1)?.x).toBeCloseTo(
671
+ 70 - 10,
672
+ 0,
673
+ );
674
+ expect(await page.evaluate(() => window.visualViewport?.scale ?? 1)).toBe(1);
675
+ });
676
+
677
+ test('a long press begins at touch-down + 500 ms on the event clock, even when the main thread was blocked', async ({
678
+ page,
679
+ browserName,
680
+ }) => {
681
+ const driver = await open(page, browserName);
682
+ const at = await center(page, 'presser');
683
+ const base = await driver.play([{t: 0, down: [{...at, id: 1}]}]);
684
+ // Block the main thread well past the press duration.
685
+ await page.evaluate(() => {
686
+ const end = performance.now() + 800;
687
+ while (performance.now() < end) {
688
+ // busy
689
+ }
690
+ });
691
+ await page.waitForTimeout(100);
692
+ await driver.play([{t: 0, down: []}]);
693
+ const entries = await log(page);
694
+ const began = entries.find(
695
+ entry => entry.name === 'press' && entry.state === 'began',
696
+ );
697
+ if (driver.engine === 'chromium') {
698
+ // The touch carries the timestamp CDP gave it (to the browser's 0.1 ms
699
+ // timestamp precision).
700
+ expect(Math.abs((began?.startedAt ?? 0) - base)).toBeLessThan(0.5);
701
+ }
702
+ // The press began exactly 500 ms after the touch, though the timer that
703
+ // noticed it ran 300 ms late.
704
+ expect((began?.beganAt ?? 0) - (began?.startedAt ?? 0)).toBeCloseTo(500, 6);
705
+ expect(states(entries, 'press')).toEqual(['began', 'ended', 'possible']);
706
+ expect(entries.some(entry => entry.name.startsWith('click:'))).toBe(false);
707
+ });
708
+
709
+ async function scrollToCenter(page: Page, id: string): Promise<void> {
710
+ await page.evaluate(target => {
711
+ document.getElementById(target)?.scrollIntoView({block: 'center'});
712
+ }, id);
713
+ }
714
+
715
+ async function resetMoves(page: Page): Promise<void> {
716
+ await page.evaluate(() => {
717
+ (window as unknown as {__moves: boolean[]}).__moves.length = 0;
718
+ });
719
+ }
720
+
721
+ test('takes a pan at 10 px of travel on its axis and not at 9: the move that reaches 10 is the first cancelled (FR19)', async ({
722
+ page,
723
+ browserName,
724
+ }) => {
725
+ const driver = await open(page, browserName);
726
+ await logTouchMoves(page);
727
+ const start = await center(page, 'hpan');
728
+ await driver.play(travel(start, alongX(3, 6, 9)));
729
+ expect(states(await log(page), 'hpan')).not.toContain('began');
730
+ expect((await readTouchMoves(page)).some(Boolean)).toBe(false);
731
+ await clearLog(page);
732
+ await resetMoves(page);
733
+ await driver.play(travel(start, alongX(3, 6, 10)));
734
+ expect(states(await log(page), 'hpan')[0]).toBe('began');
735
+ if (driver.engine === 'webkit') {
736
+ // WebKit delivers every move. Chromium holds back the moves inside its
737
+ // own touch slop, so the page sees none of them to cancel.
738
+ const moves = await readTouchMoves(page);
739
+ expect(moves.at(-1)).toBe(true);
740
+ expect(moves.filter(Boolean)).toHaveLength(1);
741
+ }
742
+ });
743
+
744
+ test('measures a pan’s travel per axis: a free pan stays possible at (7, 7) and (8, 8) and begins at (10, 10) (FR19)', async ({
745
+ page,
746
+ browserName,
747
+ }) => {
748
+ const driver = await open(page, browserName);
749
+ const start = await center(page, 'pinchable');
750
+ for (const d of [7, 8]) {
751
+ await clearLog(page);
752
+ await driver.play(
753
+ travel(start, [
754
+ {x: d - 4, y: d - 4},
755
+ {x: d, y: d},
756
+ ]),
757
+ );
758
+ expect(states(await log(page), 'pinchPan')).not.toContain('began');
759
+ }
760
+ await clearLog(page);
761
+ await driver.play(
762
+ travel(start, [
763
+ {x: 6, y: 6},
764
+ {x: 10, y: 10},
765
+ ]),
766
+ );
767
+ expect(states(await log(page), 'pinchPan')[0]).toBe('began');
768
+ });
769
+
770
+ test('keeps a narrower author touch-action: the derived value intersected with the author’s, never wider (FR18, DEC-8)', async ({
771
+ page,
772
+ browserName,
773
+ }) => {
774
+ const driver = await open(page, browserName);
775
+ // A class rule wider than what both allow is narrowed inline after a frame.
776
+ await page.evaluate(
777
+ async () =>
778
+ new Promise(resolve =>
779
+ requestAnimationFrame(() => requestAnimationFrame(resolve)),
780
+ ),
781
+ );
782
+ const values = await page.evaluate(() =>
783
+ Object.fromEntries(
784
+ [
785
+ 'authorNone',
786
+ 'authorPanY',
787
+ 'authorPinchZoom',
788
+ 'authorPanXY',
789
+ 'inlinePanXY',
790
+ ].map(id => {
791
+ const element = document.getElementById(id) as HTMLElement;
792
+ return [
793
+ id,
794
+ [getComputedStyle(element).touchAction, element.style.touchAction],
795
+ ];
796
+ }),
797
+ ),
798
+ );
799
+ expect(values).toEqual({
800
+ // A long press derives `manipulation`; the author's `none` stands.
801
+ authorNone: ['none', ''],
802
+ // Author values the derived `pan-y pinch-zoom` contains: theirs, no write.
803
+ authorPanY: ['pan-y', ''],
804
+ authorPinchZoom: ['pinch-zoom', ''],
805
+ // Wider author values: what both allow, written inline.
806
+ authorPanXY: ['pan-y', 'pan-y'],
807
+ inlinePanXY: ['pan-y', 'pan-y'],
808
+ });
809
+ // Each pan still takes a horizontal drag.
810
+ for (const id of [
811
+ 'authorPanY',
812
+ 'authorPinchZoom',
813
+ 'authorPanXY',
814
+ 'inlinePanXY',
815
+ ]) {
816
+ await scrollToCenter(page, id);
817
+ await clearLog(page);
818
+ const start = await center(page, id);
819
+ await driver.play(drag(start, {x: start.x + 60, y: start.y}, 6));
820
+ expect(states(await log(page), id)[0]).toBe('began');
821
+ }
822
+ if (driver.engine === 'chromium') {
823
+ // The browser keeps the axis the author left it, and only that: none on
824
+ // the author's `none`, the vertical one on the author's `pan-y`.
825
+ await scrollToCenter(page, 'authorNone');
826
+ let before = await page.evaluate(() => window.scrollY);
827
+ let start = await center(page, 'authorNone');
828
+ await driver.play(drag(start, {x: start.x, y: start.y - 80}, 10));
829
+ expect(await page.evaluate(() => window.scrollY)).toBe(before);
830
+ await scrollToCenter(page, 'authorPanY');
831
+ before = await page.evaluate(() => window.scrollY);
832
+ start = await center(page, 'authorPanY');
833
+ await driver.play(drag(start, {x: start.x, y: start.y - 80}, 10));
834
+ expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(
835
+ before + 20,
836
+ );
837
+ }
838
+ });
839
+
840
+ test('an edge-zone strip claims pan-x for its band, and a touch 1 px inside the edge begins at 15 px of inward travel, not 14 (FR18, DEC-13)', async ({
841
+ page,
842
+ browserName,
843
+ }) => {
844
+ const driver = await open(page, browserName);
845
+ await scrollToCenter(page, 'edgeZone');
846
+ expect(
847
+ await page.evaluate(() => [
848
+ getComputedStyle(document.getElementById('edgeZone') as HTMLElement)
849
+ .touchAction,
850
+ getComputedStyle(document.getElementById('edgeWide') as HTMLElement)
851
+ .touchAction,
852
+ document
853
+ .getElementById('edgeWide')
854
+ ?.getAttribute('data-astryx-touch-action') ?? null,
855
+ ]),
856
+ ).toEqual(['pan-y pinch-zoom', 'auto', null]);
857
+ const {y} = await center(page, 'edgeZone');
858
+ await driver.play(travel({x: 39, y}, alongX(5, 10, 14)));
859
+ expect(states(await log(page), 'edgeZone')).not.toContain('began');
860
+ await clearLog(page);
861
+ await driver.play(travel({x: 39, y}, alongX(5, 10, 15)));
862
+ expect(states(await log(page), 'edgeZone')[0]).toBe('began');
863
+ });
864
+
865
+ test('on an element reaching beyond the band, a touch at 40 px decides at 10 px and one at 41 px fails at touch-down (FR18, DEC-13)', async ({
866
+ page,
867
+ browserName,
868
+ }) => {
869
+ const driver = await open(page, browserName);
870
+ await logTouchMoves(page);
871
+ await scrollToCenter(page, 'edgeWide');
872
+ const {y} = await center(page, 'edgeWide');
873
+ await driver.play(travel({x: 40, y}, alongX(3, 6, 9)));
874
+ expect(states(await log(page), 'edgeWide')).not.toContain('began');
875
+ await clearLog(page);
876
+ await resetMoves(page);
877
+ await driver.play(travel({x: 40, y}, alongX(3, 6, 10)));
878
+ expect(states(await log(page), 'edgeWide')[0]).toBe('began');
879
+ if (driver.engine === 'webkit') {
880
+ // From the move it begins on, it claims by cancelling them (Chromium
881
+ // holds back moves inside its touch slop).
882
+ expect((await readTouchMoves(page)).at(-1)).toBe(true);
883
+ }
884
+ await clearLog(page);
885
+ await resetMoves(page);
886
+ await driver.play(travel({x: 41, y}, alongX(10, 20, 30)));
887
+ expect(states(await log(page), 'edgeWide')[0]).toBe('failed');
888
+ expect((await readTouchMoves(page)).some(Boolean)).toBe(false);
889
+ if (driver.engine === 'chromium') {
890
+ // Outside the band the author's horizontal scroll goes ahead.
891
+ await clearLog(page);
892
+ await driver.play(drag({x: 100, y}, {x: 20, y}, 10));
893
+ expect(states(await log(page), 'edgeWide')[0]).toBe('failed');
894
+ expect(
895
+ await page.evaluate(
896
+ () => document.getElementById('edgeWide')?.scrollLeft ?? 0,
897
+ ),
898
+ ).toBeGreaterThan(20);
899
+ }
900
+ });
901
+
902
+ test('under RTL the leading band is the right one: 362 px begins at 15 px leftward on the strip, and a leading pan fails a touch at 361 px (FR18, DEC-13)', async ({
903
+ page,
904
+ browserName,
905
+ }) => {
906
+ const driver = await open(page, browserName);
907
+ await scrollToCenter(page, 'edgeZoneRtl');
908
+ const zone = await center(page, 'edgeZoneRtl');
909
+ await driver.play(travel({x: 363, y: zone.y}, alongX(-5, -10, -14)));
910
+ expect(states(await log(page), 'edgeZoneRtl')).not.toContain('began');
911
+ await clearLog(page);
912
+ await driver.play(travel({x: 363, y: zone.y}, alongX(-5, -10, -15)));
913
+ expect(states(await log(page), 'edgeZoneRtl')[0]).toBe('began');
914
+ await scrollToCenter(page, 'edgeWideRtl');
915
+ const wide = await center(page, 'edgeWideRtl');
916
+ await clearLog(page);
917
+ await driver.play(travel({x: 361, y: wide.y}, alongX(-10, -20)));
918
+ expect(states(await log(page), 'edgeWideRtl')[0]).toBe('failed');
919
+ await clearLog(page);
920
+ await driver.play(travel({x: 362, y: wide.y}, alongX(-3, -6, -10)));
921
+ expect(states(await log(page), 'edgeWideRtl')[0]).toBe('began');
922
+ });
923
+
924
+ test('a scroll under a held press takes it only once the finger is 10 px from where it landed: a still finger, 9 px and (7, 7) keep it, 10 px and (8, 8) lose it (FR20)', async ({
925
+ page,
926
+ browserName,
927
+ }) => {
928
+ const driver = await open(page, browserName);
929
+ const row = await center(page, 'scrollRow');
930
+ const finger = (offset: Point): Finger => ({
931
+ x: row.x + offset.x,
932
+ y: row.y + offset.y,
933
+ id: 1,
934
+ });
935
+ const cases: ReadonlyArray<{
936
+ name: string;
937
+ path: ReadonlyArray<Point>;
938
+ kept: boolean;
939
+ }> = [
940
+ // Sub-pixel jitter where the engine takes it (WebKit's command rounds to
941
+ // whole pixels, so there the finger is still).
942
+ {
943
+ name: 'still',
944
+ path: [
945
+ {x: 0.3, y: -0.3},
946
+ {x: -0.3, y: 0.3},
947
+ {x: 0.3, y: 0},
948
+ ],
949
+ kept: true,
950
+ },
951
+ {name: '9 px', path: alongX(3, 6, 9), kept: true},
952
+ {
953
+ name: '(7, 7)',
954
+ path: [
955
+ {x: 3, y: 3},
956
+ {x: 7, y: 7},
957
+ ],
958
+ kept: true,
959
+ },
960
+ {name: '10 px', path: alongX(3, 6, 10), kept: false},
961
+ {
962
+ name: '(8, 8)',
963
+ path: [
964
+ {x: 4, y: 4},
965
+ {x: 8, y: 8},
966
+ ],
967
+ kept: false,
968
+ },
969
+ ];
970
+ for (const {name, path: offsets, kept} of cases) {
971
+ await page.evaluate(() => {
972
+ (document.getElementById('scroller') as HTMLElement).scrollTop = 0;
973
+ });
974
+ await clearLog(page);
975
+ // Chromium holds back the moves inside its own touch slop, so a finger
976
+ // held there is still to the page: it first goes past the slop, and the
977
+ // page sees every move after that.
978
+ const excursion: Point[] =
979
+ driver.engine === 'chromium' ? [{x: 20, y: 20}] : [];
980
+ const steps: Step[] = [{t: 0, down: [finger({x: 0, y: 0})]}];
981
+ [...excursion, ...offsets].forEach((offset, index) => {
982
+ steps.push({t: (index + 1) * 16, down: [finger(offset)]});
983
+ });
984
+ await driver.play(steps);
985
+ // The finger rests at its offset while the scroller scrolls under it.
986
+ await page.evaluate(() => {
987
+ (document.getElementById('scroller') as HTMLElement).scrollTop += 40;
988
+ });
989
+ await page.waitForTimeout(100);
990
+ await driver.play([{t: 0, down: []}]);
991
+ const pressed = states(await log(page), 'scrollRow');
992
+ expect(pressed[0], name).toBe('began');
993
+ if (kept) {
994
+ expect(pressed, name).not.toContain('cancelled');
995
+ expect(pressed.at(-1), name).toBe('ended');
996
+ } else {
997
+ expect(pressed.at(-1), name).toBe('cancelled');
998
+ }
999
+ }
1000
+ });
1001
+
1002
+ /** The page-level listener counts now, by type. */
1003
+ async function pageListenerCounts(page: Page): Promise<Map<string, number>> {
1004
+ return new Map(await page.evaluate(() => [...window.__pageListeners]));
1005
+ }
1006
+
1007
+ /**
1008
+ * The page-level listener types gesture work uses that are added beyond
1009
+ * `baseline`: the engine and the test runner keep listeners of their own.
1010
+ */
1011
+ async function addedPageListeners(
1012
+ page: Page,
1013
+ baseline: Map<string, number>,
1014
+ ): Promise<string[]> {
1015
+ return [...(await pageListenerCounts(page))]
1016
+ .filter(
1017
+ ([type, count]) =>
1018
+ count > (baseline.get(type) ?? 0) &&
1019
+ /^(touch|pointer|click$|blur$|scroll$|contextmenu$|visibilitychange$)/.test(
1020
+ type,
1021
+ ),
1022
+ )
1023
+ .map(([type]) => type)
1024
+ .sort();
1025
+ }
1026
+
1027
+ test.describe('under a fine primary pointer (spec:AST-094 FR43, FR24)', () => {
1028
+ test.use({hasTouch: false, isMobile: false});
1029
+
1030
+ test('a mouse drag begins a pan at 10 px on its axis and ends it at the lift; the click after it reaches no page handler', async ({
1031
+ page,
1032
+ browserName,
1033
+ }) => {
1034
+ await open(page, browserName);
1035
+ expect(
1036
+ await page.evaluate(() => matchMedia('(pointer: coarse)').matches),
1037
+ ).toBe(false);
1038
+ const start = await center(page, 'hpan');
1039
+ await page.mouse.move(start.x, start.y);
1040
+ await page.mouse.down();
1041
+ await page.mouse.move(start.x + 9, start.y);
1042
+ expect(states(await log(page), 'hpan')).not.toContain('began');
1043
+ await page.mouse.move(start.x + 10, start.y);
1044
+ expect(states(await log(page), 'hpan')[0]).toBe('began');
1045
+ await page.mouse.up();
1046
+ const entries = await log(page);
1047
+ expect(states(entries, 'hpan').slice(-2)).toEqual(['ended', 'possible']);
1048
+ expect(entries.some(entry => entry.name.startsWith('click:'))).toBe(false);
1049
+ });
1050
+
1051
+ test('a mouse tap recognizes a tap, and the right button starts nothing', async ({
1052
+ page,
1053
+ browserName,
1054
+ }) => {
1055
+ await open(page, browserName);
1056
+ const at = await center(page, 'tapper');
1057
+ await page.mouse.click(at.x, at.y, {button: 'right'});
1058
+ expect(states(await log(page), 'tap')).toEqual([]);
1059
+ await page.mouse.click(at.x, at.y);
1060
+ expect(states(await log(page), 'tap')).toContain('ended');
1061
+ });
1062
+
1063
+ test('the page carries no gesture listener between presses: a press adds them, and they go after it', async ({
1064
+ page,
1065
+ browserName,
1066
+ }) => {
1067
+ await open(page, browserName);
1068
+ // Attaching every gesture of the page added no page-level listener.
1069
+ expect(await page.evaluate(() => window.__attachListeners)).toEqual([]);
1070
+ // A press that nothing claims (a pan pressed and lifted in place).
1071
+ const start = await center(page, 'hpan');
1072
+ await page.mouse.move(start.x, start.y);
1073
+ const idle = await pageListenerCounts(page);
1074
+ await page.mouse.down();
1075
+ expect(await addedPageListeners(page, idle)).toEqual(
1076
+ expect.arrayContaining(['pointercancel', 'pointermove', 'pointerup']),
1077
+ );
1078
+ await page.mouse.up();
1079
+ await page.waitForTimeout(50);
1080
+ expect(await addedPageListeners(page, idle)).toEqual([]);
1081
+ // A right click adds none.
1082
+ await page.mouse.click(start.x, start.y, {button: 'right'});
1083
+ expect(await addedPageListeners(page, idle)).toEqual([]);
1084
+ // After a claimed drag only the click swallower stays, for 1 s.
1085
+ await page.mouse.down();
1086
+ await page.mouse.move(start.x + 40, start.y, {steps: 4});
1087
+ await page.mouse.up();
1088
+ await page.waitForTimeout(50);
1089
+ expect(await addedPageListeners(page, idle)).toEqual(['click']);
1090
+ await page.waitForTimeout(1100);
1091
+ expect(await addedPageListeners(page, idle)).toEqual([]);
1092
+ });
1093
+
1094
+ test('a pen drag begins a pan at 10 px like a mouse; the pen’s barrel button starts nothing', async ({
1095
+ page,
1096
+ browserName,
1097
+ }) => {
1098
+ test.skip(browserName === 'webkit', 'no pen input in WebKit’s driver');
1099
+ await open(page, browserName);
1100
+ const cdp = await page.context().newCDPSession(page);
1101
+ const pen = async (
1102
+ type: 'mousePressed' | 'mouseMoved' | 'mouseReleased',
1103
+ x: number,
1104
+ y: number,
1105
+ button: 'left' | 'right' | 'none',
1106
+ ) =>
1107
+ cdp.send('Input.dispatchMouseEvent', {
1108
+ type,
1109
+ x,
1110
+ y,
1111
+ button,
1112
+ buttons:
1113
+ type === 'mouseReleased' || button === 'none'
1114
+ ? 0
1115
+ : button === 'left'
1116
+ ? 1
1117
+ : 2,
1118
+ clickCount: type === 'mouseMoved' ? 0 : 1,
1119
+ pointerType: 'pen',
1120
+ });
1121
+ const start = await center(page, 'hpan');
1122
+ await pen('mousePressed', start.x, start.y, 'right');
1123
+ await pen('mouseMoved', start.x + 30, start.y, 'right');
1124
+ await pen('mouseReleased', start.x + 30, start.y, 'right');
1125
+ expect(states(await log(page), 'hpan')).toEqual([]);
1126
+ await pen('mousePressed', start.x, start.y, 'left');
1127
+ await pen('mouseMoved', start.x + 9, start.y, 'left');
1128
+ expect(states(await log(page), 'hpan')).not.toContain('began');
1129
+ await pen('mouseMoved', start.x + 10, start.y, 'left');
1130
+ expect(states(await log(page), 'hpan')[0]).toBe('began');
1131
+ await pen('mouseReleased', start.x + 10, start.y, 'left');
1132
+ expect(states(await log(page), 'hpan').slice(-2)).toEqual([
1133
+ 'ended',
1134
+ 'possible',
1135
+ ]);
1136
+ });
1137
+ });