@astryxdesign/core 0.7.6 → 0.7.7-canary.36f8ea1

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 (101) hide show
  1. package/dist/AppShell/AppShell.d.ts.map +1 -1
  2. package/dist/AppShell/AppShell.js +23 -3
  3. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.js +16 -9
  5. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  6. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  7. package/dist/Chat/ChatComposerInput.js +2 -0
  8. package/dist/Chat/ChatMessage.js +2 -2
  9. package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
  10. package/dist/Chat/chatMessage.vars.stylex.js +8 -8
  11. package/dist/Drawer/Drawer.d.ts.map +1 -1
  12. package/dist/Drawer/Drawer.js +6 -0
  13. package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
  14. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
  15. package/dist/DropdownMenu/useMenuOverflow.js +31 -8
  16. package/dist/Link/Link.d.ts.map +1 -1
  17. package/dist/Link/Link.js +1 -0
  18. package/dist/Selector/Selector.d.ts.map +1 -1
  19. package/dist/Selector/Selector.js +25 -13
  20. package/dist/Slider/Slider.d.ts.map +1 -1
  21. package/dist/Slider/Slider.js +404 -131
  22. package/dist/Slider/sliderTouchController.d.ts +189 -0
  23. package/dist/Slider/sliderTouchController.d.ts.map +1 -0
  24. package/dist/Slider/sliderTouchController.js +1017 -0
  25. package/dist/Slider/useSliderTouch.d.ts +22 -0
  26. package/dist/Slider/useSliderTouch.d.ts.map +1 -0
  27. package/dist/Slider/useSliderTouch.js +56 -0
  28. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  29. package/dist/Typeahead/BaseTypeahead.js +10 -8
  30. package/dist/astryx.css +33 -6
  31. package/dist/gestures/GestureArena.d.ts.map +1 -1
  32. package/dist/gestures/GestureArena.js +4 -1
  33. package/dist/gestures/GestureHost.d.ts +72 -2
  34. package/dist/gestures/GestureHost.d.ts.map +1 -1
  35. package/dist/gestures/GestureHost.js +643 -5
  36. package/dist/gestures/GestureRecognizer.d.ts +15 -1
  37. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  38. package/dist/gestures/GestureRecognizer.js +12 -0
  39. package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
  40. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  41. package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
  42. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
  43. package/dist/gestures/PinchGestureRecognizer.js +10 -0
  44. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
  45. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
  46. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
  47. package/dist/gestures/nativeScroll.d.ts +16 -2
  48. package/dist/gestures/nativeScroll.d.ts.map +1 -1
  49. package/dist/gestures/nativeScroll.js +39 -8
  50. package/dist/gestures/recognizerInternals.d.ts +4 -2
  51. package/dist/gestures/recognizerInternals.d.ts.map +1 -1
  52. package/dist/gestures/types.d.ts +11 -0
  53. package/dist/gestures/types.d.ts.map +1 -1
  54. package/dist/gestures/types.js +7 -0
  55. package/dist/gestures/wheelSegmenter.d.ts +45 -0
  56. package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
  57. package/dist/gestures/wheelSegmenter.js +98 -0
  58. package/dist/motion/animator.d.ts.map +1 -1
  59. package/dist/motion/animator.js +77 -14
  60. package/dist/motion/uikit/index.d.ts +2 -1
  61. package/dist/motion/uikit/index.d.ts.map +1 -1
  62. package/dist/motion/uikit/index.js +2 -1
  63. package/dist/motion/uikit/slider.d.ts +97 -0
  64. package/dist/motion/uikit/slider.d.ts.map +1 -0
  65. package/dist/motion/uikit/slider.js +111 -0
  66. package/dist/motion/uikit/springs.d.ts +46 -1
  67. package/dist/motion/uikit/springs.d.ts.map +1 -1
  68. package/dist/motion/uikit/springs.js +48 -4
  69. package/package.json +5 -5
  70. package/src/AppShell/AppShell.doc.mjs +10 -0
  71. package/src/AppShell/AppShell.tsx +48 -2
  72. package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
  73. package/src/Chat/Chat.doc.mjs +1 -0
  74. package/src/Chat/ChatComposerInput.doc.mjs +11 -0
  75. package/src/Chat/ChatComposerInput.tsx +11 -0
  76. package/src/Chat/ChatMessage.tsx +2 -2
  77. package/src/Chat/chatMessage.vars.stylex.ts +8 -8
  78. package/src/Drawer/Drawer.tsx +6 -0
  79. package/src/DropdownMenu/useMenuOverflow.ts +44 -8
  80. package/src/Link/Link.tsx +3 -0
  81. package/src/Selector/Selector.tsx +29 -2
  82. package/src/Slider/Slider.doc.mjs +11 -7
  83. package/src/Slider/Slider.tsx +539 -132
  84. package/src/Slider/sliderTouchController.ts +1209 -0
  85. package/src/Slider/useSliderTouch.ts +78 -0
  86. package/src/Typeahead/BaseTypeahead.tsx +9 -8
  87. package/src/gestures/GestureArena.ts +1 -0
  88. package/src/gestures/GestureHost.ts +765 -5
  89. package/src/gestures/GestureRecognizer.ts +28 -0
  90. package/src/gestures/PanGestureRecognizer.ts +8 -0
  91. package/src/gestures/PinchGestureRecognizer.ts +16 -1
  92. package/src/gestures/README.md +63 -0
  93. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
  94. package/src/gestures/nativeScroll.ts +49 -9
  95. package/src/gestures/recognizerInternals.ts +9 -2
  96. package/src/gestures/types.ts +12 -0
  97. package/src/gestures/wheelSegmenter.ts +114 -0
  98. package/src/motion/animator.ts +115 -19
  99. package/src/motion/uikit/index.ts +2 -1
  100. package/src/motion/uikit/slider.ts +105 -0
  101. package/src/motion/uikit/springs.ts +37 -2
