react-native-nitro-modal 0.1.4 → 0.2.0

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 (61) hide show
  1. package/README.md +19 -7
  2. package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +72 -21
  3. package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +17 -3
  4. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalPresenter.kt +121 -12
  5. package/android/src/main/java/com/margelo/nitro/nitromodal/PopupPresenter.kt +92 -31
  6. package/lib/module/NitroModal.js +1 -2
  7. package/lib/module/NitroModal.js.map +1 -1
  8. package/lib/module/NitroModalView.js +4 -3
  9. package/lib/module/NitroModalView.js.map +1 -1
  10. package/lib/module/NitroModalView.native.js +3 -0
  11. package/lib/module/NitroModalView.native.js.map +1 -1
  12. package/lib/module/NitroModalView.web.js +129 -0
  13. package/lib/module/NitroModalView.web.js.map +1 -0
  14. package/lib/module/web/AnimatedValue.js +123 -0
  15. package/lib/module/web/AnimatedValue.js.map +1 -0
  16. package/lib/module/web/ModalConfig.js +26 -0
  17. package/lib/module/web/ModalConfig.js.map +1 -0
  18. package/lib/module/web/ModalController.js +279 -0
  19. package/lib/module/web/ModalController.js.map +1 -0
  20. package/lib/module/web/ModalPresenter.js +245 -0
  21. package/lib/module/web/ModalPresenter.js.map +1 -0
  22. package/lib/module/web/PopupPresenter.js +117 -0
  23. package/lib/module/web/PopupPresenter.js.map +1 -0
  24. package/lib/module/web/SheetPresenter.js +469 -0
  25. package/lib/module/web/SheetPresenter.js.map +1 -0
  26. package/lib/module/web/dom.js +125 -0
  27. package/lib/module/web/dom.js.map +1 -0
  28. package/lib/typescript/src/NitroModal.d.ts +2 -2
  29. package/lib/typescript/src/NitroModal.d.ts.map +1 -1
  30. package/lib/typescript/src/NitroModalView.d.ts +2 -1
  31. package/lib/typescript/src/NitroModalView.d.ts.map +1 -1
  32. package/lib/typescript/src/NitroModalView.native.d.ts +1 -0
  33. package/lib/typescript/src/NitroModalView.native.d.ts.map +1 -1
  34. package/lib/typescript/src/NitroModalView.web.d.ts +6 -0
  35. package/lib/typescript/src/NitroModalView.web.d.ts.map +1 -0
  36. package/lib/typescript/src/web/AnimatedValue.d.ts +48 -0
  37. package/lib/typescript/src/web/AnimatedValue.d.ts.map +1 -0
  38. package/lib/typescript/src/web/ModalConfig.d.ts +24 -0
  39. package/lib/typescript/src/web/ModalConfig.d.ts.map +1 -0
  40. package/lib/typescript/src/web/ModalController.d.ts +65 -0
  41. package/lib/typescript/src/web/ModalController.d.ts.map +1 -0
  42. package/lib/typescript/src/web/ModalPresenter.d.ts +98 -0
  43. package/lib/typescript/src/web/ModalPresenter.d.ts.map +1 -0
  44. package/lib/typescript/src/web/PopupPresenter.d.ts +20 -0
  45. package/lib/typescript/src/web/PopupPresenter.d.ts.map +1 -0
  46. package/lib/typescript/src/web/SheetPresenter.d.ts +65 -0
  47. package/lib/typescript/src/web/SheetPresenter.d.ts.map +1 -0
  48. package/lib/typescript/src/web/dom.d.ts +40 -0
  49. package/lib/typescript/src/web/dom.d.ts.map +1 -0
  50. package/package.json +3 -2
  51. package/src/NitroModal.tsx +3 -4
  52. package/src/NitroModalView.native.tsx +4 -0
  53. package/src/NitroModalView.tsx +9 -4
  54. package/src/NitroModalView.web.tsx +138 -0
  55. package/src/web/AnimatedValue.ts +189 -0
  56. package/src/web/ModalConfig.ts +57 -0
  57. package/src/web/ModalController.ts +305 -0
  58. package/src/web/ModalPresenter.ts +298 -0
  59. package/src/web/PopupPresenter.ts +129 -0
  60. package/src/web/SheetPresenter.ts +582 -0
  61. package/src/web/dom.ts +154 -0
