@astryxdesign/core 0.7.6 → 0.7.7-canary.0eb44a2

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 (272) hide show
  1. package/dist/AppShell/AppShell.d.ts.map +1 -1
  2. package/dist/AppShell/AppShell.js +23 -3
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +1 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetPanel.js +16 -9
  7. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  8. package/dist/Breadcrumbs/BreadcrumbItem.js +2 -1
  9. package/dist/Button/Button.d.ts.map +1 -1
  10. package/dist/Button/Button.js +3 -2
  11. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  12. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  13. package/dist/Chat/ChatComposerInput.js +2 -0
  14. package/dist/Chat/ChatMessage.js +2 -2
  15. package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
  16. package/dist/Chat/chatMessage.vars.stylex.js +8 -8
  17. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  18. package/dist/CommandPalette/CommandPaletteInput.js +8 -1
  19. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  20. package/dist/DateInput/MonthScroller.js +72 -2
  21. package/dist/DateInput/Wheel.d.ts +17 -1
  22. package/dist/DateInput/Wheel.d.ts.map +1 -1
  23. package/dist/DateInput/Wheel.js +179 -19
  24. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  25. package/dist/DateRangeInput/DateRangeInput.js +63 -49
  26. package/dist/Dialog/Dialog.d.ts.map +1 -1
  27. package/dist/Dialog/Dialog.js +37 -1
  28. package/dist/Drawer/Drawer.d.ts.map +1 -1
  29. package/dist/Drawer/Drawer.js +6 -0
  30. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
  31. package/dist/Drawer/useDrawerDialogPresence.js +4 -0
  32. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  33. package/dist/DropdownMenu/DropdownMenu.js +20 -4
  34. package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
  35. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
  36. package/dist/DropdownMenu/useMenuOverflow.js +31 -8
  37. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  38. package/dist/Field/PanelSearchInput.js +8 -1
  39. package/dist/Field/textEntryTraits.d.ts +6 -6
  40. package/dist/Field/textEntryTraits.js +6 -6
  41. package/dist/Layer/layerClickBoundary.d.ts +7 -1
  42. package/dist/Layer/layerClickBoundary.d.ts.map +1 -1
  43. package/dist/Layer/layerClickBoundary.js +2 -2
  44. package/dist/Layer/usePressPin.d.ts +95 -0
  45. package/dist/Layer/usePressPin.d.ts.map +1 -0
  46. package/dist/Layer/usePressPin.js +365 -0
  47. package/dist/Layer/useTouchTrigger.d.ts +23 -1
  48. package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
  49. package/dist/Layer/useTouchTrigger.js +45 -7
  50. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  51. package/dist/Lightbox/Lightbox.js +2 -1
  52. package/dist/Link/Link.d.ts.map +1 -1
  53. package/dist/Link/Link.js +1 -0
  54. package/dist/Popover/usePopover.d.ts.map +1 -1
  55. package/dist/Popover/usePopover.js +56 -7
  56. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  57. package/dist/PowerSearch/PowerSearch.js +38 -30
  58. package/dist/ProgressBar/ProgressBar.d.ts +1 -1
  59. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  60. package/dist/ProgressBar/ProgressBar.js +52 -14
  61. package/dist/ProgressBar/ProgressBarTouch.d.ts +29 -0
  62. package/dist/ProgressBar/ProgressBarTouch.d.ts.map +1 -0
  63. package/dist/ProgressBar/ProgressBarTouch.js +91 -0
  64. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  65. package/dist/SegmentedControl/SegmentedControlItem.js +2 -1
  66. package/dist/Selector/Selector.d.ts.map +1 -1
  67. package/dist/Selector/Selector.js +25 -13
  68. package/dist/Slider/Slider.d.ts.map +1 -1
  69. package/dist/Slider/Slider.js +404 -131
  70. package/dist/Slider/sliderTouchController.d.ts +189 -0
  71. package/dist/Slider/sliderTouchController.d.ts.map +1 -0
  72. package/dist/Slider/sliderTouchController.js +1017 -0
  73. package/dist/Slider/useSliderTouch.d.ts +22 -0
  74. package/dist/Slider/useSliderTouch.d.ts.map +1 -0
  75. package/dist/Slider/useSliderTouch.js +56 -0
  76. package/dist/Switch/Switch.d.ts +1 -1
  77. package/dist/Switch/Switch.d.ts.map +1 -1
  78. package/dist/Switch/Switch.js +200 -46
  79. package/dist/Switch/switchFill.d.ts +31 -0
  80. package/dist/Switch/switchFill.d.ts.map +1 -0
  81. package/dist/Switch/switchFill.js +88 -0
  82. package/dist/Switch/switchTouch.d.ts +81 -0
  83. package/dist/Switch/switchTouch.d.ts.map +1 -0
  84. package/dist/Switch/switchTouch.js +133 -0
  85. package/dist/Switch/useSwitchTouch.d.ts +285 -0
  86. package/dist/Switch/useSwitchTouch.d.ts.map +1 -0
  87. package/dist/Switch/useSwitchTouch.js +998 -0
  88. package/dist/TabList/Tab.d.ts.map +1 -1
  89. package/dist/TabList/Tab.js +4 -3
  90. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  91. package/dist/Tokenizer/Tokenizer.js +12 -5
  92. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts +3 -0
  93. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -0
  94. package/dist/Tokenizer/TokenizerItemRemovableContext.js +17 -0
  95. package/dist/Tooltip/Tooltip.d.ts +30 -6
  96. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  97. package/dist/Tooltip/Tooltip.js +5 -0
  98. package/dist/Tooltip/index.d.ts +1 -1
  99. package/dist/Tooltip/index.d.ts.map +1 -1
  100. package/dist/Tooltip/useTooltip.d.ts +31 -5
  101. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  102. package/dist/Tooltip/useTooltip.js +59 -18
  103. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  104. package/dist/Typeahead/BaseTypeahead.js +19 -9
  105. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  106. package/dist/Typeahead/Typeahead.js +26 -15
  107. package/dist/astryx.css +91 -8
  108. package/dist/gestures/GestureArena.d.ts +21 -2
  109. package/dist/gestures/GestureArena.d.ts.map +1 -1
  110. package/dist/gestures/GestureArena.js +23 -3
  111. package/dist/gestures/GestureHost.d.ts +123 -3
  112. package/dist/gestures/GestureHost.d.ts.map +1 -1
  113. package/dist/gestures/GestureHost.js +797 -15
  114. package/dist/gestures/GestureRecognizer.d.ts +15 -1
  115. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  116. package/dist/gestures/GestureRecognizer.js +12 -0
  117. package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
  118. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  119. package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
  120. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
  121. package/dist/gestures/PinchGestureRecognizer.js +10 -0
  122. package/dist/gestures/PressTracker.d.ts +6 -0
  123. package/dist/gestures/PressTracker.d.ts.map +1 -1
  124. package/dist/gestures/PressTracker.js +3 -0
  125. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
  126. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
  127. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
  128. package/dist/gestures/nativeScroll.d.ts +16 -2
  129. package/dist/gestures/nativeScroll.d.ts.map +1 -1
  130. package/dist/gestures/nativeScroll.js +39 -8
  131. package/dist/gestures/recognizerInternals.d.ts +4 -2
  132. package/dist/gestures/recognizerInternals.d.ts.map +1 -1
  133. package/dist/gestures/types.d.ts +17 -0
  134. package/dist/gestures/types.d.ts.map +1 -1
  135. package/dist/gestures/types.js +7 -0
  136. package/dist/gestures/usePressTracker.d.ts.map +1 -1
  137. package/dist/gestures/usePressTracker.js +1 -0
  138. package/dist/gestures/wheelSegmenter.d.ts +56 -0
  139. package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
  140. package/dist/gestures/wheelSegmenter.js +122 -0
  141. package/dist/hooks/useAttachment.d.ts +16 -8
  142. package/dist/hooks/useAttachment.d.ts.map +1 -1
  143. package/dist/hooks/useAttachment.js +35 -15
  144. package/dist/hooks/useFocusTrap.d.ts +15 -0
  145. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  146. package/dist/hooks/useFocusTrap.js +13 -2
  147. package/dist/hooks/usePager.d.ts +261 -0
  148. package/dist/hooks/usePager.d.ts.map +1 -0
  149. package/dist/hooks/usePager.js +1169 -0
  150. package/dist/motion/animator.d.ts.map +1 -1
  151. package/dist/motion/animator.js +140 -19
  152. package/dist/motion/paging.d.ts +69 -0
  153. package/dist/motion/paging.d.ts.map +1 -0
  154. package/dist/motion/paging.js +148 -0
  155. package/dist/motion/uikit/index.d.ts +8 -4
  156. package/dist/motion/uikit/index.d.ts.map +1 -1
  157. package/dist/motion/uikit/index.js +8 -4
  158. package/dist/motion/uikit/paging.d.ts +171 -0
  159. package/dist/motion/uikit/paging.d.ts.map +1 -0
  160. package/dist/motion/uikit/paging.js +192 -0
  161. package/dist/motion/uikit/progressBar.d.ts +79 -0
  162. package/dist/motion/uikit/progressBar.d.ts.map +1 -0
  163. package/dist/motion/uikit/progressBar.js +102 -0
  164. package/dist/motion/uikit/scroll.d.ts +0 -19
  165. package/dist/motion/uikit/scroll.d.ts.map +1 -1
  166. package/dist/motion/uikit/scroll.js +2 -21
  167. package/dist/motion/uikit/slider.d.ts +97 -0
  168. package/dist/motion/uikit/slider.d.ts.map +1 -0
  169. package/dist/motion/uikit/slider.js +111 -0
  170. package/dist/motion/uikit/springs.d.ts +59 -30
  171. package/dist/motion/uikit/springs.d.ts.map +1 -1
  172. package/dist/motion/uikit/springs.js +64 -34
  173. package/dist/motion/uikit/switch.d.ts +166 -0
  174. package/dist/motion/uikit/switch.d.ts.map +1 -0
  175. package/dist/motion/uikit/switch.js +186 -0
  176. package/dist/privateAttributes/pressHoldsDrag.stylex.d.ts +18 -0
  177. package/dist/privateAttributes/pressHoldsDrag.stylex.d.ts.map +1 -0
  178. package/dist/privateAttributes/pressHoldsDrag.stylex.js +28 -0
  179. package/dist/utils/pressFeedback.d.ts.map +1 -1
  180. package/dist/utils/pressFeedback.js +5 -0
  181. package/dist/utils/selectionLock.d.ts +43 -0
  182. package/dist/utils/selectionLock.d.ts.map +1 -1
  183. package/dist/utils/selectionLock.js +394 -6
  184. package/package.json +5 -5
  185. package/src/AppShell/AppShell.doc.mjs +10 -0
  186. package/src/AppShell/AppShell.tsx +48 -2
  187. package/src/Avatar/Avatar.tsx +3 -1
  188. package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
  189. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  190. package/src/Button/Button.tsx +3 -2
  191. package/src/Chat/Chat.doc.mjs +1 -0
  192. package/src/Chat/ChatComposerInput.doc.mjs +11 -0
  193. package/src/Chat/ChatComposerInput.tsx +11 -0
  194. package/src/Chat/ChatMessage.tsx +2 -2
  195. package/src/Chat/chatMessage.vars.stylex.ts +8 -8
  196. package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
  197. package/src/DateInput/MonthScroller.tsx +63 -5
  198. package/src/DateInput/Wheel.tsx +174 -14
  199. package/src/DateRangeInput/DateRangeInput.doc.mjs +3 -2
  200. package/src/DateRangeInput/DateRangeInput.tsx +117 -71
  201. package/src/Dialog/Dialog.tsx +37 -1
  202. package/src/Drawer/Drawer.tsx +6 -0
  203. package/src/Drawer/useDrawerDialogPresence.ts +4 -0
  204. package/src/DropdownMenu/DropdownMenu.tsx +28 -6
  205. package/src/DropdownMenu/useMenuOverflow.ts +44 -8
  206. package/src/Field/PanelSearchInput.tsx +6 -0
  207. package/src/Field/textEntryTraits.ts +6 -6
  208. package/src/Layer/layerClickBoundary.ts +2 -2
  209. package/src/Layer/usePressPin.ts +516 -0
  210. package/src/Layer/useTouchTrigger.ts +56 -6
  211. package/src/Lightbox/Lightbox.tsx +2 -1
  212. package/src/Link/Link.tsx +3 -0
  213. package/src/Popover/usePopover.tsx +71 -9
  214. package/src/PowerSearch/PowerSearch.tsx +38 -31
  215. package/src/ProgressBar/ProgressBar.doc.mjs +7 -5
  216. package/src/ProgressBar/ProgressBar.tsx +147 -16
  217. package/src/ProgressBar/ProgressBarTouch.ts +109 -0
  218. package/src/SegmentedControl/SegmentedControlItem.tsx +2 -1
  219. package/src/Selector/Selector.tsx +29 -2
  220. package/src/Slider/Slider.doc.mjs +11 -7
  221. package/src/Slider/Slider.tsx +539 -132
  222. package/src/Slider/sliderTouchController.ts +1209 -0
  223. package/src/Slider/useSliderTouch.ts +78 -0
  224. package/src/Switch/Switch.doc.mjs +3 -3
  225. package/src/Switch/Switch.tsx +412 -86
  226. package/src/Switch/switchFill.ts +106 -0
  227. package/src/Switch/switchTouch.ts +194 -0
  228. package/src/Switch/useSwitchTouch.ts +1121 -0
  229. package/src/TabList/Tab.tsx +5 -3
  230. package/src/Tokenizer/Tokenizer.tsx +15 -5
  231. package/src/Tokenizer/TokenizerItemRemovableContext.ts +22 -0
  232. package/src/Tooltip/Tooltip.doc.mjs +21 -6
  233. package/src/Tooltip/Tooltip.tsx +42 -5
  234. package/src/Tooltip/index.ts +1 -0
  235. package/src/Tooltip/useTooltip.doc.mjs +4 -2
  236. package/src/Tooltip/useTooltip.tsx +84 -21
  237. package/src/Typeahead/BaseTypeahead.tsx +16 -8
  238. package/src/Typeahead/Typeahead.tsx +29 -15
  239. package/src/gestures/GestureArena.ts +45 -4
  240. package/src/gestures/GestureHost.ts +958 -15
  241. package/src/gestures/GestureRecognizer.ts +28 -0
  242. package/src/gestures/PanGestureRecognizer.ts +8 -0
  243. package/src/gestures/PinchGestureRecognizer.ts +16 -1
  244. package/src/gestures/PressTracker.ts +10 -0
  245. package/src/gestures/README.md +82 -3
  246. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
  247. package/src/gestures/nativeScroll.ts +49 -9
  248. package/src/gestures/recognizerInternals.ts +9 -2
  249. package/src/gestures/types.ts +18 -0
  250. package/src/gestures/usePressTracker.ts +2 -0
  251. package/src/gestures/wheelSegmenter.ts +143 -0
  252. package/src/hooks/useAttachment.ts +56 -14
  253. package/src/hooks/useFocusTrap.ts +26 -2
  254. package/src/hooks/usePager.ts +1446 -0
  255. package/src/motion/MEASUREMENTS.md +36 -27
  256. package/src/motion/README.md +7 -3
  257. package/src/motion/animator.ts +193 -25
  258. package/src/motion/paging.ts +161 -0
  259. package/src/motion/uikit/index.ts +7 -4
  260. package/src/motion/uikit/paging.ts +193 -0
  261. package/src/motion/uikit/progressBar.ts +102 -0
  262. package/src/motion/uikit/scroll.ts +2 -18
  263. package/src/motion/uikit/slider.ts +105 -0
  264. package/src/motion/uikit/springs.ts +53 -32
  265. package/src/motion/uikit/switch.ts +174 -0
  266. package/src/privateAttributes/pressHoldsDrag.stylex.ts +28 -0
  267. package/src/utils/pressFeedback.ts +5 -0
  268. package/src/utils/selectionLock.ts +501 -6
  269. package/dist/motion/selectPage.d.ts +0 -26
  270. package/dist/motion/selectPage.d.ts.map +0 -1
  271. package/dist/motion/selectPage.js +0 -55
  272. package/src/motion/selectPage.ts +0 -55