@@ -125,9 +125,10 @@ import { isRtlElement } from "../hooks/isRtlElement.js";
125
125
  import { presentedElements, presentedOffsetOf, resolvePresented, watchPresentedElements } from "../motion/presentation.js";
126
126
  import { UIKIT_PRESS, UIKIT_TAP } from "../motion/uikit/gestures.js";
127
127
  import { browserClock, GestureArena } from "./GestureArena.js";
128
- import { createNativeScrollProbe, parentAcrossShadow } from "./nativeScroll.js";
128
+ import { canElementScroll, createNativeScrollProbe, parentAcrossShadow } from "./nativeScroll.js";
129
129
  import { containsAlongFlatTree } from "../utils/flatTree.js";
130
130
  import { isIosWebKit } from "../utils/touchQueries.js";
131
+ import { WheelSegmenter } from "./wheelSegmenter.js";
131
132
  import { gestureHostStyle, intersectTouchAction } from "./touchAction.js";
132
133
  import { markTouchAction, TOUCH_ACTION_ATTRIBUTE, unmarkTouchAction } from "./touchActionRule.js";
133
134
  import { internals } from "./recognizerInternals.js";
@@ -182,6 +183,16 @@ function claimsWhileMoving(recognizer) {
182
183
  return recognizer.claimsTouches && internals(recognizer).acceptsTouchInput && internals(recognizer).family !== 'tap';
183
184
  }
184
185
 
