@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.
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +23 -3
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +16 -9
- package/dist/Chat/ChatComposerInput.d.ts +9 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -0
- package/dist/Chat/ChatMessage.js +2 -2
- package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
- package/dist/Chat/chatMessage.vars.stylex.js +8 -8
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +6 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
- package/dist/DropdownMenu/useMenuOverflow.js +31 -8
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +1 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +25 -13
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +404 -131
- package/dist/Slider/sliderTouchController.d.ts +189 -0
- package/dist/Slider/sliderTouchController.d.ts.map +1 -0
- package/dist/Slider/sliderTouchController.js +1017 -0
- package/dist/Slider/useSliderTouch.d.ts +22 -0
- package/dist/Slider/useSliderTouch.d.ts.map +1 -0
- package/dist/Slider/useSliderTouch.js +56 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +10 -8
- package/dist/astryx.css +33 -6
- package/dist/gestures/GestureArena.d.ts.map +1 -1
- package/dist/gestures/GestureArena.js +4 -1
- package/dist/gestures/GestureHost.d.ts +72 -2
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +643 -5
- package/dist/gestures/GestureRecognizer.d.ts +15 -1
- package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/GestureRecognizer.js +12 -0
- package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
- package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.js +10 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
- package/dist/gestures/nativeScroll.d.ts +16 -2
- package/dist/gestures/nativeScroll.d.ts.map +1 -1
- package/dist/gestures/nativeScroll.js +39 -8
- package/dist/gestures/recognizerInternals.d.ts +4 -2
- package/dist/gestures/recognizerInternals.d.ts.map +1 -1
- package/dist/gestures/types.d.ts +11 -0
- package/dist/gestures/types.d.ts.map +1 -1
- package/dist/gestures/types.js +7 -0
- package/dist/gestures/wheelSegmenter.d.ts +45 -0
- package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
- package/dist/gestures/wheelSegmenter.js +98 -0
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +77 -14
- package/dist/motion/uikit/index.d.ts +2 -1
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +2 -1
- package/dist/motion/uikit/slider.d.ts +97 -0
- package/dist/motion/uikit/slider.d.ts.map +1 -0
- package/dist/motion/uikit/slider.js +111 -0
- package/dist/motion/uikit/springs.d.ts +46 -1
- package/dist/motion/uikit/springs.d.ts.map +1 -1
- package/dist/motion/uikit/springs.js +48 -4
- package/package.json +5 -5
- package/src/AppShell/AppShell.doc.mjs +10 -0
- package/src/AppShell/AppShell.tsx +48 -2
- package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
- package/src/Chat/Chat.doc.mjs +1 -0
- package/src/Chat/ChatComposerInput.doc.mjs +11 -0
- package/src/Chat/ChatComposerInput.tsx +11 -0
- package/src/Chat/ChatMessage.tsx +2 -2
- package/src/Chat/chatMessage.vars.stylex.ts +8 -8
- package/src/Drawer/Drawer.tsx +6 -0
- package/src/DropdownMenu/useMenuOverflow.ts +44 -8
- package/src/Link/Link.tsx +3 -0
- package/src/Selector/Selector.tsx +29 -2
- package/src/Slider/Slider.doc.mjs +11 -7
- package/src/Slider/Slider.tsx +539 -132
- package/src/Slider/sliderTouchController.ts +1209 -0
- package/src/Slider/useSliderTouch.ts +78 -0
- package/src/Typeahead/BaseTypeahead.tsx +9 -8
- package/src/gestures/GestureArena.ts +1 -0
- package/src/gestures/GestureHost.ts +765 -5
- package/src/gestures/GestureRecognizer.ts +28 -0
- package/src/gestures/PanGestureRecognizer.ts +8 -0
- package/src/gestures/PinchGestureRecognizer.ts +16 -1
- package/src/gestures/README.md +63 -0
- package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
- package/src/gestures/nativeScroll.ts +49 -9
- package/src/gestures/recognizerInternals.ts +9 -2
- package/src/gestures/types.ts +12 -0
- package/src/gestures/wheelSegmenter.ts +114 -0
- package/src/motion/animator.ts +115 -19
- package/src/motion/uikit/index.ts +2 -1
- package/src/motion/uikit/slider.ts +105 -0
- package/src/motion/uikit/springs.ts +37 -2
|
@@ -140,9 +140,14 @@ import {
|
|
|
140
140
|
import type {GestureRecognizer, SelectionLock} from './GestureRecognizer';
|
|
141
141
|
import type {PressBounds} from './PressTracker';
|
|
142
142
|
import type {PanGestureRecognizer} from './PanGestureRecognizer';
|
|
143
|
-
import {
|
|
143
|
+
import {
|
|
144
|
+
canElementScroll,
|
|
145
|
+
createNativeScrollProbe,
|
|
146
|
+
parentAcrossShadow,
|
|
147
|
+
} from './nativeScroll';
|
|
144
148
|
import {containsAlongFlatTree} from '../utils/flatTree';
|
|
145
149
|
import {isIosWebKit} from '../utils/touchQueries';
|
|
150
|
+
import {type WheelSegment, WheelSegmenter} from './wheelSegmenter';
|
|
146
151
|
import {
|
|
147
152
|
gestureHostStyle,
|
|
148
153
|
type GestureHostStyle,
|
|
@@ -158,6 +163,7 @@ import type {
|
|
|
158
163
|
GestureEnvironment,
|
|
159
164
|
GestureTouch,
|
|
160
165
|
PointerKind,
|
|
166
|
+
ScrollType,
|
|
161
167
|
TouchBeganInput,
|
|
162
168
|
TouchConsumer,
|
|
163
169
|
TouchInput,
|
|
@@ -186,6 +192,8 @@ interface Registration {
|
|
|
186
192
|
hasTouchEndListener: boolean;
|
|
187
193
|
/** Whether the element carries the host's shared blocking touchmove handler. */
|
|
188
194
|
hasTouchMoveListener: boolean;
|
|
195
|
+
/** Whether the element carries the host's wheel handler. */
|
|
196
|
+
hasWheelListener: boolean;
|
|
189
197
|
/** Inline values the host replaced, to restore on detach. */
|
|
190
198
|
readonly savedStyle: Map<string, string>;
|
|
191
199
|
/**
|
|
@@ -305,6 +313,16 @@ function claimsWhileMoving(recognizer: GestureRecognizer): boolean {
|
|
|
305
313
|
);
|
|
306
314
|
}
|
|
307
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Whether a recognizer takes a contact that comes with Touch Events and a
|
|
318
|
+
* long press's contextmenu: a finger's or a pen's. A scroll-only recognizer
|
|
319
|
+
* (`allowedPointerTypes: []`) and a mouse-only one take neither.
|
|
320
|
+
*/
|
|
321
|
+
function takesTouchContacts(recognizer: GestureRecognizer): boolean {
|
|
322
|
+
const types = recognizer.allowedPointerTypes;
|
|
323
|
+
return types == null || types.has('touch') || types.has('pen');
|
|
324
|
+
}
|
|
325
|
+
|
|
308
326
|
/**
|
|
309
327
|
* A pan (or a pan subclass), told apart by its family rather than its class,
|
|
310
328
|
* so a page that constructs no pan never ships PanGestureRecognizer.
|
|
@@ -381,6 +399,207 @@ function landedOn(
|
|
|
381
399
|
return event.composedPath()[0] ?? retargeted;
|
|
382
400
|
}
|
|
383
401
|
|
|
402
|
+
/**
|
|
403
|
+
* How long after its last event a pinch or a mouse wheel's notches end, ms.
|
|
404
|
+
* The web reports no wheel phases, so a quiet wheel is a lifted one.
|
|
405
|
+
*/
|
|
406
|
+
const WHEEL_END_MS = 100;
|
|
407
|
+
/**
|
|
408
|
+
* How long after its last event a vertical trackpad scroll ends, ms (a
|
|
409
|
+
* sheet's pull), whoever keeps it. A slow drag's stream pauses longer than
|
|
410
|
+
* WHEEL_END_MS between events.
|
|
411
|
+
*/
|
|
412
|
+
const WHEEL_FOLLOW_END_V_MS = 180;
|
|
413
|
+
/**
|
|
414
|
+
* The same horizontally, ms (a pager's or a row's swipe), whose slow
|
|
415
|
+
* swipes pause longer still.
|
|
416
|
+
*/
|
|
417
|
+
const WHEEL_FOLLOW_END_MS = 220;
|
|
418
|
+
/** A line-mode wheel's line, px, as the platforms scroll it. */
|
|
419
|
+
const WHEEL_LINE_PX = 40;
|
|
420
|
+
/** The span a ctrl-wheel pinch's two contacts start at, px. */
|
|
421
|
+
const WHEEL_PINCH_SPAN = 100;
|
|
422
|
+
|
|
423
|
+
/** A wheel gesture: the contacts it drives in the arena, or none where the page keeps it. */
|
|
424
|
+
interface WheelGesture {
|
|
425
|
+
readonly kind: 'pan' | 'pinch';
|
|
426
|
+
/** A mouse wheel's notches, or a trackpad's continuous scroll. */
|
|
427
|
+
readonly scrollType: ScrollType;
|
|
428
|
+
readonly ids: ReadonlyArray<number>;
|
|
429
|
+
/** A pan's contact, or a pinch's center. */
|
|
430
|
+
x: number;
|
|
431
|
+
y: number;
|
|
432
|
+
/** A pinch's span between its two contacts. */
|
|
433
|
+
span: number;
|
|
434
|
+
/** The latest wheel's time. */
|
|
435
|
+
last: number;
|
|
436
|
+
/**
|
|
437
|
+
* How long after its last wheel it ends, ms. A trackpad pan stream waits
|
|
438
|
+
* out its axis's pause bound, whoever keeps it. A pinch or a mouse wheel's
|
|
439
|
+
* notches wait 100 ms.
|
|
440
|
+
*/
|
|
441
|
+
readonly endMs: number;
|
|
442
|
+
timer: unknown;
|
|
443
|
+
/** The recognizers that took it; none where the page keeps it. */
|
|
444
|
+
readonly takers: ReadonlyArray<GestureRecognizer>;
|
|
445
|
+
/** A trackpad scroll a pan follows, until its fingers lift. */
|
|
446
|
+
follow?: WheelFollow;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** A trackpad scroll a pan follows: where its fingers lift, and what waits until then. */
|
|
450
|
+
interface WheelFollow {
|
|
451
|
+
/** The fingers lifted: the momentum after them never moves the pan. */
|
|
452
|
+
lifted: boolean;
|
|
453
|
+
/**
|
|
454
|
+
* Shrinking deltas held until the splitter decides. They are the fingers'
|
|
455
|
+
* when larger ones follow, and the start of momentum when it reads the
|
|
456
|
+
* lift.
|
|
457
|
+
*/
|
|
458
|
+
heldX: number;
|
|
459
|
+
heldY: number;
|
|
460
|
+
/** The time of the fingers' last move the pan took. */
|
|
461
|
+
lastMove: number;
|
|
462
|
+
/** The fingers' travel as of that move: the contact without any held delta. */
|
|
463
|
+
trueX: number;
|
|
464
|
+
trueY: number;
|
|
465
|
+
/** Where the pan was shown at that move, which the undecided stretch moves on from. */
|
|
466
|
+
baseX: number;
|
|
467
|
+
baseY: number;
|
|
468
|
+
/** The fingers' speed at that move, content px per ms. */
|
|
469
|
+
speedX: number;
|
|
470
|
+
speedY: number;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* How long after its last wheel a wheel gesture ends, ms. A trackpad's
|
|
475
|
+
* two-finger scroll waits out a slow drag's pauses for its axis, whoever
|
|
476
|
+
* keeps the stream. A pinch and a mouse wheel's notches end 100 ms after
|
|
477
|
+
* their last wheel.
|
|
478
|
+
*/
|
|
479
|
+
function wheelEndMs(
|
|
480
|
+
kind: 'pan' | 'pinch',
|
|
481
|
+
scrollType: ScrollType,
|
|
482
|
+
isHorizontal: boolean,
|
|
483
|
+
): number {
|
|
484
|
+
if (kind === 'pinch' || scrollType !== 'continuous') {
|
|
485
|
+
return WHEEL_END_MS;
|
|
486
|
+
}
|
|
487
|
+
return isHorizontal ? WHEEL_FOLLOW_END_MS : WHEEL_FOLLOW_END_V_MS;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* A provisional lead `debt` after a move of `moved`. The debt is what a
|
|
492
|
+
* followed pan shows past the fingers' travel, or short of it. Half the move
|
|
493
|
+
* is absorbed, so the pan keeps moving the way the fingers do.
|
|
494
|
+
*/
|
|
495
|
+
function absorbedDebt(debt: number, moved: number): number {
|
|
496
|
+
return Math.sign(debt) * Math.max(0, Math.abs(debt) - Math.abs(moved) / 2);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* `target` where it is ahead of `shown` in the direction of `speed`, else
|
|
501
|
+
* `shown`: a reconcile that never steps back.
|
|
502
|
+
*/
|
|
503
|
+
function ahead(shown: number, target: number, speed: number): number {
|
|
504
|
+
return speed === 0 || (target - shown) * Math.sign(speed) > 0
|
|
505
|
+
? target
|
|
506
|
+
: shown;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* A wheel's deltas in CSS px: a line is `WHEEL_LINE_PX`, a page the
|
|
511
|
+
* viewport.
|
|
512
|
+
*/
|
|
513
|
+
function wheelPixels(
|
|
514
|
+
wheel: WheelEvent,
|
|
515
|
+
view: Window | null,
|
|
516
|
+
): {dx: number; dy: number} {
|
|
517
|
+
if (wheel.deltaMode === 1) {
|
|
518
|
+
return {dx: wheel.deltaX * WHEEL_LINE_PX, dy: wheel.deltaY * WHEEL_LINE_PX};
|
|
519
|
+
}
|
|
520
|
+
if (wheel.deltaMode === 2) {
|
|
521
|
+
return {
|
|
522
|
+
dx: wheel.deltaX * (view?.innerWidth ?? 0),
|
|
523
|
+
dy: wheel.deltaY * (view?.innerHeight ?? 0),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
return {dx: wheel.deltaX, dy: wheel.deltaY};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The web's best reading of what scrolled: a mouse wheel's notches
|
|
531
|
+
* (`discrete`) or a trackpad's scroll (`continuous`). Input it cannot tell
|
|
532
|
+
* apart counts as a mouse wheel's, which a pan that takes only a trackpad's
|
|
533
|
+
* scroll leaves to the page. These read as a mouse wheel's:
|
|
534
|
+
* - a line- or page-mode delta;
|
|
535
|
+
* - a scroll with Shift held (Shift+wheel);
|
|
536
|
+
* - a pixel delta a hair off a whole pixel (a mouse wheel on Windows);
|
|
537
|
+
* - a scroll with no legacy `wheelDelta` (an engine that sends none);
|
|
538
|
+
* - a delta that is not a third of its legacy `wheelDelta`;
|
|
539
|
+
* - a 120 tick over a 40 px delta.
|
|
540
|
+
* A trackpad's precise deltas, a third of their legacy `wheelDelta` in
|
|
541
|
+
* Chromium and WebKit, read as a trackpad's.
|
|
542
|
+
*/
|
|
543
|
+
function scrollTypeOf(wheel: WheelEvent): ScrollType {
|
|
544
|
+
if (wheel.deltaMode !== 0 || wheel.shiftKey) {
|
|
545
|
+
return 'discrete';
|
|
546
|
+
}
|
|
547
|
+
const remainder = (value: number) => {
|
|
548
|
+
const fraction = Math.abs(value - Math.round(value));
|
|
549
|
+
return fraction > 0 && fraction < 0.01;
|
|
550
|
+
};
|
|
551
|
+
if (remainder(wheel.deltaX) || remainder(wheel.deltaY)) {
|
|
552
|
+
return 'discrete';
|
|
553
|
+
}
|
|
554
|
+
const legacy = wheel as WheelEvent & {
|
|
555
|
+
wheelDeltaX?: number;
|
|
556
|
+
wheelDeltaY?: number;
|
|
557
|
+
};
|
|
558
|
+
if (
|
|
559
|
+
typeof legacy.wheelDeltaY !== 'number' ||
|
|
560
|
+
typeof legacy.wheelDeltaX !== 'number'
|
|
561
|
+
) {
|
|
562
|
+
return 'discrete';
|
|
563
|
+
}
|
|
564
|
+
const precise = (delta: number, legacyDelta: number) =>
|
|
565
|
+
delta === 0
|
|
566
|
+
? legacyDelta === 0
|
|
567
|
+
: Math.abs(Math.abs(legacyDelta) - 3 * Math.abs(delta)) <=
|
|
568
|
+
Math.max(1.5, 0.05 * Math.abs(delta) * 3);
|
|
569
|
+
if (
|
|
570
|
+
!precise(wheel.deltaY, legacy.wheelDeltaY) ||
|
|
571
|
+
!precise(wheel.deltaX, legacy.wheelDeltaX)
|
|
572
|
+
) {
|
|
573
|
+
return 'discrete';
|
|
574
|
+
}
|
|
575
|
+
const tick = (delta: number, legacyDelta: number) =>
|
|
576
|
+
legacyDelta !== 0 && legacyDelta % 120 === 0 && delta % 40 === 0;
|
|
577
|
+
return tick(wheel.deltaY, legacy.wheelDeltaY) ||
|
|
578
|
+
tick(wheel.deltaX, legacy.wheelDeltaX)
|
|
579
|
+
? 'discrete'
|
|
580
|
+
: 'continuous';
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Read without the pan class, which a page with no pan never ships. */
|
|
584
|
+
function isScrollEdgePan(recognizer: GestureRecognizer): boolean {
|
|
585
|
+
return (recognizer as {scrollEdge?: boolean}).scrollEdge === true;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Whether a pan takes a contact moving by (cx, cy), by its own direction
|
|
590
|
+
* rule, `axisRatio` included. A move its rule leaves undecided stays the
|
|
591
|
+
* page's.
|
|
592
|
+
*/
|
|
593
|
+
function panTakesMove(
|
|
594
|
+
recognizer: GestureRecognizer,
|
|
595
|
+
cx: number,
|
|
596
|
+
cy: number,
|
|
597
|
+
): boolean {
|
|
598
|
+
return (
|
|
599
|
+
(internals(recognizer).classify?.({x: cx, y: cy}) ?? 'begin') === 'begin'
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
384
603
|
/**
|
|
385
604
|
* An event's time on the arena's clock (`performance.now()`). Some engines
|
|
386
605
|
* and DOM emulators stamp events in epoch milliseconds instead.
|
|
@@ -530,6 +749,16 @@ export class GestureHost {
|
|
|
530
749
|
*/
|
|
531
750
|
private fingerTargets: ReadonlyArray<Node> = [];
|
|
532
751
|
private nextId = 1;
|
|
752
|
+
/** The wheel gesture in progress. */
|
|
753
|
+
private wheel: WheelGesture | null = null;
|
|
754
|
+
/** The wheel latch's input-stream subscription, while a gesture is latched. */
|
|
755
|
+
private stopWheelLatch: (() => void) | null = null;
|
|
756
|
+
/** Splits the wheel stream into gestures and reads a trackpad's lift. */
|
|
757
|
+
private readonly segmenter = new WheelSegmenter();
|
|
758
|
+
/** The latest wheel's segment. */
|
|
759
|
+
private wheelSegment: WheelSegment | null = null;
|
|
760
|
+
/** Wheels already taken: each element on the path with a listener hears one. */
|
|
761
|
+
private readonly wheelsSeen = new WeakSet<Event>();
|
|
533
762
|
private isInstalled = false;
|
|
534
763
|
/**
|
|
535
764
|
* Whether the primary pointer is coarse (`spec:AST-094` FR19), read when
|
|
@@ -648,6 +877,16 @@ export class GestureHost {
|
|
|
648
877
|
if (recognizer.state !== 'possible' || recognizer.isInvolved()) {
|
|
649
878
|
recognizer.cancel();
|
|
650
879
|
}
|
|
880
|
+
const wheel = this.wheel;
|
|
881
|
+
if (
|
|
882
|
+
wheel != null &&
|
|
883
|
+
wheel.takers.includes(recognizer) &&
|
|
884
|
+
!this.wheelHasTaker(wheel)
|
|
885
|
+
) {
|
|
886
|
+
// At its last wheel: a later stamp would move the arena's clock past
|
|
887
|
+
// the next input's own time.
|
|
888
|
+
this.endWheel(wheel.last);
|
|
889
|
+
}
|
|
651
890
|
this.refresh(element, registration);
|
|
652
891
|
// Even when the element keeps other attachments: one left disabled
|
|
653
892
|
// keeps no listener or observer (FR24).
|
|
@@ -1055,6 +1294,7 @@ export class GestureHost {
|
|
|
1055
1294
|
consumer: null,
|
|
1056
1295
|
hasTouchEndListener: false,
|
|
1057
1296
|
hasTouchMoveListener: false,
|
|
1297
|
+
hasWheelListener: false,
|
|
1058
1298
|
savedStyle: new Map(),
|
|
1059
1299
|
authorTouchAction: null,
|
|
1060
1300
|
manageStyle: true,
|
|
@@ -1135,6 +1375,26 @@ export class GestureHost {
|
|
|
1135
1375
|
element.removeEventListener('touchmove', this.onBlockingTouchMove);
|
|
1136
1376
|
registration.hasTouchMoveListener = false;
|
|
1137
1377
|
}
|
|
1378
|
+
// Wheel and trackpad input, only where an enabled recognizer takes it. A
|
|
1379
|
+
// non-passive wheel listener makes the browser's scrolling over the
|
|
1380
|
+
// element wait for the main thread.
|
|
1381
|
+
const needsWheel = registration.recognizers.some(
|
|
1382
|
+
recognizer =>
|
|
1383
|
+
recognizer.isEnabled &&
|
|
1384
|
+
// A watcher never takes the wheel: the page keeps it.
|
|
1385
|
+
recognizer.claimsTouches &&
|
|
1386
|
+
recognizer.allowedScrollTypes.size > 0 &&
|
|
1387
|
+
// A wheel drives a pan or a pinch only.
|
|
1388
|
+
(internals(recognizer).family === 'pan' ||
|
|
1389
|
+
internals(recognizer).family === 'pinch'),
|
|
1390
|
+
);
|
|
1391
|
+
if (needsWheel && !registration.hasWheelListener) {
|
|
1392
|
+
element.addEventListener('wheel', this.onWheel, {passive: false});
|
|
1393
|
+
registration.hasWheelListener = true;
|
|
1394
|
+
} else if (!needsWheel && registration.hasWheelListener) {
|
|
1395
|
+
element.removeEventListener('wheel', this.onWheel);
|
|
1396
|
+
registration.hasWheelListener = false;
|
|
1397
|
+
}
|
|
1138
1398
|
}
|
|
1139
1399
|
|
|
1140
1400
|
private applyStyle(element: HTMLElement, registration: Registration): void {
|
|
@@ -1290,6 +1550,9 @@ export class GestureHost {
|
|
|
1290
1550
|
if (registration.hasTouchMoveListener) {
|
|
1291
1551
|
element.removeEventListener('touchmove', this.onBlockingTouchMove);
|
|
1292
1552
|
}
|
|
1553
|
+
if (registration.hasWheelListener) {
|
|
1554
|
+
element.removeEventListener('wheel', this.onWheel);
|
|
1555
|
+
}
|
|
1293
1556
|
this.registry.delete(element);
|
|
1294
1557
|
this.uninstallIfIdle();
|
|
1295
1558
|
}
|
|
@@ -1394,7 +1657,13 @@ export class GestureHost {
|
|
|
1394
1657
|
for (const [element, registration] of this.registry) {
|
|
1395
1658
|
const consumer = registration.consumer;
|
|
1396
1659
|
if (
|
|
1397
|
-
registration.recognizers.some(
|
|
1660
|
+
registration.recognizers.some(
|
|
1661
|
+
recognizer =>
|
|
1662
|
+
recognizer.isEnabled &&
|
|
1663
|
+
// One that takes only scroll input has no press to arm.
|
|
1664
|
+
(recognizer.allowedPointerTypes == null ||
|
|
1665
|
+
recognizer.allowedPointerTypes.size > 0),
|
|
1666
|
+
) ||
|
|
1398
1667
|
(consumer != null && consumer.isEnabled?.() !== false)
|
|
1399
1668
|
) {
|
|
1400
1669
|
wanted.add(element);
|
|
@@ -1545,8 +1814,9 @@ export class GestureHost {
|
|
|
1545
1814
|
|
|
1546
1815
|
/**
|
|
1547
1816
|
* Whether an enabled participant could claim a touch or cancel a lift: a
|
|
1548
|
-
* consumer, or a recognizer that claims touches
|
|
1549
|
-
*
|
|
1817
|
+
* consumer, or a recognizer that claims touches and takes a finger or a
|
|
1818
|
+
* pen. With only watchers, or only scroll-only recognizers
|
|
1819
|
+
* (`allowedPointerTypes: []`), the host's listeners are all passive.
|
|
1550
1820
|
*/
|
|
1551
1821
|
private hasClaimer(): boolean {
|
|
1552
1822
|
for (const registration of this.registry.values()) {
|
|
@@ -1554,7 +1824,10 @@ export class GestureHost {
|
|
|
1554
1824
|
if (
|
|
1555
1825
|
(consumer != null && consumer.isEnabled?.() !== false) ||
|
|
1556
1826
|
registration.recognizers.some(
|
|
1557
|
-
recognizer =>
|
|
1827
|
+
recognizer =>
|
|
1828
|
+
recognizer.isEnabled &&
|
|
1829
|
+
recognizer.claimsTouches &&
|
|
1830
|
+
takesTouchContacts(recognizer),
|
|
1558
1831
|
)
|
|
1559
1832
|
) {
|
|
1560
1833
|
return true;
|
|
@@ -1827,6 +2100,489 @@ export class GestureHost {
|
|
|
1827
2100
|
}
|
|
1828
2101
|
}
|
|
1829
2102
|
|
|
2103
|
+
/**
|
|
2104
|
+
* Wheel and trackpad input. A wheel gesture drives the recognizers that
|
|
2105
|
+
* allow its scroll type, as a contact that moves with the content. A pan
|
|
2106
|
+
* takes it along its own axis, once no scroller between the pointer and
|
|
2107
|
+
* the pan's element can scroll that way. The gesture's first wheel decides
|
|
2108
|
+
* this. A pinch takes a ctrl-wheel, as a trackpad pinches, as two contacts
|
|
2109
|
+
* whose span follows exp(-deltaY / 100). A wheel the page keeps is never
|
|
2110
|
+
* prevented, and a wheel a recognizer takes always is. A gesture ends its
|
|
2111
|
+
* end window after its last wheel (wheelEndMs), at that wheel's time, so
|
|
2112
|
+
* its release keeps its velocity. A gesture a recognizer took is latched
|
|
2113
|
+
* to it, as a browser latches a scroller. While the gesture lasts, the
|
|
2114
|
+
* shared input stream hands this every wheel first, wherever it lands. So
|
|
2115
|
+
* a pan whose element moves from under a still pointer keeps the gesture.
|
|
2116
|
+
* The page listener exists only during a gesture.
|
|
2117
|
+
*/
|
|
2118
|
+
private readonly onWheel = (event: Event): void => {
|
|
2119
|
+
const wheel = event as WheelEvent;
|
|
2120
|
+
if (this.wheelsSeen.has(wheel)) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
this.wheelsSeen.add(wheel);
|
|
2124
|
+
const time = eventTime(wheel);
|
|
2125
|
+
const kind = wheel.ctrlKey ? 'pinch' : 'pan';
|
|
2126
|
+
let gesture = this.wheel;
|
|
2127
|
+
if (
|
|
2128
|
+
gesture != null &&
|
|
2129
|
+
(gesture.kind !== kind || time - gesture.last >= gesture.endMs)
|
|
2130
|
+
) {
|
|
2131
|
+
this.endWheel(time);
|
|
2132
|
+
gesture = null;
|
|
2133
|
+
}
|
|
2134
|
+
if (
|
|
2135
|
+
gesture != null &&
|
|
2136
|
+
gesture.ids.length > 0 &&
|
|
2137
|
+
gesture.follow?.lifted !== true &&
|
|
2138
|
+
!this.wheelHasTaker(gesture)
|
|
2139
|
+
) {
|
|
2140
|
+
// Every recognizer that took it has left (detached, disabled or
|
|
2141
|
+
// cancelled), so the rest of the gesture is the page's. A followed
|
|
2142
|
+
// pan's own lift does not count. Its momentum stays latched and
|
|
2143
|
+
// prevented until the splitter's next gesture or the end timer, and
|
|
2144
|
+
// moves nothing. The release is stamped at its last wheel.
|
|
2145
|
+
this.endWheel(gesture.last);
|
|
2146
|
+
const left = wheelPixels(wheel, this.doc.defaultView);
|
|
2147
|
+
gesture = {
|
|
2148
|
+
kind,
|
|
2149
|
+
scrollType: scrollTypeOf(wheel),
|
|
2150
|
+
ids: [],
|
|
2151
|
+
x: wheel.clientX,
|
|
2152
|
+
y: wheel.clientY,
|
|
2153
|
+
span: WHEEL_PINCH_SPAN,
|
|
2154
|
+
last: time,
|
|
2155
|
+
endMs: wheelEndMs(
|
|
2156
|
+
kind,
|
|
2157
|
+
scrollTypeOf(wheel),
|
|
2158
|
+
Math.abs(left.dx) > Math.abs(left.dy),
|
|
2159
|
+
),
|
|
2160
|
+
timer: null,
|
|
2161
|
+
takers: [],
|
|
2162
|
+
};
|
|
2163
|
+
this.wheel = gesture;
|
|
2164
|
+
}
|
|
2165
|
+
const {dx, dy} = wheelPixels(wheel, this.doc.defaultView);
|
|
2166
|
+
const horizontal = Math.abs(dx) > Math.abs(dy);
|
|
2167
|
+
// The splitter reads a pan's stream only. A ctrl-wheel pinch has no
|
|
2168
|
+
// fingers' phase or momentum to tell apart, and ends 100 ms after its
|
|
2169
|
+
// last wheel.
|
|
2170
|
+
let seg: WheelSegment | null = null;
|
|
2171
|
+
if (kind === 'pan') {
|
|
2172
|
+
seg = this.segmenter.push(
|
|
2173
|
+
time,
|
|
2174
|
+
horizontal ? dx : dy,
|
|
2175
|
+
horizontal ? WHEEL_FOLLOW_END_MS : WHEEL_FOLLOW_END_V_MS,
|
|
2176
|
+
);
|
|
2177
|
+
} else {
|
|
2178
|
+
this.segmenter.end();
|
|
2179
|
+
}
|
|
2180
|
+
this.wheelSegment = seg;
|
|
2181
|
+
if (seg?.boundary != null) {
|
|
2182
|
+
if (gesture != null) {
|
|
2183
|
+
this.endWheel(time);
|
|
2184
|
+
gesture = null;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
if (gesture == null) {
|
|
2188
|
+
gesture = this.beginWheel(wheel, kind, dx, dy, time);
|
|
2189
|
+
this.wheel = gesture;
|
|
2190
|
+
if (gesture.ids.length > 0) {
|
|
2191
|
+
this.latchWheel();
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
gesture.last = time;
|
|
2195
|
+
if (gesture.timer != null) {
|
|
2196
|
+
this.clock.clearTimeout(gesture.timer);
|
|
2197
|
+
}
|
|
2198
|
+
// A trackpad scroll waits out a slow drag's pauses, whoever
|
|
2199
|
+
// keeps it; a pinch or a mouse wheel's notches 100 ms.
|
|
2200
|
+
gesture.timer = this.clock.setTimeout(() => {
|
|
2201
|
+
this.segmenter.end();
|
|
2202
|
+
this.endWheel();
|
|
2203
|
+
}, gesture.endMs);
|
|
2204
|
+
if (gesture.ids.length === 0) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
wheel.preventDefault();
|
|
2208
|
+
if (gesture.kind === 'pan' && gesture.follow) {
|
|
2209
|
+
const f = gesture.follow;
|
|
2210
|
+
if (f.lifted) {
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (this.wheelSegment?.liftNow === true) {
|
|
2214
|
+
// Momentum began: the fingers lifted before the held deltas. The pan
|
|
2215
|
+
// keeps the travel it showed through the undecided stretch. It lets
|
|
2216
|
+
// go here, still moving at the fingers' speed, so its release carries
|
|
2217
|
+
// on from it. The rest of the momentum never moves the pan.
|
|
2218
|
+
this.followProvisionally(gesture, f, time);
|
|
2219
|
+
f.lifted = true;
|
|
2220
|
+
this.arena.ended([
|
|
2221
|
+
{id: gesture.ids[0], x: gesture.x, y: gesture.y, time},
|
|
2222
|
+
]);
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
if (this.wheelSegment?.decaying === true) {
|
|
2226
|
+
// Undecided: the real deltas wait for the splitter while the
|
|
2227
|
+
// fingers' last speed carries the pan on.
|
|
2228
|
+
f.heldX += dx;
|
|
2229
|
+
f.heldY += dy;
|
|
2230
|
+
this.followProvisionally(gesture, f, time);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
// The fingers' own delta; the held ones were theirs too. Their moves
|
|
2234
|
+
// absorb what the pan shows ahead of their travel, or behind it, half a
|
|
2235
|
+
// move at a time. So the pan never steps back, and ends on their travel.
|
|
2236
|
+
const beforeX = f.trueX - f.heldX;
|
|
2237
|
+
const beforeY = f.trueY - f.heldY;
|
|
2238
|
+
const debtX = absorbedDebt(gesture.x - beforeX, dx);
|
|
2239
|
+
const debtY = absorbedDebt(gesture.y - beforeY, dy);
|
|
2240
|
+
const trueX = beforeX - dx;
|
|
2241
|
+
const trueY = beforeY - dy;
|
|
2242
|
+
const elapsed = time - f.lastMove;
|
|
2243
|
+
if (elapsed > 0) {
|
|
2244
|
+
f.speedX = (trueX - f.trueX) / elapsed;
|
|
2245
|
+
f.speedY = (trueY - f.trueY) / elapsed;
|
|
2246
|
+
}
|
|
2247
|
+
f.trueX = trueX;
|
|
2248
|
+
f.trueY = trueY;
|
|
2249
|
+
f.heldX = 0;
|
|
2250
|
+
f.heldY = 0;
|
|
2251
|
+
f.lastMove = time;
|
|
2252
|
+
gesture.x = trueX + debtX;
|
|
2253
|
+
gesture.y = trueY + debtY;
|
|
2254
|
+
f.baseX = gesture.x;
|
|
2255
|
+
f.baseY = gesture.y;
|
|
2256
|
+
this.arena.moved([
|
|
2257
|
+
{id: gesture.ids[0], x: gesture.x, y: gesture.y, time},
|
|
2258
|
+
]);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
if (gesture.kind === 'pan') {
|
|
2262
|
+
gesture.x -= dx;
|
|
2263
|
+
gesture.y -= dy;
|
|
2264
|
+
this.arena.moved([
|
|
2265
|
+
{id: gesture.ids[0], x: gesture.x, y: gesture.y, time},
|
|
2266
|
+
]);
|
|
2267
|
+
} else {
|
|
2268
|
+
gesture.span *= Math.exp(-dy / 100);
|
|
2269
|
+
this.arena.moved(this.pinchContacts(gesture, time));
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
|
|
2273
|
+
/**
|
|
2274
|
+
* Move a followed pan through the splitter's undecided stretch. It moves on
|
|
2275
|
+
* from where it was shown at the fingers' last move, at their last speed.
|
|
2276
|
+
* It never passes what that speed projects for the time since.
|
|
2277
|
+
*/
|
|
2278
|
+
private followProvisionally(
|
|
2279
|
+
gesture: WheelGesture,
|
|
2280
|
+
f: WheelFollow,
|
|
2281
|
+
time: number,
|
|
2282
|
+
): void {
|
|
2283
|
+
const elapsed = Math.max(0, time - f.lastMove);
|
|
2284
|
+
const x = f.baseX + f.speedX * elapsed;
|
|
2285
|
+
const y = f.baseY + f.speedY * elapsed;
|
|
2286
|
+
if (x === gesture.x && y === gesture.y) {
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
gesture.x = x;
|
|
2290
|
+
gesture.y = y;
|
|
2291
|
+
this.arena.moved([{id: gesture.ids[0], x, y, time}]);
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
/** Start a wheel gesture: who takes it, decided once, or nobody. */
|
|
2295
|
+
private beginWheel(
|
|
2296
|
+
wheel: WheelEvent,
|
|
2297
|
+
kind: 'pan' | 'pinch',
|
|
2298
|
+
dx: number,
|
|
2299
|
+
dy: number,
|
|
2300
|
+
time: number,
|
|
2301
|
+
): WheelGesture {
|
|
2302
|
+
const type = scrollTypeOf(wheel);
|
|
2303
|
+
const isHorizontal = Math.abs(dx) > Math.abs(dy);
|
|
2304
|
+
const kept: WheelGesture = {
|
|
2305
|
+
kind,
|
|
2306
|
+
scrollType: type,
|
|
2307
|
+
ids: [],
|
|
2308
|
+
x: wheel.clientX,
|
|
2309
|
+
y: wheel.clientY,
|
|
2310
|
+
span: WHEEL_PINCH_SPAN,
|
|
2311
|
+
last: time,
|
|
2312
|
+
endMs: wheelEndMs(kind, type, isHorizontal),
|
|
2313
|
+
timer: null,
|
|
2314
|
+
takers: [],
|
|
2315
|
+
};
|
|
2316
|
+
// A contact already down keeps its gesture; the wheel stays the page's.
|
|
2317
|
+
// So does a first wheel another handler already prevented, or one that
|
|
2318
|
+
// cannot be cancelled: no recognizer could take it from the page.
|
|
2319
|
+
if (
|
|
2320
|
+
this.arena.touches.length > 0 ||
|
|
2321
|
+
(dx === 0 && dy === 0) ||
|
|
2322
|
+
wheel.defaultPrevented ||
|
|
2323
|
+
!wheel.cancelable
|
|
2324
|
+
) {
|
|
2325
|
+
return kept;
|
|
2326
|
+
}
|
|
2327
|
+
// Every scroller under the pointer, the page included, as a finger's
|
|
2328
|
+
// probe reads them, without touch-action, which a wheel ignores.
|
|
2329
|
+
// What the wheel landed on: at a shadow host's listener the event's
|
|
2330
|
+
// target is the host, and a scroller inside its tree must count.
|
|
2331
|
+
const target = landedOn(wheel);
|
|
2332
|
+
const scroll = createNativeScrollProbe(target, {
|
|
2333
|
+
readsTouchAction: false,
|
|
2334
|
+
});
|
|
2335
|
+
const takers: GestureRecognizer[] = [];
|
|
2336
|
+
for (
|
|
2337
|
+
let node: Node | null = target instanceof Node ? target : null;
|
|
2338
|
+
node != null;
|
|
2339
|
+
node = parentAcrossShadow(node)
|
|
2340
|
+
) {
|
|
2341
|
+
if (!(node instanceof Element)) {
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
for (const recognizer of this.registry.get(node)?.recognizers ?? []) {
|
|
2345
|
+
// A pan follows a trackpad, never a mouse wheel's notches, which
|
|
2346
|
+
// scroll what is under the pointer.
|
|
2347
|
+
if (
|
|
2348
|
+
!recognizer.isEnabled ||
|
|
2349
|
+
!recognizer.claimsTouches ||
|
|
2350
|
+
!recognizer.allowedScrollTypes.has(type) ||
|
|
2351
|
+
internals(recognizer).family !== kind ||
|
|
2352
|
+
(kind === 'pan' && type !== 'continuous')
|
|
2353
|
+
) {
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
if (
|
|
2357
|
+
kind === 'pan' &&
|
|
2358
|
+
(!panTakesMove(recognizer, -dx, -dy) ||
|
|
2359
|
+
this.scrollerTakes(target, node, dx, dy, isHorizontal) ||
|
|
2360
|
+
// A scroll-edge pan only past every scroller's edge.
|
|
2361
|
+
(isScrollEdgePan(recognizer) && scroll.canScroll(-dx, -dy)))
|
|
2362
|
+
) {
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
takers.push(recognizer);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
if (takers.length === 0) {
|
|
2369
|
+
return kept;
|
|
2370
|
+
}
|
|
2371
|
+
const base = {
|
|
2372
|
+
pointerType: 'mouse' as const,
|
|
2373
|
+
target,
|
|
2374
|
+
scroll,
|
|
2375
|
+
scrollType: type,
|
|
2376
|
+
recognizers: takers,
|
|
2377
|
+
consumer: null,
|
|
2378
|
+
};
|
|
2379
|
+
const ids =
|
|
2380
|
+
kind === 'pan' ? [this.nextId++] : [this.nextId++, this.nextId++];
|
|
2381
|
+
const gesture: WheelGesture = {...kept, ids, takers};
|
|
2382
|
+
if (kind === 'pan' && type === 'continuous') {
|
|
2383
|
+
gesture.follow = {
|
|
2384
|
+
lifted: false,
|
|
2385
|
+
heldX: 0,
|
|
2386
|
+
heldY: 0,
|
|
2387
|
+
lastMove: time,
|
|
2388
|
+
trueX: kept.x,
|
|
2389
|
+
trueY: kept.y,
|
|
2390
|
+
baseX: kept.x,
|
|
2391
|
+
baseY: kept.y,
|
|
2392
|
+
speedX: 0,
|
|
2393
|
+
speedY: 0,
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
this.arena.began(
|
|
2397
|
+
kind === 'pan'
|
|
2398
|
+
? [{...base, id: ids[0], x: wheel.clientX, y: wheel.clientY, time}]
|
|
2399
|
+
: this.pinchContacts(gesture, time).map(input => ({
|
|
2400
|
+
...base,
|
|
2401
|
+
...input,
|
|
2402
|
+
})),
|
|
2403
|
+
);
|
|
2404
|
+
// Every recognizer it was offered to may decline it (its own
|
|
2405
|
+
// shouldReceiveTouch). Then nobody took the wheel, and it scrolls the
|
|
2406
|
+
// page from its first step, as with no gesture system.
|
|
2407
|
+
if (ids.every(id => this.arena.participantsOf(id).length === 0)) {
|
|
2408
|
+
this.arena.cancelled(ids, time);
|
|
2409
|
+
return kept;
|
|
2410
|
+
}
|
|
2411
|
+
return gesture;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
/**
|
|
2415
|
+
* Whether a scroller between `target` and `boundary` (both included)
|
|
2416
|
+
* scrolls this wheel now, along the wheel's major axis. It uses the edge
|
|
2417
|
+
* test a finger's probe uses (canElementScroll).
|
|
2418
|
+
*/
|
|
2419
|
+
private scrollerTakes(
|
|
2420
|
+
target: unknown,
|
|
2421
|
+
boundary: Element,
|
|
2422
|
+
dx: number,
|
|
2423
|
+
dy: number,
|
|
2424
|
+
isHorizontal: boolean,
|
|
2425
|
+
): boolean {
|
|
2426
|
+
for (
|
|
2427
|
+
let node: Node | null = target instanceof Node ? target : null;
|
|
2428
|
+
node != null;
|
|
2429
|
+
node = parentAcrossShadow(node)
|
|
2430
|
+
) {
|
|
2431
|
+
if (
|
|
2432
|
+
node instanceof Element &&
|
|
2433
|
+
canElementScroll(node, isHorizontal ? -dx : 0, isHorizontal ? 0 : -dy)
|
|
2434
|
+
) {
|
|
2435
|
+
return true;
|
|
2436
|
+
}
|
|
2437
|
+
if (node === boundary) {
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
return false;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
/** A ctrl-wheel pinch's two contacts, `span` apart around its center. */
|
|
2445
|
+
private pinchContacts(
|
|
2446
|
+
gesture: WheelGesture,
|
|
2447
|
+
time: number,
|
|
2448
|
+
): {id: number; x: number; y: number; time: number}[] {
|
|
2449
|
+
return [
|
|
2450
|
+
{id: gesture.ids[0], x: gesture.x - gesture.span / 2, y: gesture.y, time},
|
|
2451
|
+
{id: gesture.ids[1], x: gesture.x + gesture.span / 2, y: gesture.y, time},
|
|
2452
|
+
];
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
/** Whether a recognizer that took this wheel gesture still takes part in it. */
|
|
2456
|
+
private wheelHasTaker(gesture: WheelGesture): boolean {
|
|
2457
|
+
// A cancelled or failed recognizer keeps that state until its contacts
|
|
2458
|
+
// end, which a wheel gesture's do only here.
|
|
2459
|
+
return gesture.takers.some(recognizer => {
|
|
2460
|
+
const {state} = recognizer;
|
|
2461
|
+
return (
|
|
2462
|
+
recognizer.isEnabled &&
|
|
2463
|
+
(state === 'began' ||
|
|
2464
|
+
state === 'changed' ||
|
|
2465
|
+
(state === 'possible' && recognizer.isInvolved()))
|
|
2466
|
+
);
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* Route every wheel on the page to the live wheel gesture, from its claim.
|
|
2472
|
+
* The route is the shared input stream's capture listener on the window,
|
|
2473
|
+
* which runs before any document listener. It is non-passive only while it
|
|
2474
|
+
* is subscribed.
|
|
2475
|
+
*/
|
|
2476
|
+
private latchWheel(): void {
|
|
2477
|
+
const view = this.doc.defaultView;
|
|
2478
|
+
if (this.stopWheelLatch == null && view != null) {
|
|
2479
|
+
const cancelWheel = () => {
|
|
2480
|
+
const ids = this.dropDrivenWheel();
|
|
2481
|
+
if (ids.length > 0) {
|
|
2482
|
+
this.arena.cancelled(ids, this.arena.now());
|
|
2483
|
+
}
|
|
2484
|
+
};
|
|
2485
|
+
const stops = [
|
|
2486
|
+
subscribeInput(view, 'wheel', this.onWheel, {cancels: true}),
|
|
2487
|
+
// A page that loses focus or hides ends a trackpad pan, as it ends
|
|
2488
|
+
// every touch. An element's blur reaches the window's capture phase
|
|
2489
|
+
// too and means nothing here.
|
|
2490
|
+
subscribeInput(view, 'blur', event => {
|
|
2491
|
+
if (!(event.target instanceof view.Node)) {
|
|
2492
|
+
cancelWheel();
|
|
2493
|
+
}
|
|
2494
|
+
}),
|
|
2495
|
+
subscribeInput(view, 'visibilitychange', () => {
|
|
2496
|
+
if (this.doc.visibilityState === 'hidden') {
|
|
2497
|
+
cancelWheel();
|
|
2498
|
+
}
|
|
2499
|
+
}),
|
|
2500
|
+
];
|
|
2501
|
+
this.stopWheelLatch = () => {
|
|
2502
|
+
for (const stop of stops) {
|
|
2503
|
+
stop();
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
/**
|
|
2510
|
+
* End the trackpad pan a recognizer still drives, when the page loses focus
|
|
2511
|
+
* or hides, and return its contacts to cancel. The rest of its stream is
|
|
2512
|
+
* the page's until the stream ends, as a touch the browser takes is. A
|
|
2513
|
+
* ctrl-wheel pinch keeps its stream, so the page never zooms for it. A
|
|
2514
|
+
* gesture the page keeps stays as it is. So does a pan its lift released:
|
|
2515
|
+
* its momentum stays latched and prevented until the stream ends.
|
|
2516
|
+
*/
|
|
2517
|
+
private dropDrivenWheel(): ReadonlyArray<number> {
|
|
2518
|
+
const gesture = this.wheel;
|
|
2519
|
+
if (
|
|
2520
|
+
gesture == null ||
|
|
2521
|
+
gesture.kind !== 'pan' ||
|
|
2522
|
+
gesture.ids.length === 0 ||
|
|
2523
|
+
gesture.follow?.lifted === true
|
|
2524
|
+
) {
|
|
2525
|
+
return [];
|
|
2526
|
+
}
|
|
2527
|
+
const kept: WheelGesture = {...gesture, ids: [], takers: []};
|
|
2528
|
+
delete kept.follow;
|
|
2529
|
+
this.wheel = kept;
|
|
2530
|
+
this.stopWheelLatch?.();
|
|
2531
|
+
this.stopWheelLatch = null;
|
|
2532
|
+
return gesture.ids;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
/**
|
|
2536
|
+
* End the wheel gesture in progress. On its own timer, it ends its end
|
|
2537
|
+
* window after its last wheel. A taker that leaves (a detach, a disable)
|
|
2538
|
+
* passes that last wheel's time. An input that ends it sooner (a press,
|
|
2539
|
+
* the next gesture's first wheel) passes its own time. Both times are on
|
|
2540
|
+
* the wheels' time base (eventTime), never the host's clock, which need
|
|
2541
|
+
* not share it.
|
|
2542
|
+
*/
|
|
2543
|
+
private endWheel(endTime?: number): void {
|
|
2544
|
+
const gesture = this.wheel;
|
|
2545
|
+
this.stopWheelLatch?.();
|
|
2546
|
+
this.stopWheelLatch = null;
|
|
2547
|
+
if (gesture == null) {
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
this.wheel = null;
|
|
2551
|
+
if (gesture.timer != null) {
|
|
2552
|
+
this.clock.clearTimeout(gesture.timer);
|
|
2553
|
+
}
|
|
2554
|
+
if (gesture.ids.length === 0 || gesture.follow?.lifted) {
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
// Fingers that stop without a lift end on their travel, where it runs
|
|
2558
|
+
// ahead of what the pan shows. What the undecided stretch showed past it
|
|
2559
|
+
// stays, as a step back would show.
|
|
2560
|
+
if (gesture.follow != null) {
|
|
2561
|
+
const f = gesture.follow;
|
|
2562
|
+
const x = ahead(gesture.x, f.trueX - f.heldX, f.speedX);
|
|
2563
|
+
const y = ahead(gesture.y, f.trueY - f.heldY, f.speedY);
|
|
2564
|
+
if (x !== gesture.x || y !== gesture.y) {
|
|
2565
|
+
gesture.x = x;
|
|
2566
|
+
gesture.y = y;
|
|
2567
|
+
this.arena.moved([{id: gesture.ids[0], x, y, time: gesture.last}]);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
// A trackpad scroll whose fingers stop releases still, past the
|
|
2571
|
+
// velocity's stillness interval. A pinch releases 100 ms after its last
|
|
2572
|
+
// wheel, and a lift released the pan already. The release is never later
|
|
2573
|
+
// than the input that ends it (a press, the next gesture's first wheel).
|
|
2574
|
+
// The arena's clock only moves forward, so a later stamp would move that
|
|
2575
|
+
// input's times too.
|
|
2576
|
+
const end = gesture.last + gesture.endMs;
|
|
2577
|
+
const time =
|
|
2578
|
+
endTime == null ? end : Math.max(gesture.last, Math.min(end, endTime));
|
|
2579
|
+
this.arena.ended(
|
|
2580
|
+
gesture.kind === 'pan'
|
|
2581
|
+
? [{id: gesture.ids[0], x: gesture.x, y: gesture.y, time}]
|
|
2582
|
+
: this.pinchContacts(gesture, time),
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
1830
2586
|
/** Undo `lockSelection` for a contact that ended; idempotent. */
|
|
1831
2587
|
private unlockSelection(id: number): void {
|
|
1832
2588
|
this.lockedBy.get(id)?.();
|
|
@@ -1869,6 +2625,7 @@ export class GestureHost {
|
|
|
1869
2625
|
}
|
|
1870
2626
|
|
|
1871
2627
|
private readonly onTouchStart = (event: TouchEvent): void => {
|
|
2628
|
+
this.endWheel(eventTime(event));
|
|
1872
2629
|
this.noteFingersDown(event);
|
|
1873
2630
|
// Where the host reads a finger from its pointer events (no ontouchstart,
|
|
1874
2631
|
// as after touch emulation turns on), its Touch Events would track it twice.
|
|
@@ -2554,6 +3311,7 @@ export class GestureHost {
|
|
|
2554
3311
|
// --- Pointer events --------------------------------------------------------
|
|
2555
3312
|
|
|
2556
3313
|
private readonly onPointerDown = (event: PointerEvent): void => {
|
|
3314
|
+
this.endWheel(eventTime(event));
|
|
2557
3315
|
// A new press: whatever click is still owed to the last one is not this.
|
|
2558
3316
|
disarmClickGuard(this.doc);
|
|
2559
3317
|
if (!isPointerKind(event.pointerType)) {
|
|
@@ -3024,6 +3782,8 @@ export class GestureHost {
|
|
|
3024
3782
|
this.unpairedTouches.clear();
|
|
3025
3783
|
this.lastFed.clear();
|
|
3026
3784
|
this.pointerFed.clear();
|
|
3785
|
+
// A trackpad pan a recognizer still drives is cancelled with them.
|
|
3786
|
+
ids.push(...this.dropDrivenWheel());
|
|
3027
3787
|
if (ids.length > 0) {
|
|
3028
3788
|
this.arena.cancelled(ids, time);
|
|
3029
3789
|
}
|