@@ -110,8 +110,9 @@
110
110
  * click; a click held for a recognizer that may still recognize is
111
111
  * replayed if they all fail. Keyboard and assistive-technology clicks
112
112
  * (`detail === 0`) always pass.
113
- * - A `contextmenu` while a recognizer owns a touch is cancelled, so a long
114
- * press never also opens the browser's menu.
113
+ * - A `contextmenu` while a recognizer owns a touch, or while a press
114
+ * consumer holds the menu closed (`holdsContextMenu`), is cancelled. So a
115
+ * long press never also opens the browser's menu.
115
116
  * - When the app loses focus (a window blur, a hidden page), every touch in
116
117
  * flight is cancelled, as in UIKit; a finger the browser keeps alive lifts
117
118
  * on nothing: its `touchend` is cancelled (`spec:AST-094` FR28).
@@ -125,16 +126,17 @@ import { isRtlElement } from "../hooks/isRtlElement.js";
125
126
  import { presentedElements, presentedOffsetOf, resolvePresented, watchPresentedElements } from "../motion/presentation.js";
126
127
  import { UIKIT_PRESS, UIKIT_TAP } from "../motion/uikit/gestures.js";
127
128
  import { browserClock, GestureArena } from "./GestureArena.js";
128
- import { createNativeScrollProbe, parentAcrossShadow } from "./nativeScroll.js";
129
+ import { canElementScroll, createNativeScrollProbe, parentAcrossShadow } from "./nativeScroll.js";
129
130
  import { containsAlongFlatTree } from "../utils/flatTree.js";