186
+ /**
187
+ * Whether a recognizer takes a contact that comes with Touch Events and a
188
+ * long press's contextmenu: a finger's or a pen's. A scroll-only recognizer
189
+ * (`allowedPointerTypes: []`) and a mouse-only one take neither.
190
+ */
191
+ function takesTouchContacts(recognizer) {
192
+ const types = recognizer.allowedPointerTypes;
193
+ return types == null || types.has('touch') || types.has('pen');
194
+ }
195
+
185
196
  /**
186
197
  * A pan (or a pan subclass), told apart by its family rather than its class,
187
198
  * so a page that constructs no pan never ships PanGestureRecognizer.
@@ -232,6 +243,138 @@ function landedOn(event, retargeted = event.target) {
232
243
  return event.composedPath()[0] ?? retargeted;
233
244
  }
234
245
 
246
+ /**
247
+ * How long after its last event a pinch or a mouse wheel's notches end, ms.
248
+ * The web reports no wheel phases, so a quiet wheel is a lifted one.
249
+ */
250
+ const WHEEL_END_MS = 100;
251
+ /**
252
+ * How long after its last event a vertical trackpad scroll ends, ms (a
253
+ * sheet's pull), whoever keeps it. A slow drag's stream pauses longer than
254
+ * WHEEL_END_MS between events.
255
+ */
256
+ const WHEEL_FOLLOW_END_V_MS = 180;
257
+ /**
258
+ * The same horizontally, ms (a pager's or a row's swipe), whose slow
259
+ * swipes pause longer still.
260
+ */
261
+ const WHEEL_FOLLOW_END_MS = 220;
262
+ /** A line-mode wheel's line, px, as the platforms scroll it. */
263
+ const WHEEL_LINE_PX = 40;
264
+ /** The span a ctrl-wheel pinch's two contacts start at, px. */
265
+ const WHEEL_PINCH_SPAN = 100;
266
+
267
+ /** A wheel gesture: the contacts it drives in the arena, or none where the page keeps it. */
268
+
269
+ /** A trackpad scroll a pan follows: where its fingers lift, and what waits until then. */
270
+
271
+ /**
272
+ * How long after its last wheel a wheel gesture ends, ms. A trackpad's
273
+ * two-finger scroll waits out a slow drag's pauses for its axis, whoever
274
+ * keeps the stream. A pinch and a mouse wheel's notches end 100 ms after
275
+ * their last wheel.
276
+ */
277
+ function wheelEndMs(kind, scrollType, isHorizontal) {
278
+ if (kind === 'pinch' || scrollType !== 'continuous') {
279
+ return WHEEL_END_MS;
280
+ }
281
+ return isHorizontal ? WHEEL_FOLLOW_END_MS : WHEEL_FOLLOW_END_V_MS;
282
+ }
283
+
284
+ /**
285
+ * A provisional lead `debt` after a move of `moved`. The debt is what a
286
+ * followed pan shows past the fingers' travel, or short of it. Half the move
287
+ * is absorbed, so the pan keeps moving the way the fingers do.
288
+ */
289
+ function absorbedDebt(debt, moved) {
290
+ return Math.sign(debt) * Math.max(0, Math.abs(debt) - Math.abs(moved) / 2);
291
+ }
292
+
293
+ /**
294
+ * `target` where it is ahead of `shown` in the direction of `speed`, else
295
+ * `shown`: a reconcile that never steps back.
296
+ */
297
+ function ahead(shown, target, speed) {
298
+ return speed === 0 || (target - shown) * Math.sign(speed) > 0 ? target : shown;
299
+ }
300
+
301
+ /**
302
+ * A wheel's deltas in CSS px: a line is `WHEEL_LINE_PX`, a page the
303
+ * viewport.
304
+ */
305
+ function wheelPixels(wheel, view) {
306
+ if (wheel.deltaMode === 1) {
307
+ return {
308
+ dx: wheel.deltaX * WHEEL_LINE_PX,
309
+ dy: wheel.deltaY * WHEEL_LINE_PX
310
+ };
311
+ }
312
+ if (wheel.deltaMode === 2) {
313
+ return {
314
+ dx: wheel.deltaX * (view?.innerWidth ?? 0),
315
+ dy: wheel.deltaY * (view?.innerHeight ?? 0)
316
+ };
317
+ }
318
+ return {
319
+ dx: wheel.deltaX,
320
+ dy: wheel.deltaY
321
+ };
322
+ }
323
+
324
+ /**
325
+ * The web's best reading of what scrolled: a mouse wheel's notches
326
+ * (`discrete`) or a trackpad's scroll (`continuous`). Input it cannot tell
327
+ * apart counts as a mouse wheel's, which a pan that takes only a trackpad's
328
+ * scroll leaves to the page. These read as a mouse wheel's:
329
+ * - a line- or page-mode delta;
330
+ * - a scroll with Shift held (Shift+wheel);
331
+ * - a pixel delta a hair off a whole pixel (a mouse wheel on Windows);
332
+ * - a scroll with no legacy `wheelDelta` (an engine that sends none);
333
+ * - a delta that is not a third of its legacy `wheelDelta`;
334
+ * - a 120 tick over a 40 px delta.
335
+ * A trackpad's precise deltas, a third of their legacy `wheelDelta` in
336
+ * Chromium and WebKit, read as a trackpad's.
337
+ */
338
+ function scrollTypeOf(wheel) {
339
+ if (wheel.deltaMode !== 0 || wheel.shiftKey) {
340
+ return 'discrete';
341
+ }
342
+ const remainder = value => {
343
+ const fraction = Math.abs(value - Math.round(value));
344
+ return fraction > 0 && fraction < 0.01;
345
+ };
346
+ if (remainder(wheel.deltaX) || remainder(wheel.deltaY)) {
347
+ return 'discrete';
348
+ }
349
+ const legacy = wheel;
350
+ if (typeof legacy.wheelDeltaY !== 'number' || typeof legacy.wheelDeltaX !== 'number') {
351
+ return 'discrete';
352
+ }
353
+ 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);
354
+ if (!precise(wheel.deltaY, legacy.wheelDeltaY) || !precise(wheel.deltaX, legacy.wheelDeltaX)) {
355
+ return 'discrete';
356
+ }
357
+ const tick = (delta, legacyDelta) => legacyDelta !== 0 && legacyDelta % 120 === 0 && delta % 40 === 0;
358
+ return tick(wheel.deltaY, legacy.wheelDeltaY) || tick(wheel.deltaX, legacy.wheelDeltaX) ? 'discrete' : 'continuous';
359
+ }
360
+
361
+ /** Read without the pan class, which a page with no pan never ships. */
362
+ function isScrollEdgePan(recognizer) {
363
+ return recognizer.scrollEdge === true;
364
+ }
365
+
366
+ /**
367
+ * Whether a pan takes a contact moving by (cx, cy), by its own direction
368
+ * rule, `axisRatio` included. A move its rule leaves undecided stays the
369
+ * page's.
370
+ */
371
+ function panTakesMove(recognizer, cx, cy) {
372
+ return (internals(recognizer).classify?.({
373
+ x: cx,
374
+ y: cy
375
+ }) ?? 'begin') === 'begin';
376
+ }
377
+
235
378
  /**
236
379
  * An event's time on the arena's clock (`performance.now()`). Some engines
237
380
  * and DOM emulators stamp events in epoch milliseconds instead.
@@ -365,6 +508,16 @@ export class GestureHost {
365
508
  */
366
509
  fingerTargets = [];
367
510
  nextId = 1;
511
+ /** The wheel gesture in progress. */
512
+ wheel = null;
513
+ /** The wheel latch's input-stream subscription, while a gesture is latched. */
514
+ stopWheelLatch = null;
515
+ /** Splits the wheel stream into gestures and reads a trackpad's lift. */
516
+ segmenter = new WheelSegmenter();
517
+ /** The latest wheel's segment. */
518
+ wheelSegment = null;
519
+ /** Wheels already taken: each element on the path with a listener hears one. */
520
+ wheelsSeen = new WeakSet();
368
521
  isInstalled = false;