@@ -0,0 +1,582 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import type { SheetDetent } from '../NitroModal.nitro';
4
+ import { AnimatedValue, INSTANT, type Motion } from './AnimatedValue';
5
+ import {
6
+ findVerticalScroller,
7
+ isTextInput,
8
+ prefersReducedMotion,
9
+ suppressNextClick,
10
+ type Insets,
11
+ type Size,
12
+ } from './dom';
13
+ import type { ModalConfig } from './ModalConfig';
14
+ import { ModalPresenter, type PresenterListener } from './ModalPresenter';
15
+
16
+ /** Gap kept above the sheet at its tallest. */
17
+ const TOP_GAP = 10;
18
+ /**
19
+ * Extra sheet height below the viewport so it stays attached to the bottom
20
+ * edge while rubber-banding upward.
21
+ */
22
+ const OVERSCROLL = 400;
23
+ const DEFAULT_CORNER_RADIUS = 16;
24
+ const MAX_WIDTH = 640;
25
+ /** Mouse movement before a press turns into a drag. */
26
+ const POINTER_SLOP = 6;
27
+ /** Pointer samples used for the release velocity. */
28
+ const VELOCITY_WINDOW_MS = 100;
29
+
30
+ const ENTER: Motion = { type: 'spring', response: 0.4, dampingRatio: 0.9 };
31
+ const SETTLE: Motion = { type: 'spring', response: 0.4, dampingRatio: 0.9 };
32
+ /** Underdamped and stopped at the bottom edge, so the exit ends on a fast frame instead of a slow tail. */
33
+ const EXIT: Motion = { type: 'spring', response: 0.45, dampingRatio: 0.8 };
34
+
35
+ interface Drag {
36
+ input: 'touch' | 'pointer';
37
+ /** Touch identifier or pointer id. */
38
+ id: number;
39
+ startX: number;
40
+ startY: number;
41
+ lastY: number;
42
+ /** The innermost vertical scroll container under the start point. */
43
+ scroller: HTMLElement | null;
44
+ /** The sheet claimed the gesture and follows it. */
45
+ active: boolean;
46
+ /** Unconstrained sheet top, before rubber-banding. */
47
+ rawTop: number;
48
+ /** Whether the last movement moved the sheet rather than the scroller. */
49
+ sheetDrove: boolean;
50
+ samples: { time: number; y: number }[];
51
+ }
52
+
53
+ function sheetWidth(available: number): number {
54
+ return Math.min(available, MAX_WIDTH);
55
+ }
56
+
57
+ /** Tallest height the content may occupy (excludes the bottom safe area). */
58
+ function maximumDetentValue(viewportHeight: number, insets: Insets): number {
59
+ return Math.max(0, viewportHeight - insets.top - insets.bottom - TOP_GAP);
60
+ }
61
+
62
+ /** Height the content may occupy at the largest of `detents`. */
63
+ function contentHeight(detents: SheetDetent[], maximum: number): number {
64
+ if (
65
+ detents.length === 0 ||
66
+ detents.includes('large') ||
67
+ detents.includes('fitContent')
68
+ ) {
69
+ return maximum;
70
+ }
71
+ return detents.includes('medium') ? maximum * 0.5 : maximum * 0.25;
72
+ }
73
+
74
+ /** Best guess of the content area before the sheet is on screen. */
75
+ export function sheetContentArea(
76
+ detents: SheetDetent[],
77
+ viewport: Size,
78
+ insets: Insets
79
+ ): Size {
80
+ return {
81
+ width: sheetWidth(viewport.width),
82
+ height: contentHeight(detents, maximumDetentValue(viewport.height, insets)),
83
+ };
84
+ }
85
+
86
+ function rubberBand(offset: number, dimension: number): number {
87
+ return (1 - 1 / ((offset * 0.55) / dimension + 1)) * dimension;
88
+ }
89
+
90
+ function findTouch(touches: TouchList, id: number): Touch | undefined {
91
+ for (let i = 0; i < touches.length; i++) {
92
+ const touch = touches.item(i);
93
+ if (touch?.identifier === id) return touch;
94
+ }
95
+ return undefined;
96
+ }
97
+
98
+ /**
99
+ * `bottomSheet` mode: an edge-attached sheet with detents, dragging,
100
+ * swipe-to-dismiss and hand-off to scrollable content, like the iOS
101
+ * implementation.
102
+ */
103
+ export class SheetPresenter extends ModalPresenter {
104
+ private selectedIndex = 0;
105
+ /** The sheet sits below the viewport (before presenting / while dismissing). */
106
+ private isOffscreen = true;
107
+ private drag: Drag | null = null;
108
+ /** Vertical velocity of the swipe that requested the dismissal. */
109
+ private releaseVelocity = 0;
110
+ /** The sheet's top edge in the viewport. */
111
+ private readonly top = new AnimatedValue(0, 0.5, (value) =>
112
+ this.render(value)
113
+ );
114
+
115
+ constructor(config: ModalConfig, listener: PresenterListener) {
116
+ super(config, listener);
117
+ this.selectedIndex = Math.min(
118
+ Math.max(config.initialDetentIndex, 0),
119
+ this.detents.length - 1
120
+ );
121
+ }
122
+
123
+ snapToDetent(index: number) {
124
+ if (index < 0 || index >= this.detents.length) return;
125
+ this.selectedIndex = index;
126
+ this.animateToRest();
127
+ this.listener.onDetentChange(index);
128
+ }
129
+
130
+ protected didShow() {
131
+ const { surface } = this.elements!;
132
+ surface.addEventListener('touchstart', this.handleTouchStart, {
133
+ passive: true,
134
+ });
135
+ surface.addEventListener('touchmove', this.handleTouchMove, {
136
+ passive: false,
137
+ });
138
+ surface.addEventListener('touchend', this.handleTouchEnd);
139
+ surface.addEventListener('touchcancel', this.handleTouchEnd);
140
+ surface.addEventListener('pointerdown', this.handlePointerDown);
141
+ this.layout();
142
+ this.top.set(this.viewport.height);
143
+ }
144
+
145
+ protected animateIn() {
146
+ this.isOffscreen = false;
147
+ this.settle(ENTER);
148
+ }
149
+
150
+ protected animateOut(onEnd: () => void) {
151
+ this.cancelDrag();
152
+ this.isOffscreen = true;
153
+ const velocity = this.releaseVelocity;
154
+ this.releaseVelocity = 0;
155
+ this.top.animateTo(this.viewport.height, this.motion(EXIT), {
156
+ velocity: velocity > 0 ? velocity : undefined,
157
+ stopAtTarget: true,
158
+ onEnd,
159
+ });
160
+ }
161
+
162
+ protected configDidChange() {
163
+ this.selectedIndex = Math.min(this.selectedIndex, this.detents.length - 1);
164
+ this.layout();
165
+ this.animateToRest();
166
+ }
167
+
168
+ protected viewportDidChange() {
169
+ this.layout();
170
+ this.animateToRest();
171
+ }
172
+
173
+ protected contentSizeDidChange() {
174
+ if (this.config.detents.includes('fitContent')) {
175
+ this.animateToRest();
176
+ }
177
+ }
178
+
179
+ protected stopAnimations() {
180
+ this.top.stop();
181
+ }
182
+
183
+ protected teardown() {
184
+ super.teardown();
185
+ this.cancelDrag();
186
+ const surface = this.elements?.surface;
187
+ surface?.removeEventListener('touchstart', this.handleTouchStart);
188
+ surface?.removeEventListener('touchmove', this.handleTouchMove);
189
+ surface?.removeEventListener('touchend', this.handleTouchEnd);
190
+ surface?.removeEventListener('touchcancel', this.handleTouchEnd);
191
+ surface?.removeEventListener('pointerdown', this.handlePointerDown);
192
+ }
193
+
194
+ // Layout
195
+
196
+ private get detents(): SheetDetent[] {
197
+ return this.config.detents.length > 0
198
+ ? this.config.detents
199
+ : ['fitContent'];
200
+ }
201
+
202
+ private get maximumDetentValue(): number {
203
+ return maximumDetentValue(this.viewport.height, this.insets);
204
+ }
205
+
206
+ private get keyboardLift(): number {
207
+ return this.config.keyboardBehavior === 'none'
208
+ ? 0
209
+ : Math.max(0, this.keyboardHeight - this.insets.bottom);
210
+ }
211
+
212
+ private height(detent: SheetDetent): number {
213
+ const maximum = this.maximumDetentValue;
214
+ switch (detent) {
215
+ case 'small':
216
+ return maximum * 0.25;
217
+ case 'medium':
218
+ return maximum * 0.5;
219
+ case 'large':
220
+ return maximum;
221
+ case 'fitContent':
222
+ return Math.min(Math.max(this.contentSize.height, 1), maximum);
223
+ }
224
+ }
225
+
226
+ /** Sheet top when resting on `detents[index]`. The bottom safe area is added below the content. */
227
+ private restingTop(index: number): number {
228
+ const { detents } = this;
229
+ const detent =
230
+ detents[Math.min(Math.max(index, 0), detents.length - 1)] ?? 'fitContent';
231
+ const top =
232
+ this.viewport.height -
233
+ this.insets.bottom -
234
+ this.keyboardLift -
235
+ this.height(detent);
236
+ return Math.max(top, this.insets.top + TOP_GAP);
237
+ }
238
+
239
+ private get detentTops(): number[] {
240
+ return this.detents.map((_, index) => this.restingTop(index));
241
+ }
242
+
243
+ private layout() {
244
+ const { surface, content, grabber } = this.elements!;
245
+ const width = sheetWidth(this.viewport.width);
246
+ const radius = `${this.config.cornerRadius ?? DEFAULT_CORNER_RADIUS}px`;
247
+ Object.assign(surface.style, {
248
+ left: `${(this.viewport.width - width) / 2}px`,
249
+ top: '0px',
250
+ width: `${width}px`,
251
+ height: `${this.viewport.height + OVERSCROLL}px`,
252
+ borderTopLeftRadius: radius,
253
+ borderTopRightRadius: radius,
254
+ borderBottomLeftRadius: '0px',
255
+ borderBottomRightRadius: '0px',
256
+ });
257
+ content.style.width = `${width}px`;
258
+ content.style.height = `${this.maximumDetentValue}px`;
259
+ grabber.style.display = this.config.grabberVisible ? '' : 'none';
260
+
261
+ let height = contentHeight(this.config.detents, this.maximumDetentValue);
262
+ if (this.config.keyboardBehavior === 'resize') {
263
+ height -= this.keyboardLift;
264
+ }
265
+ this.reportContentArea(width, Math.max(height, 0));
266
+ this.render(this.top.value);
267
+ }
268
+
269
+ private render(top: number) {
270
+ if (!this.elements) return;
271
+ this.elements.surface.style.transform = `translate3d(0, ${top}px, 0)`;
272
+ this.elements.backdrop.style.opacity = String(this.backdropAlpha(top));
273
+ }
274
+
275
+ /** Fully dimmed at every detent; fades out below the lowest one. */
276
+ private backdropAlpha(top: number): number {
277
+ const bottom = this.viewport.height;
278
+ const lowest = Math.max(...this.detentTops);
279
+ if (!(bottom > lowest)) return top >= bottom ? 0 : 1;
280
+ return Math.min(Math.max((bottom - top) / (bottom - lowest), 0), 1);
281
+ }
282
+
283
+ private motion(motion: Motion): Motion {
284
+ return prefersReducedMotion() ? INSTANT : motion;
285
+ }
286
+
287
+ private settle(motion: Motion, velocity?: number) {
288
+ this.top.animateTo(
289
+ this.restingTop(this.selectedIndex),
290
+ this.motion(motion),
291
+ {
292
+ velocity,
293
+ // Keeps the enter's completion when a re-layout takes it over.
294
+ onEnd: () => this.notifyPresented(),
295
+ }
296
+ );
297
+ }
298
+
299
+ private animateToRest() {
300
+ if (!this.elements || this.drag?.active || this.isOffscreen) return;
301
+ this.settle(SETTLE);
302
+ }
303
+
304
+ // Dragging
305
+
306
+ private canDrag(): boolean {
307
+ return !this.entering && !this.isDismissing && !this.isOffscreen;
308
+ }
309
+
310
+ private beginDrag(
311
+ input: Drag['input'],
312
+ id: number,
313
+ x: number,
314
+ y: number,
315
+ target: EventTarget | null
316
+ ) {
317
+ this.drag = {
318
+ input,
319
+ id,
320
+ startX: x,
321
+ startY: y,
322
+ lastY: y,
323
+ scroller: findVerticalScroller(target, this.elements!.surface),
324
+ active: false,
325
+ rawTop: 0,
326
+ sheetDrove: false,
327
+ samples: [],
328
+ };
329
+ }
330
+
331
+ /** Decides who handles a gesture once it moves: the sheet, or the content (scrolling, horizontal swipes). */
332
+ private claim(
333
+ drag: Drag,
334
+ x: number,
335
+ y: number,
336
+ slop: number
337
+ ): 'pending' | 'sheet' | 'content' {
338
+ const dx = x - drag.startX;
339
+ const dy = y - drag.startY;
340
+ if (Math.abs(dx) <= slop && Math.abs(dy) <= slop) return 'pending';
341
+ if (Math.abs(dx) > Math.abs(dy)) return 'content';
342
+ const { scroller } = drag;
343
+ if (!scroller) return 'sheet';
344
+ // Down: scroll back to the top before collapsing the sheet. Up: expand
345
+ // the sheet before scrolling.
346
+ if (dy > 0) return scroller.scrollTop <= 0 ? 'sheet' : 'content';
347
+ return this.top.value > Math.min(...this.detentTops) + 0.5
348
+ ? 'sheet'
349
+ : 'content';
350
+ }
351
+
352
+ private activate(drag: Drag) {
353
+ drag.active = true;
354
+ this.top.stop();
355
+ drag.rawTop = this.top.value;
356
+ const { surface } = this.elements!;
357
+ // A native scroll on an ancestor makes React Native Web cancel the press
358
+ // under the pointer, so dragging from a button doesn't leave it pressed.
359
+ surface.dispatchEvent(new Event('scroll'));
360
+ if (drag.input === 'pointer') {
361
+ surface.style.setProperty('user-select', 'none');
362
+ surface.style.setProperty('-webkit-user-select', 'none');
363
+ window.getSelection()?.removeAllRanges();
364
+ }
365
+ }
366
+
367
+ private moveDrag(drag: Drag, y: number) {
368
+ const delta = y - drag.lastY;
369
+ drag.lastY = y;
370
+ const time = performance.now();
371
+ drag.samples.push({ time, y });
372
+ while (
373
+ drag.samples.length > 2 &&
374
+ time - drag.samples[0]!.time > VELOCITY_WINDOW_MS
375
+ ) {
376
+ drag.samples.shift();
377
+ }
378
+
379
+ const { scroller } = drag;
380
+ if (scroller) {
381
+ // The browser's own scrolling is prevented for this gesture, so the
382
+ // content is scrolled here whenever the sheet can't move.
383
+ const minTop = Math.min(...this.detentTops);
384
+ const drive =
385
+ delta < 0 ? drag.rawTop > minTop + 0.5 : scroller.scrollTop <= 0;
386
+ if (drive) {
387
+ drag.rawTop = Math.max(drag.rawTop + delta, minTop);
388
+ } else {
389
+ scroller.scrollTop -= delta;
390
+ }
391
+ drag.sheetDrove = drive;
392
+ } else {
393
+ drag.rawTop += delta;
394
+ drag.sheetDrove = true;
395
+ }
396
+ this.top.set(this.constrained(drag.rawTop));
397
+ }
398
+
399
+ private release(drag: Drag) {
400
+ this.stopTracking();
401
+ this.drag = null;
402
+ if (!drag.active) return;
403
+ if (drag.input === 'pointer') {
404
+ suppressNextClick();
405
+ }
406
+
407
+ const velocity = drag.sheetDrove ? this.velocity(drag) : 0;
408
+ const top = this.top.value;
409
+ const tops = this.detentTops;
410
+ const lowest = Math.max(...tops);
411
+ // Where a fling of this velocity would come to rest.
412
+ const projected = top + velocity * 0.2;
413
+
414
+ if (
415
+ this.config.dismissOnSwipe &&
416
+ velocity >= 0 &&
417
+ projected > lowest + (this.viewport.height - lowest) / 2
418
+ ) {
419
+ this.releaseVelocity = velocity;
420
+ this.listener.onSwipeDismiss();
421
+ if (this.isDismissing) return;
422
+ this.releaseVelocity = 0;
423
+ }
424
+
425
+ const index = tops.reduce(
426
+ (best, candidate, i) =>
427
+ Math.abs(candidate - projected) < Math.abs(tops[best]! - projected)
428
+ ? i
429
+ : best,
430
+ 0
431
+ );
432
+ if (index !== this.selectedIndex) {
433
+ this.selectedIndex = index;
434
+ this.listener.onDetentChange(index);
435
+ }
436
+ this.settle(SETTLE, velocity);
437
+ }
438
+
439
+ /** Abandons the gesture without settling (the exit or the content takes over). */
440
+ private cancelDrag() {
441
+ this.stopTracking();
442
+ this.drag = null;
443
+ }
444
+
445
+ private stopTracking() {
446
+ window.removeEventListener('pointermove', this.handlePointerMove);
447
+ window.removeEventListener('pointerup', this.handlePointerUp);
448
+ window.removeEventListener('pointercancel', this.handlePointerUp);
449
+ const surface = this.elements?.surface;
450
+ surface?.style.removeProperty('user-select');
451
+ surface?.style.removeProperty('-webkit-user-select');
452
+ }
453
+
454
+ private velocity(drag: Drag): number {
455
+ const now = performance.now();
456
+ const samples = drag.samples.filter(
457
+ (sample) => now - sample.time <= VELOCITY_WINDOW_MS
458
+ );
459
+ const first = samples[0];
460
+ const last = samples[samples.length - 1];
461
+ if (!first || !last || last.time <= first.time) return 0;
462
+ return ((last.y - first.y) / (last.time - first.time)) * 1000;
463
+ }
464
+
465
+ /** Rubber-bands above the tallest detent, and below the lowest one when swiping can't dismiss. */
466
+ private constrained(top: number): number {
467
+ const tops = this.detentTops;
468
+ const minTop = Math.min(...tops);
469
+ const maxTop = Math.max(...tops);
470
+ const dimension = Math.max(this.viewport.height, 1);
471
+ if (top < minTop) {
472
+ return minTop - rubberBand(minTop - top, dimension);
473
+ }
474
+ if (!this.config.dismissOnSwipe && top > maxTop) {
475
+ return maxTop + rubberBand(top - maxTop, dimension);
476
+ }
477
+ return top;
478
+ }
479
+
480
+ // Touch input. Touch events (rather than pointer events) let the sheet
481
+ // cancel the browser's scrolling when it claims the gesture.
482
+
483
+ private handleTouchStart = (event: TouchEvent) => {
484
+ if (this.drag) {
485
+ // A second finger before the sheet moved: leave it to the browser.
486
+ if (this.drag.input === 'touch' && !this.drag.active) {
487
+ this.drag = null;
488
+ }
489
+ return;
490
+ }
491
+ const touch = event.touches.length === 1 ? event.touches[0] : undefined;
492
+ if (!touch || !this.canDrag()) return;
493
+ this.beginDrag(
494
+ 'touch',
495
+ touch.identifier,
496
+ touch.clientX,
497
+ touch.clientY,
498
+ event.target
499
+ );
500
+ };
501
+
502
+ private handleTouchMove = (event: TouchEvent) => {
503
+ const drag = this.drag;
504
+ if (drag?.input !== 'touch') return;
505
+ const touch = findTouch(event.changedTouches, drag.id);
506
+ if (!touch) return;
507
+ if (!drag.active) {
508
+ // Once the browser has started scrolling, the gesture is its.
509
+ const owner = event.cancelable
510
+ ? this.claim(drag, touch.clientX, touch.clientY, 0)
511
+ : 'content';
512
+ if (owner === 'pending') return;
513
+ if (owner === 'content') {
514
+ this.drag = null;
515
+ return;
516
+ }
517
+ this.activate(drag);
518
+ }
519
+ if (event.cancelable) {
520
+ event.preventDefault();
521
+ }
522
+ this.moveDrag(drag, touch.clientY);
523
+ };
524
+
525
+ private handleTouchEnd = (event: TouchEvent) => {
526
+ const drag = this.drag;
527
+ if (drag?.input !== 'touch' || !findTouch(event.changedTouches, drag.id))
528
+ return;
529
+ this.release(drag);
530
+ };
531
+
532
+ // Mouse and pen input.
533
+
534
+ private handlePointerDown = (event: PointerEvent) => {
535
+ if (
536
+ event.pointerType === 'touch' ||
537
+ event.button !== 0 ||
538
+ this.drag ||
539
+ !this.canDrag() ||
540
+ // Leave text selection in inputs alone.
541
+ isTextInput(event.target)
542
+ ) {
543
+ return;
544
+ }
545
+ this.beginDrag(
546
+ 'pointer',
547
+ event.pointerId,
548
+ event.clientX,
549
+ event.clientY,
550
+ event.target
551
+ );
552
+ window.addEventListener('pointermove', this.handlePointerMove);
553
+ window.addEventListener('pointerup', this.handlePointerUp);
554
+ window.addEventListener('pointercancel', this.handlePointerUp);
555
+ };
556
+
557
+ private handlePointerMove = (event: PointerEvent) => {
558
+ const drag = this.drag;
559
+ if (drag?.input !== 'pointer' || event.pointerId !== drag.id) return;
560
+ if (!drag.active) {
561
+ const owner = this.claim(
562
+ drag,
563
+ event.clientX,
564
+ event.clientY,
565
+ POINTER_SLOP
566
+ );
567
+ if (owner === 'pending') return;
568
+ if (owner === 'content') {
569
+ this.cancelDrag();
570
+ return;
571
+ }
572
+ this.activate(drag);
573
+ }
574
+ this.moveDrag(drag, event.clientY);
575
+ };
576
+
577
+ private handlePointerUp = (event: PointerEvent) => {
578
+ const drag = this.drag;
579
+ if (drag?.input !== 'pointer' || event.pointerId !== drag.id) return;
580
+ this.release(drag);
581
+ };
582
+ }