130
131
  import { isIosWebKit } from "../utils/touchQueries.js";
132
+ import { WheelSegmenter } from "./wheelSegmenter.js";
131
133
  import { gestureHostStyle, intersectTouchAction } from "./touchAction.js";
132
134
  import { markTouchAction, TOUCH_ACTION_ATTRIBUTE, unmarkTouchAction } from "./touchActionRule.js";
133
135
  import { internals } from "./recognizerInternals.js";
134
136
  import { armClickGuard, disarmClickGuard, withoutClickGuard } from "../utils/clickGuard.js";
135
137
  import { subscribeInput } from "../utils/inputStream.js";
136
138
  import { isPrimaryClick } from "../utils/primaryClick.js";
137
- import { locksSelectionFor } from "../utils/selectionLock.js";
139
+ import { lockDragSelection, locksSelectionFor, selectionRangesOf, startsOnSelectableText } from "../utils/selectionLock.js";
138
140
  import { isCoarsePointer, watchCoarsePointer } from "../utils/coarsePointer.js";
139
141
  import { gestureHosts } from "./hostRegistry.js";
140
142
  const STYLE_PROPERTIES = {
@@ -182,6 +184,16 @@ function claimsWhileMoving(recognizer) {
182
184
  return recognizer.claimsTouches && internals(recognizer).acceptsTouchInput && internals(recognizer).family !== 'tap';
183
185
  }
184
186
 
187
+ /**
188
+ * Whether a recognizer takes a contact that comes with Touch Events and a
189
+ * long press's contextmenu: a finger's or a pen's. A scroll-only recognizer
190
+ * (`allowedPointerTypes: []`) and a mouse-only one take neither.
191
+ */
192
+ function takesTouchContacts(recognizer) {
193
+ const types = recognizer.allowedPointerTypes;
194
+ return types == null || types.has('touch') || types.has('pen');
195
+ }
196
+
185
197
  /**
186
198
  * A pan (or a pan subclass), told apart by its family rather than its class,
187
199
  * so a page that constructs no pan never ships PanGestureRecognizer.
@@ -232,6 +244,138 @@ function landedOn(event, retargeted = event.target) {
232
244
  return event.composedPath()[0] ?? retargeted;
233
245
  }
234
246
 
247
+ /**
248
+ * How long after its last event a pinch or a mouse wheel's notches end, ms.
249
+ * The web reports no wheel phases, so a quiet wheel is a lifted one.
250
+ */
251
+ const WHEEL_END_MS = 100;
252
+ /**
253
+ * How long after its last event a vertical trackpad scroll ends, ms (a
254
+ * sheet's pull), whoever keeps it. A slow drag's stream pauses longer than
255
+ * WHEEL_END_MS between events.
256
+ */
257
+ const WHEEL_FOLLOW_END_V_MS = 180;
258
+ /**
259
+ * The same horizontally, ms (a pager's or a row's swipe), whose slow
260
+ * swipes pause longer still.
261
+ */
262
+ const WHEEL_FOLLOW_END_MS = 220;
263
+ /** A line-mode wheel's line, px, as the platforms scroll it. */
264
+ const WHEEL_LINE_PX = 40;
265
+ /** The span a ctrl-wheel pinch's two contacts start at, px. */
266
+ const WHEEL_PINCH_SPAN = 100;
267
+
268
+ /** A wheel gesture: the contacts it drives in the arena, or none where the page keeps it. */
269
+
270
+ /** A trackpad scroll a pan follows: where its fingers lift, and what waits until then. */
271
+
272
+ /**
273
+ * How long after its last wheel a wheel gesture ends, ms. A trackpad's
274
+ * two-finger scroll waits out a slow drag's pauses for its axis, whoever
275
+ * keeps the stream. A pinch and a mouse wheel's notches end 100 ms after
276
+ * their last wheel.
277
+ */
278
+ function wheelEndMs(kind, scrollType, isHorizontal) {
279
+ if (kind === 'pinch' || scrollType !== 'continuous') {
280
+ return WHEEL_END_MS;
281
+ }
282
+ return isHorizontal ? WHEEL_FOLLOW_END_MS : WHEEL_FOLLOW_END_V_MS;
283
+ }
284
+
285
+ /**
286
+ * A provisional lead `debt` after a move of `moved`. The debt is what a
287
+ * followed pan shows past the fingers' travel, or short of it. Half the move
288
+ * is absorbed, so the pan keeps moving the way the fingers do.
289
+ */
290
+ function absorbedDebt(debt, moved) {
291
+ return Math.sign(debt) * Math.max(0, Math.abs(debt) - Math.abs(moved) / 2);
292
+ }
293
+
294
+ /**
295
+ * `target` where it is ahead of `shown` in the direction of `speed`, else
296
+ * `shown`: a reconcile that never steps back.
297
+ */
298
+ function ahead(shown, target, speed) {
299
+ return speed === 0 || (target - shown) * Math.sign(speed) > 0 ? target : shown;
300
+ }
301
+
302
+ /**
303
+ * A wheel's deltas in CSS px: a line is `WHEEL_LINE_PX`, a page the
304
+ * viewport.
305
+ */
306
+ function wheelPixels(wheel, view) {
307
+ if (wheel.deltaMode === 1) {
308
+ return {
309
+ dx: wheel.deltaX * WHEEL_LINE_PX,
310
+ dy: wheel.deltaY * WHEEL_LINE_PX
311
+ };
312
+ }
313
+ if (wheel.deltaMode === 2) {
314
+ return {
315
+ dx: wheel.deltaX * (view?.innerWidth ?? 0),
316
+ dy: wheel.deltaY * (view?.innerHeight ?? 0)
317
+ };
318
+ }
319
+ return {
320
+ dx: wheel.deltaX,
321
+ dy: wheel.deltaY
322
+ };
323
+ }
324
+
325
+ /**
326
+ * The web's best reading of what scrolled: a mouse wheel's notches
327
+ * (`discrete`) or a trackpad's scroll (`continuous`). Input it cannot tell
328
+ * apart counts as a mouse wheel's, which a pan that takes only a trackpad's
329
+ * scroll leaves to the page. These read as a mouse wheel's:
330
+ * - a line- or page-mode delta;
331
+ * - a scroll with Shift held (Shift+wheel);
332
+ * - a pixel delta a hair off a whole pixel (a mouse wheel on Windows);
333
+ * - a scroll with no legacy `wheelDelta` (an engine that sends none);
334
+ * - a delta that is not a third of its legacy `wheelDelta`;
335
+ * - a 120 tick over a 40 px delta.
336
+ * A trackpad's precise deltas, a third of their legacy `wheelDelta` in
337
+ * Chromium and WebKit, read as a trackpad's.
338
+ */
339
+ function scrollTypeOf(wheel) {
340
+ if (wheel.deltaMode !== 0 || wheel.shiftKey) {
341
+ return 'discrete';
342
+ }
343
+ const remainder = value => {
344
+ const fraction = Math.abs(value - Math.round(value));
345
+ return fraction > 0 && fraction < 0.01;
346
+ };
347
+ if (remainder(wheel.deltaX) || remainder(wheel.deltaY)) {
348
+ return 'discrete';
349
+ }
350
+ const legacy = wheel;
351
+ if (typeof legacy.wheelDeltaY !== 'number' || typeof legacy.wheelDeltaX !== 'number') {
352
+ return 'discrete';
353
+ }
354
+ const precise = (delta, legacyDelta) => delta === 0 ? legacyDelta === 0 : Math.abs(Math.abs(legacyDelta) - 3 * Math.abs(delta)) <= Math.max(1.5, 0.05 * Math.abs(delta) * 3);
355
+ if (!precise(wheel.deltaY, legacy.wheelDeltaY) || !precise(wheel.deltaX, legacy.wheelDeltaX)) {
356
+ return 'discrete';
357
+ }
358
+ const tick = (delta, legacyDelta) => legacyDelta !== 0 && legacyDelta % 120 === 0 && delta % 40 === 0;
359
+ return tick(wheel.deltaY, legacy.wheelDeltaY) || tick(wheel.deltaX, legacy.wheelDeltaX) ? 'discrete' : 'continuous';
360
+ }
361
+
362
+ /** Read without the pan class, which a page with no pan never ships. */
363
+ function isScrollEdgePan(recognizer) {
364
+ return recognizer.scrollEdge === true;
365
+ }
366
+
367
+ /**
368
+ * Whether a pan takes a contact moving by (cx, cy), by its own direction
369
+ * rule, `axisRatio` included. A move its rule leaves undecided stays the
370
+ * page's.
371
+ */
372
+ function panTakesMove(recognizer, cx, cy) {
373
+ return (internals(recognizer).classify?.({
374
+ x: cx,
375
+ y: cy
376
+ }) ?? 'begin') === 'begin';
377
+ }
378
+
235
379
  /**
236
380
  * An event's time on the arena's clock (`performance.now()`). Some engines
237
381
  * and DOM emulators stamp events in epoch milliseconds instead.
@@ -261,6 +405,23 @@ export class GestureHost {
261
405
  cancelCallerRead = null;
262
406
  /** The selection lock each contact holds, by host contact id. */
263
407
  lockedBy = new Map();
408
+ /**
409
+ * The page's selection when each mouse or pen press began: a drag a
410
+ * recognizer claims keeps it.
411
+ */
412
+ pressSelections = new Map();
413
+ /**
414
+ * Mouse and pen contacts that began on selectable text. Once one moves, the
415
+ * browser keeps the drag for a selection, and no recognizer claims it.
416
+ */
417
+ selectingContacts = new Set();
418
+ /**
419
+ * Drag selection holds until their last task ends, a task after their lift
420
+ * (lockDragSelection); the last detach ends them at once.
421
+ */
422
+ dragHolds = new Set();
423
+ /** Each claimed contact's drag hold, until its lift or a give-back. */
424
+ claimHolds = new Map();
264
425
  /**
265
426
  * Contacts sent to what is drawn instead of what the engine hit, by host
266
427
  * contact id, with where they landed (for their click).
@@ -365,6 +526,16 @@ export class GestureHost {
365
526
  */
366
527
  fingerTargets = [];
367
528
  nextId = 1;
529
+ /** The wheel gesture in progress. */
530
+ wheel = null;
531
+ /** The wheel latch's input-stream subscription, while a gesture is latched. */
532
+ stopWheelLatch = null;
533
+ /** Splits the wheel stream into gestures and reads a trackpad's lift. */
534
+ segmenter = new WheelSegmenter();
535
+ /** The latest wheel's segment. */
536
+ wheelSegment = null;
537
+ /** Wheels already taken: each element on the path with a listener hears one. */
538
+ wheelsSeen = new WeakSet();
368
539
  isInstalled = false;
369
540
  /**
370
541
  * Whether the primary pointer is coarse (`spec:AST-094` FR19), read when
@@ -428,7 +599,10 @@ export class GestureHost {
428
599
  // A press whose decision waited on a timer (a tap waiting on a double
429
600
  // tap) ends here: an armed press stops listening (FR24).
430
601
  onIdle: () => this.releaseArmedPress(),
431
- onTransition: options.onTransition
602
+ onTransition: options.onTransition,
603
+ onClaim: (ids, claimer) => this.lockClaimedDrags(ids, claimer),
604
+ onRelease: ids => this.releaseClaimedDrags(ids),
605
+ refusesClaim: touch => this.selectingContacts.has(touch.id) && (touch.x !== touch.startX || touch.y !== touch.startY)
432
606
  });
433
607
  const view = doc.defaultView;
434
608
  // A test may pass its own read; otherwise the one coarse-pointer gate,
@@ -485,6 +659,12 @@ export class GestureHost {
485
659
  if (recognizer.state !== 'possible' || recognizer.isInvolved()) {
486
660
  recognizer.cancel();
487
661
  }
662
+ const wheel = this.wheel;
663
+ if (wheel != null && wheel.takers.includes(recognizer) && !this.wheelHasTaker(wheel)) {
664
+ // At its last wheel: a later stamp would move the arena's clock past
665
+ // the next input's own time.
666
+ this.endWheel(wheel.last);
667
+ }
488
668
  this.refresh(element, registration);
489
669
  // Even when the element keeps other attachments: one left disabled
490
670
  // keeps no listener or observer (FR24).
@@ -847,6 +1027,7 @@ export class GestureHost {
847
1027
  consumer: null,
848
1028
  hasTouchEndListener: false,
849
1029
  hasTouchMoveListener: false,
1030
+ hasWheelListener: false,
850
1031
  savedStyle: new Map(),
851
1032
  authorTouchAction: null,
852
1033
  manageStyle: true,
@@ -911,6 +1092,23 @@ export class GestureHost {
911
1092
  element.removeEventListener('touchmove', this.onBlockingTouchMove);
912
1093
  registration.hasTouchMoveListener = false;
913
1094
  }
1095
+ // Wheel and trackpad input, only where an enabled recognizer takes it. A
1096
+ // non-passive wheel listener makes the browser's scrolling over the
1097
+ // element wait for the main thread.
1098
+ const needsWheel = registration.recognizers.some(recognizer => recognizer.isEnabled &&
1099
+ // A watcher never takes the wheel: the page keeps it.
1100
+ recognizer.claimsTouches && recognizer.allowedScrollTypes.size > 0 && (
1101
+ // A wheel drives a pan or a pinch only.
1102
+ internals(recognizer).family === 'pan' || internals(recognizer).family === 'pinch'));
1103
+ if (needsWheel && !registration.hasWheelListener) {
1104
+ element.addEventListener('wheel', this.onWheel, {
1105
+ passive: false
1106
+ });
1107
+ registration.hasWheelListener = true;
1108
+ } else if (!needsWheel && registration.hasWheelListener) {
1109
+ element.removeEventListener('wheel', this.onWheel);
1110
+ registration.hasWheelListener = false;
1111
+ }
914
1112
  }
915
1113
  applyStyle(element, registration) {
916
1114
  // A disabled recognizer leaves the browser everything (FR24).
@@ -1050,6 +1248,9 @@ export class GestureHost {
1050
1248
  if (registration.hasTouchMoveListener) {
1051
1249
  element.removeEventListener('touchmove', this.onBlockingTouchMove);
1052
1250
  }
1251
+ if (registration.hasWheelListener) {
1252
+ element.removeEventListener('wheel', this.onWheel);
1253
+ }
1053
1254
  this.registry.delete(element);
1054
1255
  this.uninstallIfIdle();
1055
1256
  }
@@ -1138,7 +1339,9 @@ export class GestureHost {
1138
1339
  if (!this.isCoarse) {
1139
1340
  for (const [element, registration] of this.registry) {
1140
1341
  const consumer = registration.consumer;
1141
- if (registration.recognizers.some(recognizer => recognizer.isEnabled) || consumer != null && consumer.isEnabled?.() !== false) {
1342
+ if (registration.recognizers.some(recognizer => recognizer.isEnabled && (
1343
+ // One that takes only scroll input has no press to arm.
1344
+ recognizer.allowedPointerTypes == null || recognizer.allowedPointerTypes.size > 0)) || consumer != null && consumer.isEnabled?.() !== false) {
1142
1345
  wanted.add(element);
1143
1346
  }
1144
1347
  }
@@ -1240,6 +1443,15 @@ export class GestureHost {
1240
1443
  }
1241
1444
  this.ownScrollFrames.clear();
1242
1445
  this.ownedScrolls.clear();
1446
+ // A drag's hold whose press lifted in this task ends now, not a task
1447
+ // later: nothing is attached to meet its mouseup or click.
1448
+ for (const release of [...this.dragHolds]) {
1449
+ release({
1450
+ isCancel: true
1451
+ });
1452
+ }
1453
+ this.pressSelections.clear();
1454
+ this.selectingContacts.clear();
1243
1455
  });
1244
1456
  }
1245
1457
 
@@ -1273,13 +1485,14 @@ export class GestureHost {
1273
1485
 
1274
1486
  /**
1275
1487
  * Whether an enabled participant could claim a touch or cancel a lift: a
1276
- * consumer, or a recognizer that claims touches. With only watchers
1277
- * attached, the host's listeners are all passive (`spec:AST-094` FR20).
1488
+ * consumer, or a recognizer that claims touches and takes a finger or a
1489
+ * pen. With only watchers, or only scroll-only recognizers
1490
+ * (`allowedPointerTypes: []`), the host's listeners are all passive.
1278
1491
  */
1279
1492
  hasClaimer() {
1280
1493
  for (const registration of this.registry.values()) {
1281
1494
  const consumer = registration.consumer;
1282
- if (consumer != null && consumer.isEnabled?.() !== false || registration.recognizers.some(recognizer => recognizer.isEnabled && recognizer.claimsTouches)) {
1495
+ if (consumer != null && consumer.isEnabled?.() !== false || registration.recognizers.some(recognizer => recognizer.isEnabled && recognizer.claimsTouches && takesTouchContacts(recognizer))) {
1283
1496
  return true;
1284
1497
  }
1285
1498
  }
@@ -1498,10 +1711,566 @@ export class GestureHost {
1498
1711
  }
1499
1712
  }
1500
1713
 
1501
- /** Undo `lockSelection` for a contact that ended; idempotent. */
1502
- unlockSelection(id) {
1503
- this.lockedBy.get(id)?.();
1714
+ /**
1715
+ * Wheel and trackpad input. A wheel gesture drives the recognizers that
1716
+ * allow its scroll type, as a contact that moves with the content. A pan
1717
+ * takes it along its own axis, once no scroller between the pointer and
1718
+ * the pan's element can scroll that way. The gesture's first wheel decides
1719
+ * this. A pinch takes a ctrl-wheel, as a trackpad pinches, as two contacts
1720
+ * whose span follows exp(-deltaY / 100). A wheel the page keeps is never
1721
+ * prevented, and a wheel a recognizer takes always is. A gesture ends its
1722
+ * end window after its last wheel (wheelEndMs), at that wheel's time, so
1723
+ * its release keeps its velocity. A gesture a recognizer took is latched
1724
+ * to it, as a browser latches a scroller. While the gesture lasts, the
1725
+ * shared input stream hands this every wheel first, wherever it lands. So
1726
+ * a pan whose element moves from under a still pointer keeps the gesture.
1727
+ * The page listener exists only during a gesture.
1728
+ */
1729
+ onWheel = event => {
1730
+ const wheel = event;
1731
+ if (this.wheelsSeen.has(wheel)) {
1732
+ return;
1733
+ }
1734
+ this.wheelsSeen.add(wheel);
1735
+ const time = eventTime(wheel);
1736
+ const kind = wheel.ctrlKey ? 'pinch' : 'pan';
1737
+ let gesture = this.wheel;
1738
+ if (gesture != null && (gesture.kind !== kind || time - gesture.last >= gesture.endMs)) {
1739
+ this.endWheel(time);
1740
+ gesture = null;
1741
+ }
1742
+ if (gesture != null && gesture.ids.length > 0 && gesture.follow?.lifted !== true && !this.wheelHasTaker(gesture)) {
1743
+ // Every recognizer that took it has left (detached, disabled or
1744
+ // cancelled), so the rest of the gesture is the page's. A followed
1745
+ // pan's own lift does not count. Its momentum stays latched and
1746
+ // prevented until the splitter's next gesture or the end timer, and
1747
+ // moves nothing. The release is stamped at its last wheel.
1748
+ this.endWheel(gesture.last);
1749
+ const left = wheelPixels(wheel, this.doc.defaultView);
1750
+ gesture = {
1751
+ kind,
1752
+ scrollType: scrollTypeOf(wheel),
1753
+ ids: [],
1754
+ x: wheel.clientX,
1755
+ y: wheel.clientY,
1756
+ span: WHEEL_PINCH_SPAN,
1757
+ last: time,
1758
+ endMs: wheelEndMs(kind, scrollTypeOf(wheel), Math.abs(left.dx) > Math.abs(left.dy)),
1759
+ timer: null,
1760
+ takers: []
1761
+ };
1762
+ this.wheel = gesture;
1763
+ }
1764
+ const {
1765
+ dx,
1766
+ dy
1767
+ } = wheelPixels(wheel, this.doc.defaultView);
1768
+ const horizontal = Math.abs(dx) > Math.abs(dy);
1769
+ // The splitter reads a pan's stream only. A ctrl-wheel pinch has no
1770
+ // fingers' phase or momentum to tell apart, and ends 100 ms after its
1771
+ // last wheel.
1772
+ let seg = null;
1773
+ if (kind === 'pan') {
1774
+ seg = this.segmenter.push(time, horizontal ? dx : dy, horizontal ? WHEEL_FOLLOW_END_MS : WHEEL_FOLLOW_END_V_MS);
1775
+ } else {
1776
+ this.segmenter.end();
1777
+ }
1778
+ this.wheelSegment = seg;
1779
+ if (seg?.boundary != null) {
1780
+ if (gesture != null) {
1781
+ this.endWheel(time);
1782
+ gesture = null;
1783
+ }
1784
+ }
1785
+ if (gesture == null) {
1786
+ gesture = this.beginWheel(wheel, kind, dx, dy, time);
1787
+ this.wheel = gesture;
1788
+ if (gesture.ids.length > 0) {
1789
+ this.latchWheel();
1790
+ }
1791
+ }
1792
+ gesture.last = time;
1793
+ if (gesture.timer != null) {
1794
+ this.clock.clearTimeout(gesture.timer);
1795
+ }
1796
+ // A trackpad scroll waits out a slow drag's pauses, whoever
1797
+ // keeps it; a pinch or a mouse wheel's notches 100 ms.
1798
+ gesture.timer = this.clock.setTimeout(() => {
1799
+ this.segmenter.end();
1800
+ this.endWheel();
1801
+ }, gesture.endMs);
1802
+ if (gesture.ids.length === 0) {
1803
+ return;
1804
+ }
1805
+ wheel.preventDefault();
1806
+ if (gesture.kind === 'pan' && gesture.follow) {
1807
+ const f = gesture.follow;
1808
+ if (f.lifted) {
1809
+ return;
1810
+ }
1811
+ if (this.wheelSegment?.liftNow === true) {
1812
+ // Momentum began: the fingers lifted before the held deltas. The pan
1813
+ // keeps the travel it showed through the undecided stretch. It lets
1814
+ // go here, still moving at the fingers' speed, so its release carries
1815
+ // on from it. The rest of the momentum never moves the pan.
1816
+ this.followProvisionally(gesture, f, time);
1817
+ f.lifted = true;
1818
+ this.arena.ended([{
1819
+ id: gesture.ids[0],
1820
+ x: gesture.x,
1821
+ y: gesture.y,
1822
+ time
1823
+ }]);
1824
+ return;
1825
+ }
1826
+ if (this.wheelSegment?.decaying === true) {
1827
+ // Undecided: the real deltas wait for the splitter while the
1828
+ // fingers' last speed carries the pan on.
1829
+ f.heldX += dx;
1830
+ f.heldY += dy;
1831
+ this.followProvisionally(gesture, f, time);
1832
+ return;
1833
+ }
1834
+ // The fingers' own delta; the held ones were theirs too. Their moves
1835
+ // absorb what the pan shows ahead of their travel, or behind it, half a
1836
+ // move at a time. So the pan never steps back, and ends on their travel.
1837
+ const beforeX = f.trueX - f.heldX;
1838
+ const beforeY = f.trueY - f.heldY;
1839
+ const debtX = absorbedDebt(gesture.x - beforeX, dx);
1840
+ const debtY = absorbedDebt(gesture.y - beforeY, dy);
1841
+ const trueX = beforeX - dx;
1842
+ const trueY = beforeY - dy;
1843
+ // A wheel the splitter reads as one stall's merge sets no speed. Its
1844
+ // speed is one the fingers never had.
1845
+ const elapsed = time - f.lastMove;
1846
+ if (elapsed > 0 && this.wheelSegment?.bunched !== true) {
1847
+ f.speedX = (trueX - f.trueX) / elapsed;
1848
+ f.speedY = (trueY - f.trueY) / elapsed;
1849
+ }
1850
+ f.trueX = trueX;
1851
+ f.trueY = trueY;
1852
+ f.heldX = 0;
1853
+ f.heldY = 0;
1854
+ f.lastMove = time;
1855
+ gesture.x = trueX + debtX;
1856
+ gesture.y = trueY + debtY;
1857
+ f.baseX = gesture.x;
1858
+ f.baseY = gesture.y;
1859
+ this.arena.moved([{
1860
+ id: gesture.ids[0],
1861
+ x: gesture.x,
1862
+ y: gesture.y,
1863
+ time
1864
+ }]);
1865
+ return;
1866
+ }
1867
+ if (gesture.kind === 'pan') {
1868
+ gesture.x -= dx;
1869
+ gesture.y -= dy;
1870
+ this.arena.moved([{
1871
+ id: gesture.ids[0],
1872
+ x: gesture.x,
1873
+ y: gesture.y,
1874
+ time
1875
+ }]);
1876
+ } else {
1877
+ gesture.span *= Math.exp(-dy / 100);
1878
+ this.arena.moved(this.pinchContacts(gesture, time));
1879
+ }
1880
+ };
1881
+
1882
+ /**
1883
+ * Move a followed pan through the splitter's undecided stretch. It moves on
1884
+ * from where it was shown at the fingers' last move, at their last speed.
1885
+ * It never passes what that speed projects for the time since.
1886
+ */
1887
+ followProvisionally(gesture, f, time) {
1888
+ const elapsed = Math.max(0, time - f.lastMove);
1889
+ const x = f.baseX + f.speedX * elapsed;
1890
+ const y = f.baseY + f.speedY * elapsed;
1891
+ if (x === gesture.x && y === gesture.y) {
1892
+ return;
1893
+ }
1894
+ gesture.x = x;
1895
+ gesture.y = y;
1896
+ this.arena.moved([{
1897
+ id: gesture.ids[0],
1898
+ x,
1899
+ y,
1900
+ time
1901
+ }]);
1902
+ }
1903
+
1904
+ /** Start a wheel gesture: who takes it, decided once, or nobody. */
1905
+ beginWheel(wheel, kind, dx, dy, time) {
1906
+ const type = scrollTypeOf(wheel);
1907
+ const isHorizontal = Math.abs(dx) > Math.abs(dy);
1908
+ const kept = {
1909
+ kind,
1910
+ scrollType: type,
1911
+ ids: [],
1912
+ x: wheel.clientX,
1913
+ y: wheel.clientY,
1914
+ span: WHEEL_PINCH_SPAN,
1915
+ last: time,
1916
+ endMs: wheelEndMs(kind, type, isHorizontal),
1917
+ timer: null,
1918
+ takers: []
1919
+ };
1920
+ // A contact already down keeps its gesture; the wheel stays the page's.
1921
+ // So does a first wheel another handler already prevented, or one that
1922
+ // cannot be cancelled: no recognizer could take it from the page.
1923
+ if (this.arena.touches.length > 0 || dx === 0 && dy === 0 || wheel.defaultPrevented || !wheel.cancelable) {
1924
+ return kept;
1925
+ }
1926
+ // Every scroller under the pointer, the page included, as a finger's
1927
+ // probe reads them, without touch-action, which a wheel ignores.
1928
+ // What the wheel landed on: at a shadow host's listener the event's
1929
+ // target is the host, and a scroller inside its tree must count.
1930
+ const target = landedOn(wheel);
1931
+ const scroll = createNativeScrollProbe(target, {
1932
+ readsTouchAction: false
1933
+ });
1934
+ const takers = [];
1935
+ for (let node = target instanceof Node ? target : null; node != null; node = parentAcrossShadow(node)) {
1936
+ if (!(node instanceof Element)) {
1937
+ continue;
1938
+ }
1939
+ for (const recognizer of this.registry.get(node)?.recognizers ?? []) {
1940
+ // A pan follows a trackpad, never a mouse wheel's notches, which
1941
+ // scroll what is under the pointer.
1942
+ if (!recognizer.isEnabled || !recognizer.claimsTouches || !recognizer.allowedScrollTypes.has(type) || internals(recognizer).family !== kind || kind === 'pan' && type !== 'continuous') {
1943
+ continue;
1944
+ }
1945
+ if (kind === 'pan' && (!panTakesMove(recognizer, -dx, -dy) || this.scrollerTakes(target, node, dx, dy, isHorizontal) ||
1946
+ // A scroll-edge pan only past every scroller's edge.
1947
+ isScrollEdgePan(recognizer) && scroll.canScroll(-dx, -dy))) {
1948
+ continue;
1949
+ }
1950
+ takers.push(recognizer);
1951
+ }
1952
+ }
1953
+ if (takers.length === 0) {
1954
+ return kept;
1955
+ }
1956
+ const base = {
1957
+ pointerType: 'mouse',
1958
+ target,
1959
+ scroll,
1960
+ scrollType: type,
1961
+ recognizers: takers,
1962
+ consumer: null
1963
+ };
1964
+ const ids = kind === 'pan' ? [this.nextId++] : [this.nextId++, this.nextId++];
1965
+ const gesture = {
1966
+ ...kept,
1967
+ ids,
1968
+ takers
1969
+ };
1970
+ if (kind === 'pan' && type === 'continuous') {
1971
+ gesture.follow = {
1972
+ lifted: false,
1973
+ heldX: 0,
1974
+ heldY: 0,
1975
+ lastMove: time,
1976
+ trueX: kept.x,
1977
+ trueY: kept.y,
1978
+ baseX: kept.x,
1979
+ baseY: kept.y,
1980
+ speedX: 0,
1981
+ speedY: 0
1982
+ };
1983
+ }
1984
+ this.arena.began(kind === 'pan' ? [{
1985
+ ...base,
1986
+ id: ids[0],
1987
+ x: wheel.clientX,
1988
+ y: wheel.clientY,
1989
+ time
1990
+ }] : this.pinchContacts(gesture, time).map(input => ({
1991
+ ...base,
1992
+ ...input
1993
+ })));
1994
+ // Every recognizer it was offered to may decline it (its own
1995
+ // shouldReceiveTouch). Then nobody took the wheel, and it scrolls the
1996
+ // page from its first step, as with no gesture system.
1997
+ if (ids.every(id => this.arena.participantsOf(id).length === 0)) {
1998
+ this.arena.cancelled(ids, time);
1999
+ return kept;
2000
+ }
2001
+ return gesture;
2002
+ }
2003
+
2004
+ /**
2005
+ * Whether a scroller between `target` and `boundary` (both included)
2006
+ * scrolls this wheel now, along the wheel's major axis. It uses the edge
2007
+ * test a finger's probe uses (canElementScroll).
2008
+ */
2009
+ scrollerTakes(target, boundary, dx, dy, isHorizontal) {
2010
+ for (let node = target instanceof Node ? target : null; node != null; node = parentAcrossShadow(node)) {
2011
+ if (node instanceof Element && canElementScroll(node, isHorizontal ? -dx : 0, isHorizontal ? 0 : -dy)) {
2012
+ return true;
2013
+ }
2014
+ if (node === boundary) {
2015
+ break;
2016
+ }
2017
+ }
2018
+ return false;
2019
+ }
2020
+
2021
+ /** A ctrl-wheel pinch's two contacts, `span` apart around its center. */
2022
+ pinchContacts(gesture, time) {
2023
+ return [{
2024
+ id: gesture.ids[0],
2025
+ x: gesture.x - gesture.span / 2,
2026
+ y: gesture.y,
2027
+ time
2028
+ }, {
2029
+ id: gesture.ids[1],
2030
+ x: gesture.x + gesture.span / 2,
2031
+ y: gesture.y,
2032
+ time
2033
+ }];
2034
+ }
2035
+
2036
+ /** Whether a recognizer that took this wheel gesture still takes part in it. */
2037
+ wheelHasTaker(gesture) {
2038
+ // A cancelled or failed recognizer keeps that state until its contacts
2039
+ // end, which a wheel gesture's do only here.
2040
+ return gesture.takers.some(recognizer => {
2041
+ const {
2042
+ state
2043
+ } = recognizer;
2044
+ return recognizer.isEnabled && (state === 'began' || state === 'changed' || state === 'possible' && recognizer.isInvolved());
2045
+ });
2046
+ }
2047
+
2048
+ /**
2049
+ * Route every wheel on the page to the live wheel gesture, from its claim.
2050
+ * The route is the shared input stream's capture listener on the window,
2051
+ * which runs before any document listener. It is non-passive only while it
2052
+ * is subscribed.
2053
+ */
2054
+ latchWheel() {
2055
+ const view = this.doc.defaultView;
2056
+ if (this.stopWheelLatch == null && view != null) {
2057
+ const cancelWheel = () => {
2058
+ const ids = this.dropDrivenWheel();
2059
+ if (ids.length > 0) {
2060
+ this.arena.cancelled(ids, this.arena.now());
2061
+ }
2062
+ };
2063
+ const stops = [subscribeInput(view, 'wheel', this.onWheel, {
2064
+ cancels: true
2065
+ }),
2066
+ // A page that loses focus or hides ends a trackpad pan, as it ends
2067
+ // every touch. An element's blur reaches the window's capture phase
2068
+ // too and means nothing here.
2069
+ subscribeInput(view, 'blur', event => {
2070
+ if (!(event.target instanceof view.Node)) {
2071
+ cancelWheel();
2072
+ }
2073
+ }), subscribeInput(view, 'visibilitychange', () => {
2074
+ if (this.doc.visibilityState === 'hidden') {
2075
+ cancelWheel();
2076
+ }
2077
+ })];
2078
+ this.stopWheelLatch = () => {
2079
+ for (const stop of stops) {
2080
+ stop();
2081
+ }
2082
+ };
2083
+ }
2084
+ }
2085
+
2086
+ /**
2087
+ * Hold the page's selection for a mouse or pen contact that a recognizer
2088
+ * claims. Examples are a pan's drag and a press a group claims at its own
2089
+ * pointerdown. While the contact is down, the claiming recognizers'
2090
+ * elements refuse a new selection. The selection the page had at the press
2091
+ * is put back over anything that replaced it. That includes the press's own
2092
+ * collapse and a selection the drag made before its claim. The claiming
2093
+ * recognizer's element holds from its claim, in any state: a waiting
2094
+ * recognizer claims while possible, and a swipe claims as ended. The
2095
+ * participants that already recognize hold too. A claim given back to the
2096
+ * browser lets its hold go (releaseClaimedDrags). A drag that starts on
2097
+ * selectable text never gets here, because no recognizer claims it
2098
+ * (`selectingContacts`). A tap claims only at its lift, so a click keeps
2099
+ * the selection it made. A finger keeps the touch-down lock alone.
2100
+ */
2101
+ lockClaimedDrags(ids, claimer) {
2102
+ for (const id of ids) {
2103
+ if (this.claimHolds.has(id)) {
2104
+ continue;
2105
+ }
2106
+ const touch = this.arena.touches.find(each => each.id === id);
2107
+ // A claim made at the contact's own lift (a tap's) holds nothing: the
2108
+ // drag is over, and a click keeps the selection it made. A wheel's
2109
+ // contact keeps the page's selection, as a scroll does.
2110
+ if (touch == null || touch.pointerType === 'touch' || touch.scrollType != null || touch.phase === 'ended' || touch.phase === 'cancelled') {
2111
+ continue;
2112
+ }
2113
+ const elements = new Set();
2114
+ for (const recognizer of this.arena.participantsOf(id)) {
2115
+ if ((recognizer.state === 'began' || recognizer.state === 'changed') && recognizer.view?.nodeType === 1) {
2116
+ elements.add(recognizer.view);
2117
+ }
2118
+ }
2119
+ if (claimer.view?.nodeType === 1) {
2120
+ elements.add(claimer.view);
2121
+ }
2122
+ if (elements.size === 0) {
2123
+ continue;
2124
+ }
2125
+ const release = lockDragSelection(elements, this.doc.defaultView, this.pressSelections.get(id) ?? [], () => {
2126
+ this.dragHolds.delete(release);
2127
+ });
2128
+ this.dragHolds.add(release);
2129
+ // A press lock the contact took at its press stays in `lockedBy`. The
2130
+ // hold sits beside it, and the contact's end releases both.
2131
+ if (!this.lockedBy.has(id)) {
2132
+ this.lockedBy.set(id, release);
2133
+ }
2134
+ this.claimHolds.set(id, release);
2135
+ }
2136
+ }
2137
+
2138
+ /**
2139
+ * Claimed contacts the arena gave back. Their drag holds end at once, so
2140
+ * the browser's drag selects again. A contact's press lock (lockSelection)
2141
+ * is not a drag hold and stays.
2142
+ */
2143
+ releaseClaimedDrags(ids) {
2144
+ for (const id of ids) {
2145
+ const release = this.claimHolds.get(id);
2146
+ if (release == null) {
2147
+ continue;
2148
+ }
2149
+ this.claimHolds.delete(id);
2150
+ if (this.lockedBy.get(id) === release) {
2151
+ this.lockedBy.delete(id);
2152
+ }
2153
+ release({
2154
+ isCancel: true
2155
+ });
2156
+ }
2157
+ }
2158
+
2159
+ /**
2160
+ * End the trackpad pan a recognizer still drives, when the page loses focus
2161
+ * or hides, and return its contacts to cancel. The rest of its stream is
2162
+ * the page's until the stream ends, as a touch the browser takes is. A
2163
+ * ctrl-wheel pinch keeps its stream, so the page never zooms for it. A
2164
+ * gesture the page keeps stays as it is. So does a pan its lift released:
2165
+ * its momentum stays latched and prevented until the stream ends.
2166
+ */
2167
+ dropDrivenWheel() {
2168
+ const gesture = this.wheel;
2169
+ if (gesture == null || gesture.kind !== 'pan' || gesture.ids.length === 0 || gesture.follow?.lifted === true) {
2170
+ return [];
2171
+ }
2172
+ const kept = {
2173
+ ...gesture,
2174
+ ids: [],
2175
+ takers: []
2176
+ };
2177
+ delete kept.follow;
2178
+ this.wheel = kept;
2179
+ this.stopWheelLatch?.();
2180
+ this.stopWheelLatch = null;
2181
+ return gesture.ids;
2182
+ }
2183
+
2184
+ /**
2185
+ * End the wheel gesture in progress. On its own timer, it ends its end
2186
+ * window after its last wheel. A taker that leaves (a detach, a disable)
2187
+ * passes that last wheel's time. An input that ends it sooner (a press,
2188
+ * the next gesture's first wheel) passes its own time. Both times are on
2189
+ * the wheels' time base (eventTime), never the host's clock, which need
2190
+ * not share it.
2191
+ */
2192
+ endWheel(endTime) {
2193
+ const gesture = this.wheel;
2194
+ this.stopWheelLatch?.();
2195
+ this.stopWheelLatch = null;
2196
+ if (gesture == null) {
2197
+ return;
2198
+ }
2199
+ this.wheel = null;
2200
+ if (gesture.timer != null) {
2201
+ this.clock.clearTimeout(gesture.timer);
2202
+ }
2203
+ if (gesture.ids.length === 0 || gesture.follow?.lifted) {
2204
+ return;
2205
+ }
2206
+ // Fingers that stop without a lift end on their travel, where it runs
2207
+ // ahead of what the pan shows. What the undecided stretch showed past it
2208
+ // stays, as a step back would show.
2209
+ if (gesture.follow != null) {
2210
+ const f = gesture.follow;
2211
+ const x = ahead(gesture.x, f.trueX - f.heldX, f.speedX);
2212
+ const y = ahead(gesture.y, f.trueY - f.heldY, f.speedY);
2213
+ if (x !== gesture.x || y !== gesture.y) {
2214
+ gesture.x = x;
2215
+ gesture.y = y;
2216
+ this.arena.moved([{
2217
+ id: gesture.ids[0],
2218
+ x,
2219
+ y,
2220
+ time: gesture.last
2221
+ }]);
2222
+ }
2223
+ }
2224
+ // A trackpad scroll whose fingers stop releases still, past the
2225
+ // velocity's stillness interval. A pinch releases 100 ms after its last
2226
+ // wheel, and a lift released the pan already. The release is never later
2227
+ // than the input that ends it (a press, the next gesture's first wheel).
2228
+ // The arena's clock only moves forward, so a later stamp would move that
2229
+ // input's times too.
2230
+ const end = gesture.last + gesture.endMs;
2231
+ const time = endTime == null ? end : Math.max(gesture.last, Math.min(end, endTime));
2232
+ this.arena.ended(gesture.kind === 'pan' ? [{
2233
+ id: gesture.ids[0],
2234
+ x: gesture.x,
2235
+ y: gesture.y,
2236
+ time
2237
+ }] : this.pinchContacts(gesture, time));
2238
+ }
2239
+
2240
+ /**
2241
+ * At a mouse or pen press, mark one that lands on selectable text: once it
2242
+ * moves, it is the browser's drag, and selects text.
2243
+ * Text whose hold does something is locked at this press already
2244
+ * (lockSelection), so it is not selectable text.
2245
+ */
2246
+ noteSelectingPress(id, x, y, target) {
2247
+ if (!this.lockedBy.has(id) && startsOnSelectableText(this.doc, x, y, target)) {
2248
+ this.selectingContacts.add(id);
2249
+ }
2250
+ }
2251
+
2252
+ /**
2253
+ * Undo a contact's selection locks, its press lock and its drag hold, at
2254
+ * once for a cancel, and forget its press's selection; idempotent. A
2255
+ * drag's hold keeps its last task after a lift (lockDragSelection).
2256
+ */
2257
+ unlockSelection(id, isCancel = false) {
2258
+ const lock = this.lockedBy.get(id);
2259
+ lock?.({
2260
+ isCancel
2261
+ });
2262
+ // A press lock keeps `lockedBy` when the contact also has a drag hold.
2263
+ // Release the hold too.
2264
+ const hold = this.claimHolds.get(id);
2265
+ if (hold != null && hold !== lock) {
2266
+ hold({
2267
+ isCancel
2268
+ });
2269
+ }
1504
2270
  this.lockedBy.delete(id);
2271
+ this.claimHolds.delete(id);
2272
+ this.pressSelections.delete(id);
2273
+ this.selectingContacts.delete(id);
1505
2274
  }
1506
2275
 
1507
2276
  // --- Touch events ----------------------------------------------------------
@@ -1539,6 +2308,7 @@ export class GestureHost {
1539
2308
  return true;
1540
2309
  }
1541
2310
  onTouchStart = event => {
2311
+ this.endWheel(eventTime(event));
1542
2312
  this.noteFingersDown(event);
1543
2313
  // Where the host reads a finger from its pointer events (no ontouchstart,
1544
2314
  // as after touch emulation turns on), its Touch Events would track it twice.
@@ -2157,6 +2927,7 @@ export class GestureHost {
2157
2927
  // --- Pointer events --------------------------------------------------------
2158
2928
 
2159
2929
  onPointerDown = event => {
2930
+ this.endWheel(eventTime(event));
2160
2931
  // A new press: whatever click is still owed to the last one is not this.
2161
2932
  disarmClickGuard(this.doc);
2162
2933
  if (!isPointerKind(event.pointerType)) {
@@ -2194,6 +2965,11 @@ export class GestureHost {
2194
2965
  target
2195
2966
  });
2196
2967
  }
2968
+ // Before the arena admits it: a press a group claims at its own
2969
+ // pointerdown keeps the selection the page has now.
2970
+ if (event.pointerType !== 'touch') {
2971
+ this.pressSelections.set(id, selectionRangesOf(this.doc.defaultView));
2972
+ }
2197
2973
  this.arena.began([{
2198
2974
  id,
2199
2975
  x: event.clientX,
@@ -2210,6 +2986,10 @@ export class GestureHost {
2210
2986
  // hit test may still see the motion's start.
2211
2987
  this.lockSelection(id, this.arena.participantsOf(id), target, this.arena.consumerOf(id) ?? null, consumerElement);
2212
2988
  }
2989
+ if (event.pointerType !== 'touch') {
2990
+ // The engine's own hit: the text its caret query and a drag select.
2991
+ this.noteSelectingPress(id, event.clientX, event.clientY, landed);
2992
+ }
2213
2993
  };
2214
2994
  onPointerMove = event => {
2215
2995
  if (event.pointerType === 'touch' && this.usesTouchEvents) {
@@ -2345,7 +3125,7 @@ export class GestureHost {
2345
3125
  const id = this.pointerIds.get(event.pointerId);
2346
3126
  if (id != null) {
2347
3127
  this.pointerIds.delete(event.pointerId);
2348
- this.unlockSelection(id);
3128
+ this.unlockSelection(id, true);
2349
3129
  this.retargeted.delete(id);
2350
3130
  this.arena.cancelled([id], eventTime(event));
2351
3131
  }
@@ -2401,7 +3181,7 @@ export class GestureHost {
2401
3181
  }
2402
3182
  onContextMenu = event => {
2403
3183
  for (const touch of this.arena.touches) {
2404
- if (this.arena.isClaimed(touch.id)) {
3184
+ if (this.arena.isClaimed(touch.id) || this.arena.consumerOf(touch.id)?.holdsContextMenu?.(touch.id) === true) {
2405
3185
  event.preventDefault();
2406
3186
  return;
2407
3187
  }
@@ -2549,7 +3329,7 @@ export class GestureHost {
2549
3329
  cancelEverything(time) {
2550
3330
  const ids = [...this.pointerIds.values()];
2551
3331
  for (const id of ids) {
2552
- this.unlockSelection(id);
3332
+ this.unlockSelection(id, true);
2553
3333
  }
2554
3334
  for (const identifier of [...this.touchIds.keys()]) {
2555
3335
  // The browser may keep the touch alive: its lift must not click.
@@ -2566,6 +3346,8 @@ export class GestureHost {
2566
3346
  this.unpairedTouches.clear();
2567
3347
  this.lastFed.clear();
2568
3348
  this.pointerFed.clear();
3349
+ // A trackpad pan a recognizer still drives is cancelled with them.
3350
+ ids.push(...this.dropDrivenWheel());
2569
3351
  if (ids.length > 0) {
2570
3352
  this.arena.cancelled(ids, time);
2571
3353
  }