369
522
  /**
370
523
  * Whether the primary pointer is coarse (`spec:AST-094` FR19), read when
@@ -485,6 +638,12 @@ export class GestureHost {
485
638
  if (recognizer.state !== 'possible' || recognizer.isInvolved()) {
486
639
  recognizer.cancel();
487
640
  }
641
+ const wheel = this.wheel;
642
+ if (wheel != null && wheel.takers.includes(recognizer) && !this.wheelHasTaker(wheel)) {
643
+ // At its last wheel: a later stamp would move the arena's clock past
644
+ // the next input's own time.
645
+ this.endWheel(wheel.last);
646
+ }
488
647
  this.refresh(element, registration);
489
648
  // Even when the element keeps other attachments: one left disabled
490
649
  // keeps no listener or observer (FR24).
@@ -847,6 +1006,7 @@ export class GestureHost {
847
1006
  consumer: null,
848
1007
  hasTouchEndListener: false,
849
1008
  hasTouchMoveListener: false,
1009
+ hasWheelListener: false,
850
1010
  savedStyle: new Map(),
851
1011
  authorTouchAction: null,
852
1012
  manageStyle: true,
@@ -911,6 +1071,23 @@ export class GestureHost {
911
1071
  element.removeEventListener('touchmove', this.onBlockingTouchMove);
912
1072
  registration.hasTouchMoveListener = false;
913
1073
  }
1074
+ // Wheel and trackpad input, only where an enabled recognizer takes it. A
1075
+ // non-passive wheel listener makes the browser's scrolling over the
1076
+ // element wait for the main thread.
1077
+ const needsWheel = registration.recognizers.some(recognizer => recognizer.isEnabled &&
1078
+ // A watcher never takes the wheel: the page keeps it.
1079
+ recognizer.claimsTouches && recognizer.allowedScrollTypes.size > 0 && (
1080
+ // A wheel drives a pan or a pinch only.
1081
+ internals(recognizer).family === 'pan' || internals(recognizer).family === 'pinch'));
1082
+ if (needsWheel && !registration.hasWheelListener) {
1083
+ element.addEventListener('wheel', this.onWheel, {
1084
+ passive: false
1085
+ });
1086
+ registration.hasWheelListener = true;
1087
+ } else if (!needsWheel && registration.hasWheelListener) {
1088
+ element.removeEventListener('wheel', this.onWheel);
1089
+ registration.hasWheelListener = false;
1090
+ }
914
1091
  }
915
1092
  applyStyle(element, registration) {
916
1093
  // A disabled recognizer leaves the browser everything (FR24).
@@ -1050,6 +1227,9 @@ export class GestureHost {
1050
1227
  if (registration.hasTouchMoveListener) {
1051
1228
  element.removeEventListener('touchmove', this.onBlockingTouchMove);
1052
1229
  }
1230
+ if (registration.hasWheelListener) {
1231
+ element.removeEventListener('wheel', this.onWheel);
1232
+ }
1053
1233
  this.registry.delete(element);
1054
1234
  this.uninstallIfIdle();
1055
1235
  }
@@ -1138,7 +1318,9 @@ export class GestureHost {
1138
1318
  if (!this.isCoarse) {
1139
1319
  for (const [element, registration] of this.registry) {
1140
1320
  const consumer = registration.consumer;
1141
- if (registration.recognizers.some(recognizer => recognizer.isEnabled) || consumer != null && consumer.isEnabled?.() !== false) {
1321
+ if (registration.recognizers.some(recognizer => recognizer.isEnabled && (
1322
+ // One that takes only scroll input has no press to arm.
1323
+ recognizer.allowedPointerTypes == null || recognizer.allowedPointerTypes.size > 0)) || consumer != null && consumer.isEnabled?.() !== false) {
1142
1324
  wanted.add(element);
1143
1325
  }
1144
1326
  }
@@ -1273,13 +1455,14 @@ export class GestureHost {
1273
1455
 
1274
1456
  /**
1275
1457
  * 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).
1458
+ * consumer, or a recognizer that claims touches and takes a finger or a
1459
+ * pen. With only watchers, or only scroll-only recognizers
1460
+ * (`allowedPointerTypes: []`), the host's listeners are all passive.
1278
1461
  */
