@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
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
GestureTouch,
|
|
37
37
|
Point,
|
|
38
38
|
PointerKind,
|
|
39
|
+
ScrollType,
|
|
39
40
|
} from './types';
|
|
40
41
|
import {DEFAULT_ENVIRONMENT} from './types';
|
|
41
42
|
|
|
@@ -81,6 +82,16 @@ export interface GestureRecognizerOptions<R = GestureRecognizer> {
|
|
|
81
82
|
delaysTouchesEnded?: boolean;
|
|
82
83
|
/** Pointer kinds this recognizer listens to (UIKit's `allowedTouchTypes`). @default all */
|
|
83
84
|
allowedPointerTypes?: ReadonlyArray<PointerKind>;
|
|
85
|
+
/**
|
|
86
|
+
* Scroll input this recognizer takes, as UIKit's `allowedScrollTypesMask`.
|
|
87
|
+
* A pan follows a trackpad's two-finger scroll (`continuous`), never a
|
|
88
|
+
* mouse wheel's notches. It moves as a contact with the content, along its
|
|
89
|
+
* own axis, after the scrollers under the pointer. A pinch takes a
|
|
90
|
+
* ctrl-wheel of either, as a trackpad pinches.
|
|
91
|
+
* @default none; a pinch takes both, the screen-edge pan none whatever a
|
|
92
|
+
* caller passes
|
|
93
|
+
*/
|
|
94
|
+
allowedScrollTypes?: ReadonlyArray<ScrollType>;
|
|
84
95
|
/** Ignore a touch of a different kind than the ones already tracked. @default true */
|
|
85
96
|
requiresExclusiveTouchType?: boolean;
|
|
86
97
|
/**
|
|
@@ -202,6 +213,8 @@ export abstract class GestureRecognizer {
|
|
|
202
213
|
delaysTouchesBegan: boolean;
|
|
203
214
|
delaysTouchesEnded: boolean;
|
|
204
215
|
allowedPointerTypes: ReadonlySet<PointerKind> | null;
|
|
216
|
+
/** See GestureRecognizerOptions.allowedScrollTypes. */
|
|
217
|
+
readonly allowedScrollTypes: ReadonlySet<ScrollType>;
|
|
205
218
|
requiresExclusiveTouchType: boolean;
|
|
206
219
|
/**
|
|
207
220
|
* The element (or any object) the recognizer is attached to. The DOM host
|
|
@@ -247,6 +260,9 @@ export abstract class GestureRecognizer {
|
|
|
247
260
|
options.allowedPointerTypes == null
|
|
248
261
|
? null
|
|
249
262
|
: new Set(options.allowedPointerTypes);
|
|
263
|
+
this.allowedScrollTypes = new Set(
|
|
264
|
+
options.allowedScrollTypes ?? this.defaultScrollTypes,
|
|
265
|
+
);
|
|
250
266
|
this.requiresExclusiveTouchType =
|
|
251
267
|
options.requiresExclusiveTouchType ?? true;
|
|
252
268
|
}
|
|
@@ -479,6 +495,11 @@ export abstract class GestureRecognizer {
|
|
|
479
495
|
/** Clear per-gesture state before the next attempt. */
|
|
480
496
|
protected reset(): void {}
|
|
481
497
|
|
|
498
|
+
/** The scroll types taken when the options name none (UIKit's: none). */
|
|
499
|
+
protected get defaultScrollTypes(): ReadonlyArray<ScrollType> {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
|
|
482
503
|
/**
|
|
483
504
|
* Whether a touch may join. The base checks enabled, pointer kind and
|
|
484
505
|
* exclusivity of kind; subclasses narrow it (a one-finger pan ignores a
|
|
@@ -488,6 +509,13 @@ export abstract class GestureRecognizer {
|
|
|
488
509
|
if (!this.isEnabled) {
|
|
489
510
|
return false;
|
|
490
511
|
}
|
|
512
|
+
if (touch.scrollType != null) {
|
|
513
|
+
// Scroll input is no pointer: only the scroll types allowed count.
|
|
514
|
+
return (
|
|
515
|
+
this.allowedScrollTypes.has(touch.scrollType) &&
|
|
516
|
+
(this.options.shouldReceiveTouch?.(this, touch) ?? true)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
491
519
|
if (
|
|
492
520
|
this.allowedPointerTypes != null &&
|
|
493
521
|
!this.allowedPointerTypes.has(touch.pointerType)
|
|
@@ -73,6 +73,14 @@ export interface PanGestureRecognizerOptions extends GestureRecognizerOptions<Pa
|
|
|
73
73
|
* at the start only (UIScrollView's directional lock).
|
|
74
74
|
*/
|
|
75
75
|
lockAxis?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* A trackpad's two-finger scroll (`continuous`) drives the pan where its
|
|
78
|
+
* owner allows it. It moves as a contact with the content, along its own
|
|
79
|
+
* axis, after the scrollers under the pointer. A mouse wheel's notches
|
|
80
|
+
* never drive a pan; they scroll what is under the pointer.
|
|
81
|
+
* @default [] (none)
|
|
82
|
+
*/
|
|
83
|
+
allowedScrollTypes?: ReadonlyArray<'continuous'>;
|
|
76
84
|
/** Begin only when the native scrollers under the touch are at the edge. */
|
|
77
85
|
scrollEdge?: boolean;
|
|
78
86
|
}
|
|
@@ -27,7 +27,12 @@ import {
|
|
|
27
27
|
type GestureRecognizerOptions,
|
|
28
28
|
type NativeTouchBehavior,
|
|
29
29
|
} from './GestureRecognizer';
|
|
30
|
-
import type {
|
|
30
|
+
import type {
|
|
31
|
+
GestureEventContext,
|
|
32
|
+
GestureTouch,
|
|
33
|
+
Point,
|
|
34
|
+
ScrollType,
|
|
35
|
+
} from './types';
|
|
31
36
|
import {VelocityTracker} from './VelocityTracker';
|
|
32
37
|
|
|
33
38
|
export interface PinchGestureRecognizerOptions extends GestureRecognizerOptions<PinchGestureRecognizer> {
|
|
@@ -51,6 +56,16 @@ export class PinchGestureRecognizer extends GestureRecognizer {
|
|
|
51
56
|
this.hysteresis = options.hysteresis ?? UIKIT_PINCH.hysteresis.value;
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
/** @internal The host's brand: a ctrl-wheel drives a pinch (recognizerInternals). */
|
|
60
|
+
protected get family(): 'pinch' {
|
|
61
|
+
return 'pinch';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A ctrl-wheel drives a pinch, from a trackpad or a mouse. */
|
|
65
|
+
protected override get defaultScrollTypes(): ReadonlyArray<ScrollType> {
|
|
66
|
+
return ['continuous', 'discrete'];
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
/** Current span over the span the fingers started at. */
|
|
55
70
|
get scale(): number {
|
|
56
71
|
return this.scaleValue;
|
package/src/gestures/README.md
CHANGED
|
@@ -452,6 +452,69 @@ anywhere (often on a plain row no recognizer is on) fails a pan limited to
|
|
|
452
452
|
one finger, so the two fingers zoom the page (WCAG 1.4.4) instead of the pan
|
|
453
453
|
claiming them midway.
|
|
454
454
|
|
|
455
|
+
## Wheel and trackpad
|
|
456
|
+
|
|
457
|
+
A recognizer takes scroll input only where it allows that scroll type
|
|
458
|
+
(`allowedScrollTypes`, UIKit's `allowedScrollTypesMask`): `continuous`
|
|
459
|
+
for a trackpad's two-finger scroll or pinch, `discrete` for a mouse wheel's
|
|
460
|
+
notches. A pan follows a trackpad, never a mouse wheel (`spec:AST-094` FR43,
|
|
461
|
+
DEC-24): it allows only `continuous`, and none by default, so it takes a
|
|
462
|
+
trackpad's scroll only where its owner allows it, its element then getting
|
|
463
|
+
the one wheel listener, and a mouse wheel's notches always scroll what is
|
|
464
|
+
under the pointer. A pinch allows both, for a ctrl-wheel from a trackpad or
|
|
465
|
+
a mouse. The screen-edge pan takes none, as it takes no mouse, trackpad or
|
|
466
|
+
pen (FR43). A control's thumb, a slider's or a switch's, tracks as a control,
|
|
467
|
+
not a pan, and takes no wheel, so a page scroll over it never changes its
|
|
468
|
+
value (DEC-23). The web reports no wheel phases or device, so the deltas
|
|
469
|
+
tell, and what they cannot tell apart reads as a mouse wheel: a line- or
|
|
470
|
+
page-mode delta; a scroll with Shift held (Shift+wheel); a pixel delta a hair
|
|
471
|
+
off a whole pixel (a mouse wheel on Windows); a scroll with no legacy `wheelDelta` (an engine that
|
|
472
|
+
sends none); a delta that is not a third of its legacy `wheelDelta`; or a 120
|
|
473
|
+
tick over a 40 px delta. A trackpad's precise deltas, a third of their legacy
|
|
474
|
+
`wheelDelta` (Chromium and WebKit), read as a trackpad.
|
|
475
|
+
|
|
476
|
+
A wheel gesture drives a pan as a contact moving with the content, along the
|
|
477
|
+
pan's own axis, and only once every scroller between the pointer and the
|
|
478
|
+
pan's element is at its edge that way, decided at the gesture's first wheel:
|
|
479
|
+
a wheel the pan does not take is never prevented. A watcher
|
|
480
|
+
(`claimsTouches: false`) never takes one, and a gesture whose first wheel
|
|
481
|
+
another handler already prevented, or that cannot be cancelled, stays the
|
|
482
|
+
page's. A scroll-edge pan (a pull to
|
|
483
|
+
refresh) takes one only if no scroller under the pointer could scroll for it,
|
|
484
|
+
its own scroll owner and the page included, as its finger's rule reads them
|
|
485
|
+
(`spec:AST-094` FR19); `touch-action` narrows only a finger's pan, so a wheel
|
|
486
|
+
reads every scroller past it. A ctrl-wheel (a trackpad
|
|
487
|
+
pinch, or Control and a mouse wheel) drives a pinch, whose scale follows
|
|
488
|
+
`exp(-deltaY / 100)`; a pinch takes it by default and ends 100 ms after its
|
|
489
|
+
last wheel. While the fingers move, a trackpad scroll ends 180 ms after its
|
|
490
|
+
last wheel vertically and 220 ms horizontally, whoever keeps it (a pan, a
|
|
491
|
+
scroller under the pointer, or the page), as a slow drag pauses between
|
|
492
|
+
wheels (DEC-24). A gesture's release is never stamped
|
|
493
|
+
later than an input that ends it sooner (FR16).
|
|
494
|
+
|
|
495
|
+
The web reports no trackpad contact or phase either, so one splitter reads
|
|
496
|
+
every trackpad stream (`wheelSegmenter.ts`, DEC-24): at the fourth or later
|
|
497
|
+
delta of a shrinking run, once the run has lost a fifth of its size, the
|
|
498
|
+
fingers lifted and momentum began. Until it decides,
|
|
499
|
+
the shrinking deltas wait, and a pan it follows moves on at the fingers' last
|
|
500
|
+
speed, never past what that speed projects. At the lift the pan keeps that
|
|
501
|
+
travel and lets go there, still moving at that speed, and the momentum after
|
|
502
|
+
it never moves it; when the fingers carry on instead, their moves absorb
|
|
503
|
+
what the pan showed past their travel, or short of it, so it never steps
|
|
504
|
+
back and ends on their travel. Fingers that stop release still, once the end
|
|
505
|
+
time passes. In momentum, a reversal, a delta that grows
|
|
506
|
+
against the decay, or a 50 ms pause starts a new gesture. A finger, mouse or
|
|
507
|
+
pen that comes down ends it, and a wheel claim keeps the page's text
|
|
508
|
+
selection. A gesture a
|
|
509
|
+
recognizer took is latched to it, as a browser latches a scroller: while it
|
|
510
|
+
lasts, the host hears every step first, through the shared input stream's
|
|
511
|
+
window-capture listener, wherever it lands, so a pan whose element moves from
|
|
512
|
+
under a still pointer keeps the gesture. A page that loses focus or hides
|
|
513
|
+
cancels a trackpad pan a recognizer still drives, as it cancels every touch;
|
|
514
|
+
the rest of that stream is the page's. A ctrl-wheel pinch keeps its stream,
|
|
515
|
+
so the page never zooms for it, and a pan its lift released keeps its
|
|
516
|
+
momentum latched until the stream ends.
|
|
517
|
+
|
|
455
518
|
## Time
|
|
456
519
|
|
|
457
520
|
Every input carries its own timestamp (`event.timeStamp`), and timers are
|
|
@@ -71,12 +71,12 @@ interface EdgeLayout {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* A screen-edge pan is a finger's: it takes no `allowedPointerTypes`
|
|
75
|
-
*
|
|
74
|
+
* A screen-edge pan is a finger's: it takes no `allowedPointerTypes` and no
|
|
75
|
+
* `allowedScrollTypes`, so neither a mouse, a trackpad nor a pen drives it.
|
|
76
76
|
*/
|
|
77
77
|
export interface ScreenEdgePanGestureRecognizerOptions extends Omit<
|
|
78
78
|
PanGestureRecognizerOptions,
|
|
79
|
-
'direction' | 'allowedPointerTypes'
|
|
79
|
+
'direction' | 'allowedPointerTypes' | 'allowedScrollTypes'
|
|
80
80
|
> {
|
|
81
81
|
/** The edges a touch may start from. @default ['leading'] */
|
|
82
82
|
edges?: ReadonlyArray<ScreenEdge>;
|
|
@@ -109,9 +109,11 @@ export class ScreenEdgePanGestureRecognizer extends PanGestureRecognizer {
|
|
|
109
109
|
maximumNumberOfTouches: 1,
|
|
110
110
|
hysteresis: UIKIT_SCREEN_EDGE_PAN.hysteresis.value,
|
|
111
111
|
...options,
|
|
112
|
-
// A finger's only:
|
|
113
|
-
// pan stays off
|
|
112
|
+
// A finger's only (`spec:AST-094` FR43). Under a mouse, a trackpad or
|
|
113
|
+
// a pen, the screen-edge pan stays off, whatever a caller passes. It
|
|
114
|
+
// takes no wheel or trackpad scrolling.
|
|
114
115
|
allowedPointerTypes: ['touch'],
|
|
116
|
+
allowedScrollTypes: [],
|
|
115
117
|
});
|
|
116
118
|
this.edges = options.edges ?? ['leading'];
|
|
117
119
|
this.edgeRegion =
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @file nativeScroll.ts
|
|
5
5
|
* @input An element a touch landed on
|
|
6
6
|
* @output Exports createNativeScrollProbe (the NativeScrollProbe a host gives
|
|
7
|
-
* each touch
|
|
7
|
+
* each touch, and each wheel gesture without the touch-action narrowing)
|
|
8
|
+
* and parseTouchAction
|
|
8
9
|
* @position DOM half of the scroll coordination in packages/core/src/gestures
|
|
9
10
|
*
|
|
10
11
|
* Answers one question for a recognizer without letting it touch the DOM:
|
|
@@ -82,7 +83,7 @@ interface Chain {
|
|
|
82
83
|
readonly allowsPan: boolean;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
function buildChain(target: Element): Chain {
|
|
86
|
+
function buildChain(target: Element, readsTouchAction: boolean): Chain {
|
|
86
87
|
const doc = target.ownerDocument;
|
|
87
88
|
const view = doc.defaultView;
|
|
88
89
|
if (view == null) {
|
|
@@ -103,9 +104,11 @@ function buildChain(target: Element): Chain {
|
|
|
103
104
|
continue;
|
|
104
105
|
}
|
|
105
106
|
const style = view.getComputedStyle(node);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if (readsTouchAction) {
|
|
108
|
+
const pans = parseTouchAction(style.touchAction || 'auto');
|
|
109
|
+
allowX &&= pans.x;
|
|
110
|
+
allowY &&= pans.y;
|
|
111
|
+
}
|
|
109
112
|
const isRoot = node === root || node === doc.documentElement;
|
|
110
113
|
if (isRoot) {
|
|
111
114
|
reachedRoot = true;
|
|
@@ -196,7 +199,10 @@ function canMoveY(element: Element, dy: number): boolean {
|
|
|
196
199
|
element.scrollHeight - element.clientHeight - EDGE_TOLERANCE + 0.5;
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
function canMoveX(
|
|
202
|
+
function canMoveX(
|
|
203
|
+
entry: Pick<ChainEntry, 'element' | 'isRtl'>,
|
|
204
|
+
dx: number,
|
|
205
|
+
): boolean {
|
|
200
206
|
const {element} = entry;
|
|
201
207
|
const overflow = element.scrollWidth - element.clientWidth;
|
|
202
208
|
if (overflow <= EDGE_TOLERANCE) {
|
|
@@ -210,13 +216,47 @@ function canMoveX(entry: ChainEntry, dx: number): boolean {
|
|
|
210
216
|
: element.scrollLeft < max - EDGE_TOLERANCE + 0.5;
|
|
211
217
|
}
|
|
212
218
|
|
|
213
|
-
/**
|
|
214
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Whether `element` itself scrolls natively now for a finger moving by
|
|
221
|
+
* (dx, dy). It uses the probe's own test of each scroller: overflow that
|
|
222
|
+
* scrolls on that axis, and room past the edge tolerance. The position must
|
|
223
|
+
* also be short of the end it moves toward, a right-to-left range included.
|
|
224
|
+
* A wheel moves the content the other way, so pass its deltas negated.
|
|
225
|
+
*/
|
|
226
|
+
export function canElementScroll(
|
|
227
|
+
element: Element,
|
|
228
|
+
dx: number,
|
|
229
|
+
dy: number,
|
|
230
|
+
): boolean {
|
|
231
|
+
const view = element.ownerDocument.defaultView;
|
|
232
|
+
if (view == null) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const style = view.getComputedStyle(element);
|
|
236
|
+
if (dy !== 0 && SCROLLABLE.has(style.overflowY) && canMoveY(element, dy)) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
return (
|
|
240
|
+
dx !== 0 &&
|
|
241
|
+
SCROLLABLE.has(style.overflowX) &&
|
|
242
|
+
canMoveX({element, isRtl: isRtlElement(element as HTMLElement)}, dx)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* A probe of the native scrollers under `target`. A wheel scrolls whatever
|
|
248
|
+
* `touch-action` says, which narrows only a finger's pan: a wheel's probe
|
|
249
|
+
* passes `readsTouchAction: false`.
|
|
250
|
+
*/
|
|
251
|
+
export function createNativeScrollProbe(
|
|
252
|
+
target: unknown,
|
|
253
|
+
{readsTouchAction = true}: {readsTouchAction?: boolean} = {},
|
|
254
|
+
): NativeScrollProbe {
|
|
215
255
|
let chain: Chain | null = null;
|
|
216
256
|
const getChain = (): ChainEntry[] => {
|
|
217
257
|
chain ??=
|
|
218
258
|
target instanceof Element
|
|
219
|
-
? buildChain(target)
|
|
259
|
+
? buildChain(target, readsTouchAction)
|
|
220
260
|
: {entries: [], allowsPan: false};
|
|
221
261
|
return chain.entries;
|
|
222
262
|
};
|
|
@@ -20,7 +20,12 @@
|
|
|
20
20
|
|
|
21
21
|
import type {GestureArena} from './GestureArena';
|
|
22
22
|
import type {GestureRecognizer} from './GestureRecognizer';
|
|
23
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
GestureEventContext,
|
|
25
|
+
GestureState,
|
|
26
|
+
GestureTouch,
|
|
27
|
+
Point,
|
|
28
|
+
} from './types';
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
31
|
* The kinds of recognizer the host and touch-action treat specially. Each of
|
|
@@ -28,12 +33,14 @@ import type {GestureEventContext, GestureState, GestureTouch} from './types';
|
|
|
28
33
|
* never imports a recognizer class: a page ships only the recognizers it
|
|
29
34
|
* constructs.
|
|
30
35
|
*/
|
|
31
|
-
export type RecognizerFamily = 'tap' | 'longPress' | 'pan';
|
|
36
|
+
export type RecognizerFamily = 'tap' | 'longPress' | 'pan' | 'pinch';
|
|
32
37
|
|
|
33
38
|
/** The members of a recognizer only the gesture system calls. */
|
|
34
39
|
export interface RecognizerInternals {
|
|
35
40
|
/** The recognizer's family, when the host treats it specially. */
|
|
36
41
|
readonly family?: RecognizerFamily;
|
|
42
|
+
/** A pan's direction rule for a translation, `axisRatio` included. */
|
|
43
|
+
classify?(translation: Point): 'begin' | 'fail' | 'wait';
|
|
37
44
|
arena: GestureArena | null;
|
|
38
45
|
pending: 'began' | 'ended' | null;
|
|
39
46
|
readonly seenTouchIds: Set<number>;
|
package/src/gestures/types.ts
CHANGED
|
@@ -21,6 +21,14 @@ export type GestureState =
|
|
|
21
21
|
/** What made the contact. A pen on an iPad is `pen`, not `touch`. */
|
|
22
22
|
export type PointerKind = 'touch' | 'mouse' | 'pen';
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Scroll input a recognizer can take, as UIKit's `UIScrollTypeMask`: a
|
|
26
|
+
* trackpad's two-finger scroll or pinch (`continuous`), or a mouse wheel's
|
|
27
|
+
* notches (`discrete`). A pan takes only a trackpad's; a pinch takes a
|
|
28
|
+
* ctrl-wheel of either.
|
|
29
|
+
*/
|
|
30
|
+
export type ScrollType = 'continuous' | 'discrete';
|
|
31
|
+
|
|
24
32
|
export interface Point {
|
|
25
33
|
readonly x: number;
|
|
26
34
|
readonly y: number;
|
|
@@ -85,6 +93,8 @@ export interface GestureTouch {
|
|
|
85
93
|
readonly target: unknown;
|
|
86
94
|
/** The native scrollers under the contact, when the host knows them. */
|
|
87
95
|
readonly scroll: NativeScrollProbe | null;
|
|
96
|
+
/** Set for a wheel's or a trackpad's scroll input, which is no contact. */
|
|
97
|
+
readonly scrollType?: ScrollType;
|
|
88
98
|
/** The browser took this contact for its own pan or zoom. */
|
|
89
99
|
takenByBrowser: boolean;
|
|
90
100
|
}
|
|
@@ -104,6 +114,8 @@ export interface TouchBeganInput extends TouchInput {
|
|
|
104
114
|
readonly pointerType: PointerKind;
|
|
105
115
|
readonly target: unknown;
|
|
106
116
|
readonly scroll?: NativeScrollProbe | null;
|
|
117
|
+
/** Set for a wheel's or a trackpad's scroll input. */
|
|
118
|
+
readonly scrollType?: ScrollType;
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
/** What a recognizer is told alongside the touches that changed. */
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file wheelSegmenter.ts
|
|
5
|
+
* @input A wheel stream's deltas along one axis, with their event times
|
|
6
|
+
* @output Exports WheelSegmenter: splits the stream into gestures and reads
|
|
7
|
+
* a trackpad's lift (the onset of momentum)
|
|
8
|
+
* @position Read by GestureHost.ts's wheel input: the web reports no
|
|
9
|
+
* trackpad contact or phase, so both are inferred.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type WheelBoundary = 'first' | 'idle' | 'reversal' | 'jump' | 'gap';
|
|
13
|
+
export type WheelPhase = 'finger' | 'momentum';
|
|
14
|
+
|
|
15
|
+
export interface WheelSegment {
|
|
16
|
+
/** Why this event starts a new gesture, or null when it continues one. */
|
|
17
|
+
readonly boundary: WheelBoundary | null;
|
|
18
|
+
readonly phase: WheelPhase;
|
|
19
|
+
/** The gesture's number, counting from 1. */
|
|
20
|
+
readonly gesture: number;
|
|
21
|
+
/** This event is where the fingers lifted (momentum begins with it). */
|
|
22
|
+
readonly liftNow: boolean;
|
|
23
|
+
/** This event shrank, so it may be momentum: hold it until known. */
|
|
24
|
+
readonly decaying: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Momentum arrives at frame rate: a pause this long ends it. */
|
|
28
|
+
const MOMENTUM_GAP_MS = 50;
|
|
29
|
+
/** Shrinking deltas in a row before the lift is read. */
|
|
30
|
+
const LIFT_RUN = 4;
|
|
31
|
+
/** And how much of its size the run has lost by then. */
|
|
32
|
+
const LIFT_DECAY = 0.8;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A trackpad gesture has a finger phase and, after a fling, a momentum phase.
|
|
36
|
+
* In the finger phase, deltas come while the fingers move, with pauses when
|
|
37
|
+
* they move slowly. In the momentum phase, deltas come at frame rate and
|
|
38
|
+
* shrink smoothly. The lift is the start of a run of shrinking deltas that
|
|
39
|
+
* has lost a fifth of its size. In the finger phase, a pause of `idleMs`
|
|
40
|
+
* ends the gesture. In the momentum phase, a reversal, a jump up against the
|
|
41
|
+
* decay, or a pause of 50 ms starts a new gesture. Momentum never pauses,
|
|
42
|
+
* reverses or grows.
|
|
43
|
+
*/
|
|
44
|
+
export class WheelSegmenter {
|
|
45
|
+
gesture = 0;
|
|
46
|
+
phase: WheelPhase = 'finger';
|
|
47
|
+
private last = -Infinity;
|
|
48
|
+
private previous = 0;
|
|
49
|
+
private sign = 0;
|
|
50
|
+
private count = 0;
|
|
51
|
+
private run = 0;
|
|
52
|
+
private runStart = 0;
|
|
53
|
+
|
|
54
|
+
push(time: number, delta: number, idleMs: number): WheelSegment {
|
|
55
|
+
const size = Math.abs(delta);
|
|
56
|
+
const sign = Math.sign(delta);
|
|
57
|
+
const gap = time - this.last;
|
|
58
|
+
let boundary: WheelBoundary | null = null;
|
|
59
|
+
if (this.count === 0) {
|
|
60
|
+
boundary = this.gesture === 0 ? 'first' : 'idle';
|
|
61
|
+
} else if (this.phase === 'finger' && gap >= idleMs) {
|
|
62
|
+
boundary = 'idle';
|
|
63
|
+
} else if (this.phase === 'momentum') {
|
|
64
|
+
if (sign !== 0 && this.sign !== 0 && sign !== this.sign) {
|
|
65
|
+
boundary = 'reversal';
|
|
66
|
+
} else if (gap >= MOMENTUM_GAP_MS) {
|
|
67
|
+
boundary = 'gap';
|
|
68
|
+
} else if (size >= 2 && size > this.previous * 1.3 + 0.5) {
|
|
69
|
+
boundary = 'jump';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (boundary != null) {
|
|
73
|
+
this.gesture += 1;
|
|
74
|
+
this.phase = 'finger';
|
|
75
|
+
this.count = 0;
|
|
76
|
+
this.run = 0;
|
|
77
|
+
}
|
|
78
|
+
let liftNow = false;
|
|
79
|
+
let decaying = false;
|
|
80
|
+
if (this.phase === 'finger' && this.count >= 3 && size > 0) {
|
|
81
|
+
decaying = size <= this.previous * 0.995 && size >= this.previous * 0.5;
|
|
82
|
+
if (decaying) {
|
|
83
|
+
if (this.run === 0) {
|
|
84
|
+
this.runStart = this.previous;
|
|
85
|
+
}
|
|
86
|
+
this.run += 1;
|
|
87
|
+
} else {
|
|
88
|
+
this.run = 0;
|
|
89
|
+
}
|
|
90
|
+
if (this.run >= LIFT_RUN && size <= this.runStart * LIFT_DECAY) {
|
|
91
|
+
this.phase = 'momentum';
|
|
92
|
+
liftNow = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.count += 1;
|
|
96
|
+
this.previous = size;
|
|
97
|
+
if (sign !== 0) {
|
|
98
|
+
this.sign = sign;
|
|
99
|
+
}
|
|
100
|
+
this.last = time;
|
|
101
|
+
return {
|
|
102
|
+
boundary,
|
|
103
|
+
phase: this.phase,
|
|
104
|
+
gesture: this.gesture,
|
|
105
|
+
liftNow,
|
|
106
|
+
decaying: decaying && !liftNow,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The gesture ended on its own (its timer): the next event starts one. */
|
|
111
|
+
end(): void {
|
|
112
|
+
this.count = 0;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/motion/animator.ts
CHANGED
|
@@ -517,6 +517,14 @@ interface StartGroup {
|
|
|
517
517
|
madeInFrame?: boolean;
|
|
518
518
|
/** The task it was made in, with every call made in that task. */
|
|
519
519
|
task?: TaskMark;
|
|
520
|
+
/**
|
|
521
|
+
* Per channel, when its animation's first frame ran without a known start.
|
|
522
|
+
* That frame came too late to stand for the start, and the browser had not
|
|
523
|
+
* told it yet. The compositor starts the animation within about a frame of
|
|
524
|
+
* this time. Until the browser tells its start, a read of the channel takes
|
|
525
|
+
* the curve from here.
|
|
526
|
+
*/
|
|
527
|
+
latest?: Map<Channel, number>;
|
|
520
528
|
/** A later call in the same task that took over its curves. */
|
|
521
529
|
adoptedBy?: StartGroup;
|
|
522
530
|
readonly resolve: (start: number) => void;
|
|
@@ -1070,16 +1078,42 @@ export function createAnimator(
|
|
|
1070
1078
|
};
|
|
1071
1079
|
|
|
1072
1080
|
/**
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
|
|
1076
|
-
|
|
1081
|
+
* A held curve's start once a frame dated its call, as the browser's start
|
|
1082
|
+
* moves it: by that frame's lead over the call's own time.
|
|
1083
|
+
*/
|
|
1084
|
+
const datedFromLatest = (
|
|
1085
|
+
segment: Segment,
|
|
1086
|
+
latest: number | undefined,
|
|
1087
|
+
): {latest: number; start: number} | null =>
|
|
1088
|
+
latest == null || segment.group == null
|
|
1089
|
+
? null
|
|
1090
|
+
: {latest, start: segment.start + latest - segment.group.provisional};
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* The value on screen at `time`. Until the browser reports its start, a
|
|
1094
|
+
* held curve shows its first value. A frame that ran without that start
|
|
1095
|
+
* dates the curve. Without that frame, a later time reads from the
|
|
1096
|
+
* provisional start, so a companion that samples ahead follows the curve's
|
|
1097
|
+
* shape.
|
|
1077
1098
|
*/
|
|
1078
1099
|
const valueAt = (property: MotionProperty, time: number): number => {
|
|
1079
1100
|
const segment = segments.get(property);
|
|
1080
|
-
if (segment != null && isHeld(segment)
|
|
1101
|
+
if (segment != null && isHeld(segment)) {
|
|
1102
|
+
// The compositor starts the call near its first frame. The curve is
|
|
1103
|
+
// dated as the browser's start would date it. A curve that joined by an
|
|
1104
|
+
// older clock keeps its lead over the call's own time.
|
|
1105
|
+
const latest = segment.group?.latest?.get(CHANNEL_OF[property]);
|
|
1106
|
+
const dated = datedFromLatest(segment, latest);
|
|
1107
|
+
if (dated != null) {
|
|
1108
|
+
const elapsed = Math.max(time, dated.latest) - dated.start;
|
|
1109
|
+
return elapsed >= segment.duration
|
|
1110
|
+
? segment.target
|
|
1111
|
+
: segment.at(elapsed);
|
|
1112
|
+
}
|
|
1081
1113
|
// Nothing has moved on screen until the browser starts the call.
|
|
1082
|
-
|
|
1114
|
+
if (time <= now()) {
|
|
1115
|
+
return segment.at(0);
|
|
1116
|
+
}
|
|
1083
1117
|
}
|
|
1084
1118
|
return curveAt(property, time);
|
|
1085
1119
|
};
|
|
@@ -1087,10 +1121,22 @@ export function createAnimator(
|
|
|
1087
1121
|
const velocityAt = (property: MotionProperty, time: number): number => {
|
|
1088
1122
|
const held = segments.get(property);
|
|
1089
1123
|
if (held != null && isHeld(held)) {
|
|
1090
|
-
// Still at its start on screen, but moving in the model
|
|
1091
|
-
// carries the velocity the curve has reached by now
|
|
1092
|
-
// moves all land before the browser starts any keeps its speed.
|
|
1093
|
-
|
|
1124
|
+
// Still at its start on screen, but moving in the model. A retarget
|
|
1125
|
+
// carries the velocity the curve has reached by now. So a flick whose
|
|
1126
|
+
// moves all land before the browser starts any keeps its speed. Past
|
|
1127
|
+
// its first frame, it reads from that frame, as its value does.
|
|
1128
|
+
const dated = datedFromLatest(
|
|
1129
|
+
held,
|
|
1130
|
+
held.group?.latest?.get(CHANNEL_OF[property]),
|
|
1131
|
+
);
|
|
1132
|
+
return held.speed(
|
|
1133
|
+
Math.max(
|
|
1134
|
+
0,
|
|
1135
|
+
dated != null && time > dated.latest
|
|
1136
|
+
? time - dated.start
|
|
1137
|
+
: time - held.start,
|
|
1138
|
+
),
|
|
1139
|
+
);
|
|
1094
1140
|
}
|
|
1095
1141
|
const segment = segmentAt(property, time);
|
|
1096
1142
|
if (segment == null || time < segment.start) {
|
|
@@ -1276,6 +1322,35 @@ export function createAnimator(
|
|
|
1276
1322
|
}
|
|
1277
1323
|
}
|
|
1278
1324
|
const moving = properties.filter(p => segments.has(p));
|
|
1325
|
+
// An explicit call can rebuild a channel while one of its curves still
|
|
1326
|
+
// waits for the browser's start. The animation then joins the current
|
|
1327
|
+
// call, or else that waiting call, with no start of its own.
|
|
1328
|
+
const waiting =
|
|
1329
|
+
callGroup ??
|
|
1330
|
+
moving
|
|
1331
|
+
.map(p => (segments.get(p) as Segment).group)
|
|
1332
|
+
.find(group => group != null && !group.resolved) ??
|
|
1333
|
+
null;
|
|
1334
|
+
const rebuiltAfterFirstFrame = waiting?.framed === true;
|
|
1335
|
+
// The new animation starts at the frame that commits it, from its first
|
|
1336
|
+
// keyframe. That holds for a waiting call it joins after that call's
|
|
1337
|
+
// first frame, and for every waiting call it takes a curve from. So this
|
|
1338
|
+
// channel's reads below, and until that frame, take that first value.
|
|
1339
|
+
const restarted = new Set<StartGroup>();
|
|
1340
|
+
if (rebuiltAfterFirstFrame) {
|
|
1341
|
+
restarted.add(waiting);
|
|
1342
|
+
}
|
|
1343
|
+
if (callGroup != null) {
|
|
1344
|
+
for (const property of moving) {
|
|
1345
|
+
const group = (segments.get(property) as Segment).group;
|
|
1346
|
+
if (group != null && group !== callGroup && !group.resolved) {
|
|
1347
|
+
restarted.add(group);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
for (const group of restarted) {
|
|
1352
|
+
group.latest?.delete(channel);
|
|
1353
|
+
}
|
|
1279
1354
|
if (channel === 'translate') {
|
|
1280
1355
|
// The animation built below starts from the value at `time` (its first
|
|
1281
1356
|
// keyframe); a rebase or a retarget builds a new one, so a new start.
|
|
@@ -1308,15 +1383,6 @@ export function createAnimator(
|
|
|
1308
1383
|
previous?.cancel();
|
|
1309
1384
|
return;
|
|
1310
1385
|
}
|
|
1311
|
-
// A channel rebuilt by an explicit call (set, rebase, a retarget of
|
|
1312
|
-
// another axis) while one of its curves still waits for the browser's
|
|
1313
|
-
// start keeps that start: no explicit start for this animation.
|
|
1314
|
-
const waiting =
|
|
1315
|
-
callGroup ??
|
|
1316
|
-
moving
|
|
1317
|
-
.map(p => (segments.get(p) as Segment).group)
|
|
1318
|
-
.find(group => group != null && !group.resolved) ??
|
|
1319
|
-
null;
|
|
1320
1386
|
// The keyframes start where the animation will: at the waiting call's
|
|
1321
1387
|
// own time, which its start moves to the browser's (resolveGroup), so a
|
|
1322
1388
|
// write later in the task keeps the screen on the model's clock.
|
|
@@ -1431,6 +1497,20 @@ export function createAnimator(
|
|
|
1431
1497
|
waiting.channels.add(channel);
|
|
1432
1498
|
// A cancel before the first frame rejects its ready promise.
|
|
1433
1499
|
(animation as Partial<Animation>).ready?.catch(() => {});
|
|
1500
|
+
if (rebuiltAfterFirstFrame) {
|
|
1501
|
+
// The frame that commits it dates this channel's start again. The
|
|
1502
|
+
// call's other channels keep their own.
|
|
1503
|
+
requestFrame?.(() => {
|
|
1504
|
+
if (
|
|
1505
|
+
!waiting.resolved &&
|
|
1506
|
+
running.get(channel) === animation &&
|
|
1507
|
+
animation.startTime == null &&
|
|
1508
|
+
animation.playState === 'running'
|
|
1509
|
+
) {
|
|
1510
|
+
(waiting.latest ??= new Map()).set(channel, now());
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1434
1514
|
} else {
|
|
1435
1515
|
// One clock: the keyframes were sampled from `time`.
|
|
1436
1516
|
animation.startTime = time;
|
|
@@ -2135,6 +2215,22 @@ export function createAnimator(
|
|
|
2135
2215
|
) {
|
|
2136
2216
|
resolveGroup(group, now());
|
|
2137
2217
|
}
|
|
2218
|
+
// The browser's start is still unread. The compositor starts the call
|
|
2219
|
+
// near this frame, so each moving channel's reads take the curve from
|
|
2220
|
+
// now until the browser tells its start.
|
|
2221
|
+
if (!group.resolved) {
|
|
2222
|
+
for (const channel of group.channels) {
|
|
2223
|
+
const animation = running.get(channel);
|
|
2224
|
+
if (
|
|
2225
|
+
animation != null &&
|
|
2226
|
+
group.animations.includes(animation) &&
|
|
2227
|
+
animation.startTime == null &&
|
|
2228
|
+
animation.playState === 'running'
|
|
2229
|
+
) {
|
|
2230
|
+
(group.latest ??= new Map()).set(channel, now());
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2138
2234
|
});
|
|
2139
2235
|
ready.then(
|
|
2140
2236
|
() => syncGroup(group),
|