1279
1462
  hasClaimer() {
1280
1463
  for (const registration of this.registry.values()) {
1281
1464
  const consumer = registration.consumer;
1282
- if (consumer != null && consumer.isEnabled?.() !== false || registration.recognizers.some(recognizer => recognizer.isEnabled && recognizer.claimsTouches)) {
1465
+ if (consumer != null && consumer.isEnabled?.() !== false || registration.recognizers.some(recognizer => recognizer.isEnabled && recognizer.claimsTouches && takesTouchContacts(recognizer))) {
1283
1466
  return true;
1284
1467
  }
1285
1468
  }
@@ -1498,6 +1681,457 @@ export class GestureHost {
1498
1681
  }
1499
1682
  }
1500
1683
 
1684
+ /**
1685
+ * Wheel and trackpad input. A wheel gesture drives the recognizers that
1686
+ * allow its scroll type, as a contact that moves with the content. A pan
1687
+ * takes it along its own axis, once no scroller between the pointer and
1688
+ * the pan's element can scroll that way. The gesture's first wheel decides
1689
+ * this. A pinch takes a ctrl-wheel, as a trackpad pinches, as two contacts
1690
+ * whose span follows exp(-deltaY / 100). A wheel the page keeps is never
1691
+ * prevented, and a wheel a recognizer takes always is. A gesture ends its
1692
+ * end window after its last wheel (wheelEndMs), at that wheel's time, so
1693
+ * its release keeps its velocity. A gesture a recognizer took is latched
1694
+ * to it, as a browser latches a scroller. While the gesture lasts, the
1695
+ * shared input stream hands this every wheel first, wherever it lands. So
1696
+ * a pan whose element moves from under a still pointer keeps the gesture.
1697
+ * The page listener exists only during a gesture.
1698
+ */
1699
+ onWheel = event => {
1700
+ const wheel = event;
1701
+ if (this.wheelsSeen.has(wheel)) {
1702
+ return;
1703
+ }
1704
+ this.wheelsSeen.add(wheel);
1705
+ const time = eventTime(wheel);
1706
+ const kind = wheel.ctrlKey ? 'pinch' : 'pan';
1707
+ let gesture = this.wheel;
1708
+ if (gesture != null && (gesture.kind !== kind || time - gesture.last >= gesture.endMs)) {
1709
+ this.endWheel(time);
1710
+ gesture = null;
1711
+ }
1712
+ if (gesture != null && gesture.ids.length > 0 && gesture.follow?.lifted !== true && !this.wheelHasTaker(gesture)) {
1713
+ // Every recognizer that took it has left (detached, disabled or
1714
+ // cancelled), so the rest of the gesture is the page's. A followed
1715
+ // pan's own lift does not count. Its momentum stays latched and
1716
+ // prevented until the splitter's next gesture or the end timer, and
1717
+ // moves nothing. The release is stamped at its last wheel.
1718
+ this.endWheel(gesture.last);
1719
+ const left = wheelPixels(wheel, this.doc.defaultView);
1720
+ gesture = {
1721
+ kind,
1722
+ scrollType: scrollTypeOf(wheel),
1723
+ ids: [],
1724
+ x: wheel.clientX,
1725
+ y: wheel.clientY,
1726
+ span: WHEEL_PINCH_SPAN,
1727
+ last: time,
1728
+ endMs: wheelEndMs(kind, scrollTypeOf(wheel), Math.abs(left.dx) > Math.abs(left.dy)),
1729
+ timer: null,
1730
+ takers: []
1731
+ };
1732
+ this.wheel = gesture;
1733
+ }
1734
+ const {
1735
+ dx,
1736
+ dy
1737
+ } = wheelPixels(wheel, this.doc.defaultView);
1738
+ const horizontal = Math.abs(dx) > Math.abs(dy);
1739
+ // The splitter reads a pan's stream only. A ctrl-wheel pinch has no
1740
+ // fingers' phase or momentum to tell apart, and ends 100 ms after its
1741
+ // last wheel.
1742
+ let seg = null;
1743
+ if (kind === 'pan') {
1744
+ seg = this.segmenter.push(time, horizontal ? dx : dy, horizontal ? WHEEL_FOLLOW_END_MS : WHEEL_FOLLOW_END_V_MS);
1745
+ } else {
1746
+ this.segmenter.end();
1747
+ }
1748
+ this.wheelSegment = seg;
1749
+ if (seg?.boundary != null) {
1750
+ if (gesture != null) {
1751
+ this.endWheel(time);
1752
+ gesture = null;
1753
+ }
1754
+ }
1755
+ if (gesture == null) {
1756
+ gesture = this.beginWheel(wheel, kind, dx, dy, time);
1757
+ this.wheel = gesture;
1758
+ if (gesture.ids.length > 0) {
1759
+ this.latchWheel();
1760
+ }
1761
+ }
1762
+ gesture.last = time;
1763
+ if (gesture.timer != null) {
1764
+ this.clock.clearTimeout(gesture.timer);
1765
+ }
1766
+ // A trackpad scroll waits out a slow drag's pauses, whoever
1767
+ // keeps it; a pinch or a mouse wheel's notches 100 ms.
1768
+ gesture.timer = this.clock.setTimeout(() => {
1769
+ this.segmenter.end();
1770
+ this.endWheel();
1771
+ }, gesture.endMs);
1772
+ if (gesture.ids.length === 0) {
1773
+ return;
1774
+ }
1775
+ wheel.preventDefault();
1776
+ if (gesture.kind === 'pan' && gesture.follow) {
1777
+ const f = gesture.follow;
1778
+ if (f.lifted) {
1779
+ return;
1780
+ }
1781
+ if (this.wheelSegment?.liftNow === true) {
1782
+ // Momentum began: the fingers lifted before the held deltas. The pan
1783
+ // keeps the travel it showed through the undecided stretch. It lets
1784
+ // go here, still moving at the fingers' speed, so its release carries
1785
+ // on from it. The rest of the momentum never moves the pan.
1786
+ this.followProvisionally(gesture, f, time);
1787
+ f.lifted = true;
1788
+ this.arena.ended([{
1789
+ id: gesture.ids[0],
1790
+ x: gesture.x,
1791
+ y: gesture.y,
1792
+ time
1793
+ }]);
1794
+ return;
1795
+ }
1796
+ if (this.wheelSegment?.decaying === true) {
1797
+ // Undecided: the real deltas wait for the splitter while the
1798
+ // fingers' last speed carries the pan on.
1799
+ f.heldX += dx;
1800
+ f.heldY += dy;
1801
+ this.followProvisionally(gesture, f, time);
1802
+ return;
1803
+ }
1804
+ // The fingers' own delta; the held ones were theirs too. Their moves
1805
+ // absorb what the pan shows ahead of their travel, or behind it, half a
1806
+ // move at a time. So the pan never steps back, and ends on their travel.
1807
+ const beforeX = f.trueX - f.heldX;
1808
+ const beforeY = f.trueY - f.heldY;
1809
+ const debtX = absorbedDebt(gesture.x - beforeX, dx);
1810
+ const debtY = absorbedDebt(gesture.y - beforeY, dy);
1811
+ const trueX = beforeX - dx;
1812
+ const trueY = beforeY - dy;
1813
+ const elapsed = time - f.lastMove;
1814
+ if (elapsed > 0) {
1815
+ f.speedX = (trueX - f.trueX) / elapsed;
1816
+ f.speedY = (trueY - f.trueY) / elapsed;
1817
+ }
1818
+ f.trueX = trueX;
1819
+ f.trueY = trueY;
1820
+ f.heldX = 0;
1821
+ f.heldY = 0;
1822
+ f.lastMove = time;
1823
+ gesture.x = trueX + debtX;
1824
+ gesture.y = trueY + debtY;
1825
+ f.baseX = gesture.x;
1826
+ f.baseY = gesture.y;
1827
+ this.arena.moved([{
1828
+ id: gesture.ids[0],
1829
+ x: gesture.x,
1830
+ y: gesture.y,
1831
+ time
1832
+ }]);
1833
+ return;
1834
+ }
1835
+ if (gesture.kind === 'pan') {
1836
+ gesture.x -= dx;
1837
+ gesture.y -= dy;
1838
+ this.arena.moved([{
1839
+ id: gesture.ids[0],
1840
+ x: gesture.x,
1841
+ y: gesture.y,
1842
+ time
1843
+ }]);
1844
+ } else {
1845
+ gesture.span *= Math.exp(-dy / 100);
1846
+ this.arena.moved(this.pinchContacts(gesture, time));
1847
+ }
1848
+ };
1849
+
1850
+ /**
1851
+ * Move a followed pan through the splitter's undecided stretch. It moves on
1852
+ * from where it was shown at the fingers' last move, at their last speed.
1853
+ * It never passes what that speed projects for the time since.
1854
+ */
1855
+ followProvisionally(gesture, f, time) {
1856
+ const elapsed = Math.max(0, time - f.lastMove);
1857
+ const x = f.baseX + f.speedX * elapsed;
1858
+ const y = f.baseY + f.speedY * elapsed;
1859
+ if (x === gesture.x && y === gesture.y) {
1860
+ return;
1861
+ }
1862
+ gesture.x = x;
1863
+ gesture.y = y;
1864
+ this.arena.moved([{
1865
+ id: gesture.ids[0],
1866
+ x,
1867
+ y,
1868
+ time
1869
+ }]);
1870
+ }
1871
+
1872
+ /** Start a wheel gesture: who takes it, decided once, or nobody. */
1873
+ beginWheel(wheel, kind, dx, dy, time) {
1874
+ const type = scrollTypeOf(wheel);
1875
+ const isHorizontal = Math.abs(dx) > Math.abs(dy);
1876
+ const kept = {
1877
+ kind,
1878
+ scrollType: type,
1879
+ ids: [],
1880
+ x: wheel.clientX,
1881
+ y: wheel.clientY,
1882
+ span: WHEEL_PINCH_SPAN,
1883
+ last: time,
1884
+ endMs: wheelEndMs(kind, type, isHorizontal),
1885
+ timer: null,
1886
+ takers: []
1887
+ };
1888
+ // A contact already down keeps its gesture; the wheel stays the page's.
1889
+ // So does a first wheel another handler already prevented, or one that
1890
+ // cannot be cancelled: no recognizer could take it from the page.
1891
+ if (this.arena.touches.length > 0 || dx === 0 && dy === 0 || wheel.defaultPrevented || !wheel.cancelable) {
1892
+ return kept;
1893
+ }
1894
+ // Every scroller under the pointer, the page included, as a finger's
1895
+ // probe reads them, without touch-action, which a wheel ignores.
1896
+ // What the wheel landed on: at a shadow host's listener the event's
1897
+ // target is the host, and a scroller inside its tree must count.
1898
+ const target = landedOn(wheel);
1899
+ const scroll = createNativeScrollProbe(target, {
1900
+ readsTouchAction: false
1901
+ });
1902
+ const takers = [];
1903
+ for (let node = target instanceof Node ? target : null; node != null; node = parentAcrossShadow(node)) {
1904
+ if (!(node instanceof Element)) {
1905
+ continue;
1906
+ }
1907
+ for (const recognizer of this.registry.get(node)?.recognizers ?? []) {
1908
+ // A pan follows a trackpad, never a mouse wheel's notches, which
1909
+ // scroll what is under the pointer.
1910
+ if (!recognizer.isEnabled || !recognizer.claimsTouches || !recognizer.allowedScrollTypes.has(type) || internals(recognizer).family !== kind || kind === 'pan' && type !== 'continuous') {
1911
+ continue;
1912
+ }
1913
+ if (kind === 'pan' && (!panTakesMove(recognizer, -dx, -dy) || this.scrollerTakes(target, node, dx, dy, isHorizontal) ||
1914
+ // A scroll-edge pan only past every scroller's edge.
1915
+ isScrollEdgePan(recognizer) && scroll.canScroll(-dx, -dy))) {
1916
+ continue;
1917
+ }
1918
+ takers.push(recognizer);
1919
+ }
1920
+ }
1921
+ if (takers.length === 0) {
1922
+ return kept;
1923
+ }
1924
+ const base = {
1925
+ pointerType: 'mouse',
1926
+ target,
1927
+ scroll,
1928
+ scrollType: type,
1929
+ recognizers: takers,
1930
+ consumer: null
1931
+ };
1932
+ const ids = kind === 'pan' ? [this.nextId++] : [this.nextId++, this.nextId++];
1933
+ const gesture = {
1934
+ ...kept,
1935
+ ids,
1936
+ takers
1937
+ };
1938
+ if (kind === 'pan' && type === 'continuous') {
1939
+ gesture.follow = {
1940
+ lifted: false,
1941
+ heldX: 0,
1942
+ heldY: 0,
1943
+ lastMove: time,
1944
+ trueX: kept.x,
1945
+ trueY: kept.y,
1946
+ baseX: kept.x,
1947
+ baseY: kept.y,
1948
+ speedX: 0,
1949
+ speedY: 0
1950
+ };
1951
+ }
1952
+ this.arena.began(kind === 'pan' ? [{
1953
+ ...base,
1954
+ id: ids[0],
1955
+ x: wheel.clientX,
1956
+ y: wheel.clientY,
1957
+ time
1958
+ }] : this.pinchContacts(gesture, time).map(input => ({
1959
+ ...base,
1960
+ ...input
1961
+ })));
1962
+ // Every recognizer it was offered to may decline it (its own
1963
+ // shouldReceiveTouch). Then nobody took the wheel, and it scrolls the
1964
+ // page from its first step, as with no gesture system.
1965
+ if (ids.every(id => this.arena.participantsOf(id).length === 0)) {
1966
+ this.arena.cancelled(ids, time);
1967
+ return kept;
1968
+ }
1969
+ return gesture;
1970
+ }
1971
+
1972
+ /**
1973
+ * Whether a scroller between `target` and `boundary` (both included)
1974
+ * scrolls this wheel now, along the wheel's major axis. It uses the edge
1975
+ * test a finger's probe uses (canElementScroll).
1976
+ */
1977
+ scrollerTakes(target, boundary, dx, dy, isHorizontal) {
1978
+ for (let node = target instanceof Node ? target : null; node != null; node = parentAcrossShadow(node)) {
1979
+ if (node instanceof Element && canElementScroll(node, isHorizontal ? -dx : 0, isHorizontal ? 0 : -dy)) {
1980
+ return true;
1981
+ }
1982
+ if (node === boundary) {
1983
+ break;
1984
+ }
1985
+ }
1986
+ return false;
1987
+ }
1988
+
1989
+ /** A ctrl-wheel pinch's two contacts, `span` apart around its center. */
1990
+ pinchContacts(gesture, time) {
1991
+ return [{
1992
+ id: gesture.ids[0],
1993
+ x: gesture.x - gesture.span / 2,
1994
+ y: gesture.y,
1995
+ time
1996
+ }, {
1997
+ id: gesture.ids[1],
1998
+ x: gesture.x + gesture.span / 2,
1999
+ y: gesture.y,
2000
+ time
2001
+ }];
2002
+ }
2003
+
2004
+ /** Whether a recognizer that took this wheel gesture still takes part in it. */
2005
+ wheelHasTaker(gesture) {
2006
+ // A cancelled or failed recognizer keeps that state until its contacts
2007
+ // end, which a wheel gesture's do only here.
2008
+ return gesture.takers.some(recognizer => {
2009
+ const {
2010
+ state
2011
+ } = recognizer;
2012
+ return recognizer.isEnabled && (state === 'began' || state === 'changed' || state === 'possible' && recognizer.isInvolved());
2013
+ });
2014
+ }
2015
+
2016
+ /**
2017
+ * Route every wheel on the page to the live wheel gesture, from its claim.
2018
+ * The route is the shared input stream's capture listener on the window,
2019
+ * which runs before any document listener. It is non-passive only while it
2020
+ * is subscribed.
2021
+ */
2022
+ latchWheel() {
2023
+ const view = this.doc.defaultView;
2024
+ if (this.stopWheelLatch == null && view != null) {
2025
+ const cancelWheel = () => {
2026
+ const ids = this.dropDrivenWheel();
2027
+ if (ids.length > 0) {
2028
+ this.arena.cancelled(ids, this.arena.now());
2029
+ }
2030
+ };
2031
+ const stops = [subscribeInput(view, 'wheel', this.onWheel, {
2032
+ cancels: true
2033
+ }),
2034
+ // A page that loses focus or hides ends a trackpad pan, as it ends
2035
+ // every touch. An element's blur reaches the window's capture phase
2036
+ // too and means nothing here.
2037
+ subscribeInput(view, 'blur', event => {
2038
+ if (!(event.target instanceof view.Node)) {
2039
+ cancelWheel();
2040
+ }
2041
+ }), subscribeInput(view, 'visibilitychange', () => {
2042
+ if (this.doc.visibilityState === 'hidden') {
2043
+ cancelWheel();
2044
+ }
2045
+ })];
2046
+ this.stopWheelLatch = () => {
2047
+ for (const stop of stops) {
2048
+ stop();
2049
+ }
2050
+ };
2051
+ }
2052
+ }
2053
+
2054
+ /**
2055
+ * End the trackpad pan a recognizer still drives, when the page loses focus
2056
+ * or hides, and return its contacts to cancel. The rest of its stream is
2057
+ * the page's until the stream ends, as a touch the browser takes is. A
2058
+ * ctrl-wheel pinch keeps its stream, so the page never zooms for it. A
2059
+ * gesture the page keeps stays as it is. So does a pan its lift released:
2060
+ * its momentum stays latched and prevented until the stream ends.
2061
+ */
2062
+ dropDrivenWheel() {
2063
+ const gesture = this.wheel;
2064
+ if (gesture == null || gesture.kind !== 'pan' || gesture.ids.length === 0 || gesture.follow?.lifted === true) {
2065
+ return [];
2066
+ }
2067
+ const kept = {
2068
+ ...gesture,
2069
+ ids: [],
2070
+ takers: []
2071
+ };
2072
+ delete kept.follow;
2073
+ this.wheel = kept;
2074
+ this.stopWheelLatch?.();
2075
+ this.stopWheelLatch = null;
2076
+ return gesture.ids;
2077
+ }
2078
+
2079
+ /**
2080
+ * End the wheel gesture in progress. On its own timer, it ends its end
2081
+ * window after its last wheel. A taker that leaves (a detach, a disable)
2082
+ * passes that last wheel's time. An input that ends it sooner (a press,
2083
+ * the next gesture's first wheel) passes its own time. Both times are on
2084
+ * the wheels' time base (eventTime), never the host's clock, which need
2085
+ * not share it.
2086
+ */
2087
+ endWheel(endTime) {
2088
+ const gesture = this.wheel;
2089
+ this.stopWheelLatch?.();
2090
+ this.stopWheelLatch = null;
2091
+ if (gesture == null) {
2092
+ return;
2093
+ }
2094
+ this.wheel = null;
2095
+ if (gesture.timer != null) {
2096
+ this.clock.clearTimeout(gesture.timer);
2097
+ }
2098
+ if (gesture.ids.length === 0 || gesture.follow?.lifted) {
2099
+ return;
2100
+ }
2101
+ // Fingers that stop without a lift end on their travel, where it runs
2102
+ // ahead of what the pan shows. What the undecided stretch showed past it
2103
+ // stays, as a step back would show.
2104
+ if (gesture.follow != null) {
2105
+ const f = gesture.follow;
2106
+ const x = ahead(gesture.x, f.trueX - f.heldX, f.speedX);
2107
+ const y = ahead(gesture.y, f.trueY - f.heldY, f.speedY);
2108
+ if (x !== gesture.x || y !== gesture.y) {
2109
+ gesture.x = x;
2110
+ gesture.y = y;
2111
+ this.arena.moved([{
2112
+ id: gesture.ids[0],
2113
+ x,
2114
+ y,
2115
+ time: gesture.last
2116
+ }]);
2117
+ }
2118
+ }
2119
+ // A trackpad scroll whose fingers stop releases still, past the
2120
+ // velocity's stillness interval. A pinch releases 100 ms after its last
2121
+ // wheel, and a lift released the pan already. The release is never later
2122
+ // than the input that ends it (a press, the next gesture's first wheel).
2123
+ // The arena's clock only moves forward, so a later stamp would move that
2124
+ // input's times too.
2125
+ const end = gesture.last + gesture.endMs;
2126
+ const time = endTime == null ? end : Math.max(gesture.last, Math.min(end, endTime));
2127
+ this.arena.ended(gesture.kind === 'pan' ? [{
2128
+ id: gesture.ids[0],
2129
+ x: gesture.x,
2130
+ y: gesture.y,
2131
+ time
2132
+ }] : this.pinchContacts(gesture, time));
2133
+ }
2134
+
1501
2135
  /** Undo `lockSelection` for a contact that ended; idempotent. */
1502
2136
  unlockSelection(id) {
1503
2137
  this.lockedBy.get(id)?.();
@@ -1539,6 +2173,7 @@ export class GestureHost {
1539
2173
  return true;
1540
2174
  }
1541
2175
  onTouchStart = event => {
2176
+ this.endWheel(eventTime(event));
1542
2177
  this.noteFingersDown(event);
1543
2178
  // Where the host reads a finger from its pointer events (no ontouchstart,
1544
2179
  // as after touch emulation turns on), its Touch Events would track it twice.
@@ -2157,6 +2792,7 @@ export class GestureHost {
2157
2792
  // --- Pointer events --------------------------------------------------------
2158
2793
 
2159
2794
  onPointerDown = event => {
2795
+ this.endWheel(eventTime(event));
2160
2796
  // A new press: whatever click is still owed to the last one is not this.
2161
2797
  disarmClickGuard(this.doc);
2162
2798
  if (!isPointerKind(event.pointerType)) {
@@ -2566,6 +3202,8 @@ export class GestureHost {
2566
3202
  this.unpairedTouches.clear();
2567
3203
  this.lastFed.clear();
2568
3204
  this.pointerFed.clear();
3205
+ // A trackpad pan a recognizer still drives is cancelled with them.
3206
+ ids.push(...this.dropDrivenWheel());
2569
3207
  if (ids.length > 0) {
2570
3208
  this.arena.cancelled(ids, time);
2571
3209
  }