react-native-gesture-image-viewer 2.2.0 → 2.3.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 (44) hide show
  1. package/README.md +6 -1
  2. package/lib/module/GestureViewer.js +2 -2
  3. package/lib/module/GestureViewer.js.map +1 -1
  4. package/lib/module/GestureViewerManager.js +3 -0
  5. package/lib/module/GestureViewerManager.js.map +1 -1
  6. package/lib/module/index.js.map +1 -1
  7. package/lib/module/useGestureViewer.js +58 -15
  8. package/lib/module/useGestureViewer.js.map +1 -1
  9. package/lib/module/useGestureViewerEvent.js +15 -1
  10. package/lib/module/useGestureViewerEvent.js.map +1 -1
  11. package/lib/module/useGestureViewerPaging.js +1 -1
  12. package/lib/module/useGestureViewerPaging.js.map +1 -1
  13. package/lib/module/useGestureViewerPaging.web.js +47 -26
  14. package/lib/module/useGestureViewerPaging.web.js.map +1 -1
  15. package/lib/module/utils/dismiss.js +25 -0
  16. package/lib/module/utils/dismiss.js.map +1 -0
  17. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  18. package/lib/typescript/src/GestureViewerManager.d.ts +2 -1
  19. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  20. package/lib/typescript/src/index.d.ts +1 -1
  21. package/lib/typescript/src/index.d.ts.map +1 -1
  22. package/lib/typescript/src/types.d.ts +33 -4
  23. package/lib/typescript/src/types.d.ts.map +1 -1
  24. package/lib/typescript/src/useGestureViewer.d.ts +2 -2
  25. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  26. package/lib/typescript/src/useGestureViewerEvent.d.ts +15 -1
  27. package/lib/typescript/src/useGestureViewerEvent.d.ts.map +1 -1
  28. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +9 -1
  29. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +1 -1
  30. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +1 -1
  31. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +1 -1
  32. package/lib/typescript/src/utils/dismiss.d.ts +4 -0
  33. package/lib/typescript/src/utils/dismiss.d.ts.map +1 -0
  34. package/package.json +1 -1
  35. package/src/GestureViewer.tsx +4 -2
  36. package/src/GestureViewerManager.ts +4 -0
  37. package/src/index.tsx +1 -0
  38. package/src/types.ts +31 -4
  39. package/src/useGestureViewer.ts +108 -47
  40. package/src/useGestureViewerEvent.ts +15 -1
  41. package/src/useGestureViewerPaging.ts +1 -1
  42. package/src/useGestureViewerPaging.types.ts +7 -1
  43. package/src/useGestureViewerPaging.web.ts +56 -32
  44. package/src/utils/dismiss.ts +36 -0
package/src/types.ts CHANGED
@@ -75,6 +75,15 @@ export type TriggerRect = {
75
75
  height: number;
76
76
  };
77
77
 
78
+ export type GestureViewerSingleTapEvent<ItemT> = {
79
+ x: number;
80
+ y: number;
81
+ index: number;
82
+ item: ItemT;
83
+ };
84
+
85
+ export type GestureViewerDismissDirection = 'down' | 'up' | 'both';
86
+
78
87
  export interface TriggerAnimationConfig extends WithTimingConfig {
79
88
  /**
80
89
  * Animation duration in milliseconds
@@ -121,9 +130,18 @@ export interface GestureViewerProps<ItemT, LC> {
121
130
  * A callback function that is called to render the item.
122
131
  */
123
132
  renderItem: (item: ItemT, index: number) => React.ReactElement;
133
+ /**
134
+ * A callback function that is called when a single tap is confirmed on the viewer content.
135
+ * @remarks
136
+ * - The callback runs only after the tap is resolved as a single tap, so it may be slightly delayed when double-tap zoom is enabled.
137
+ * - It is not called for swipe, pinch, dismiss, or double-tap zoom gestures.
138
+ * - Prefer this callback over overlaying a pressable in `renderContainer` for fullscreen tap handling.
139
+ */
140
+ onSingleTap?: (event: GestureViewerSingleTapEvent<ItemT>) => void;
124
141
  /**
125
142
  * A callback function that is called to render the container.
126
143
  * @remarks Useful for composing additional UI (e.g., close button, toolbars) around the viewer.
144
+ * Prefer `onSingleTap` for fullscreen tap handling instead of overlaying a pressable over the viewer content.
127
145
  * The second argument provides control helpers such as `dismiss()` to close the viewer.
128
146
  *
129
147
  * @param children - The viewer content to be rendered inside your container.
@@ -183,8 +201,8 @@ export interface GestureViewerProps<ItemT, LC> {
183
201
  */
184
202
  autoPlayInterval?: number;
185
203
  /**
186
- * Dismiss gesture options. Calls `onDismiss` function when swiping down.
187
- * @remarks Useful for closing modals with downward swipe gestures.
204
+ * Dismiss gesture options.
205
+ * @remarks Useful for closing modals with configurable vertical swipe gestures.
188
206
  */
189
207
  dismiss?: {
190
208
  /**
@@ -192,6 +210,12 @@ export interface GestureViewerProps<ItemT, LC> {
192
210
  * @defaultValue true
193
211
  */
194
212
  enabled?: boolean;
213
+ /**
214
+ * Controls which vertical swipe direction can trigger `onDismiss`.
215
+ * @remarks Use `down` for backward-compatible behavior, `up` for upward-only dismiss, or `both` for either direction.
216
+ * @defaultValue 'down'
217
+ */
218
+ direction?: GestureViewerDismissDirection;
195
219
  /**
196
220
  * `threshold` controls when `onDismiss` is called by applying a threshold value during vertical gestures.
197
221
  * @defaultValue 80
@@ -203,7 +227,7 @@ export interface GestureViewerProps<ItemT, LC> {
203
227
  */
204
228
  resistance?: number;
205
229
  /**
206
- * By default, the background `opacity` gradually decreases from 1 to 0 during downward swipe gestures.
230
+ * By default, the background `opacity` gradually decreases as you drag in the configured dismiss direction.
207
231
  * @remarks When `false`, this animation is disabled.
208
232
  * @defaultValue true
209
233
  */
@@ -425,11 +449,14 @@ export type GestureViewerState = {
425
449
  readonly totalCount: number;
426
450
  };
427
451
 
428
- export type GestureViewerEventType = 'zoomChange' | 'rotationChange';
452
+ export type GestureViewerEventType = 'zoomChange' | 'rotationChange' | 'tap';
453
+
454
+ export type SingleTapEventData = { kind: 'single'; x: number; y: number; index: number };
429
455
 
430
456
  export type GestureViewerEventData = {
431
457
  zoomChange: { scale: number; previousScale: number | null };
432
458
  rotationChange: { rotation: number; previousRotation: number | null };
459
+ tap: SingleTapEventData;
433
460
  };
434
461
 
435
462
  export type GestureViewerEventCallback<T extends GestureViewerEventType> = (
@@ -17,6 +17,7 @@ import { registry } from './GestureViewerRegistry';
17
17
  import type { GestureViewerProps, TriggerRect } from './types';
18
18
  import { useGestureViewerPaging } from './useGestureViewerPaging';
19
19
  import { createBoundsConstraint, createScrollAction } from './utils';
20
+ import { getDismissDistance, shouldDismissByDirection } from './utils/dismiss';
20
21
  import { applyTapZoomAtPoint } from './utils/tapZoom';
21
22
 
22
23
  type UseGestureViewerProps<ItemT, LC> = Omit<
@@ -34,6 +35,7 @@ export const useGestureViewer = <ItemT, LC>({
34
35
  data,
35
36
  initialIndex = 0,
36
37
  onDismiss,
38
+ onSingleTap,
37
39
  width: customWidth,
38
40
  dismiss,
39
41
  enableDoubleTapZoom = true,
@@ -68,6 +70,9 @@ export const useGestureViewer = <ItemT, LC>({
68
70
  const triggerRectRef = useRef<TriggerRect | null>(null);
69
71
  const pendingIndexRef = useRef(initialIndex);
70
72
  const onAnimationCompleteRef = useRef(triggerAnimation?.onAnimationComplete);
73
+ const onSingleTapRef = useRef(onSingleTap);
74
+ const dataRef = useRef(data);
75
+ const managerRef = useRef(manager);
71
76
 
72
77
  const isValidTriggerRect = useCallback((rect: TriggerRect | null): rect is TriggerRect => {
73
78
  return !!rect && rect.width > 0 && rect.height > 0;
@@ -104,12 +109,19 @@ export const useGestureViewer = <ItemT, LC>({
104
109
 
105
110
  const dismissOptions = useMemo(
106
111
  () => ({
112
+ direction: dismiss?.direction ?? 'down',
107
113
  enabled: dismiss?.enabled ?? true,
108
114
  fadeBackdrop: dismiss?.fadeBackdrop ?? true,
109
115
  resistance: dismiss?.resistance ?? 2,
110
116
  threshold: dismiss?.threshold ?? 80,
111
117
  }),
112
- [dismiss?.enabled, dismiss?.threshold, dismiss?.resistance, dismiss?.fadeBackdrop],
118
+ [
119
+ dismiss?.direction,
120
+ dismiss?.enabled,
121
+ dismiss?.threshold,
122
+ dismiss?.resistance,
123
+ dismiss?.fadeBackdrop,
124
+ ],
113
125
  );
114
126
 
115
127
  const adjustedInitialIndex = useMemo(() => {
@@ -176,23 +188,13 @@ export const useGestureViewer = <ItemT, LC>({
176
188
  [dataLength, manager, resetTransformState],
177
189
  );
178
190
 
179
- const emitZoomChange = useCallback(
180
- (currentScale: number, prevScale: number | null) => {
181
- if (manager) {
182
- manager.emitZoomChange(currentScale, prevScale);
183
- }
184
- },
185
- [manager],
186
- );
191
+ const emitZoomChange = useCallback((currentScale: number, prevScale: number | null) => {
192
+ managerRef.current?.emitZoomChange(currentScale, prevScale);
193
+ }, []);
187
194
 
188
- const emitRotationChange = useCallback(
189
- (currentRotation: number, prevRotation: number | null) => {
190
- if (manager) {
191
- manager.emitRotationChange(currentRotation, prevRotation);
192
- }
193
- },
194
- [manager],
195
- );
195
+ const emitRotationChange = useCallback((currentRotation: number, prevRotation: number | null) => {
196
+ managerRef.current?.emitRotationChange(currentRotation, prevRotation);
197
+ }, []);
196
198
 
197
199
  const onAnimationComplete = useCallback(() => {
198
200
  onAnimationCompleteRef.current?.();
@@ -317,6 +319,18 @@ export const useGestureViewer = <ItemT, LC>({
317
319
  onAnimationCompleteRef.current = triggerAnimation?.onAnimationComplete;
318
320
  }, [triggerAnimation?.onAnimationComplete]);
319
321
 
322
+ useEffect(() => {
323
+ dataRef.current = data;
324
+ }, [data]);
325
+
326
+ useEffect(() => {
327
+ managerRef.current = manager;
328
+ }, [manager]);
329
+
330
+ useEffect(() => {
331
+ onSingleTapRef.current = onSingleTap;
332
+ }, [onSingleTap]);
333
+
320
334
  useEffect(() => {
321
335
  if (shouldStartTriggerAnimation && triggerRectRef.current) {
322
336
  const startX = triggerRectRef.current.x + triggerRectRef.current.width / 2 - width / 2;
@@ -475,7 +489,14 @@ export const useGestureViewer = <ItemT, LC>({
475
489
  translateY.value = event.translationY / dismissOptions.resistance;
476
490
  })
477
491
  .onEnd((event) => {
478
- if (canDismiss && event.translationY > dismissOptions.threshold) {
492
+ if (
493
+ canDismiss &&
494
+ shouldDismissByDirection(
495
+ event.translationY,
496
+ dismissOptions.threshold,
497
+ dismissOptions.direction,
498
+ )
499
+ ) {
479
500
  scheduleOnRN(handleDismiss);
480
501
  return;
481
502
  }
@@ -647,6 +668,40 @@ export const useGestureViewer = <ItemT, LC>({
647
668
  ],
648
669
  );
649
670
 
671
+ const emitSingleTap = useCallback((x: number, y: number) => {
672
+ const index = pendingIndexRef.current;
673
+ const currentData = dataRef.current;
674
+
675
+ if (index < 0 || index >= currentData.length) {
676
+ return;
677
+ }
678
+
679
+ managerRef.current?.emitTap({ kind: 'single', x, y, index });
680
+
681
+ const item = currentData[index];
682
+
683
+ if (item === undefined) {
684
+ return;
685
+ }
686
+
687
+ onSingleTapRef.current?.({ x, y, index, item });
688
+ }, []);
689
+
690
+ const singleTapGesture = useMemo(
691
+ () =>
692
+ Gesture.Tap()
693
+ .enabled(Platform.OS !== 'web')
694
+ .numberOfTaps(1)
695
+ .onEnd((event, success) => {
696
+ if (!success) {
697
+ return;
698
+ }
699
+
700
+ scheduleOnRN(emitSingleTap, event.x, event.y);
701
+ }),
702
+ [emitSingleTap],
703
+ );
704
+
650
705
  const doubleTapGesture = useMemo(
651
706
  () =>
652
707
  Gesture.Tap()
@@ -667,9 +722,14 @@ export const useGestureViewer = <ItemT, LC>({
667
722
  [enableDoubleTapZoom, height, maxZoomScale, scale, translateX, translateY, width],
668
723
  );
669
724
 
725
+ const tapGesture = useMemo(
726
+ () => Gesture.Exclusive(doubleTapGesture, singleTapGesture),
727
+ [doubleTapGesture, singleTapGesture],
728
+ );
729
+
670
730
  const zoomGesture = useMemo(
671
- () => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, doubleTapGesture)),
672
- [zoomPinchGesture, zoomPanGesture, doubleTapGesture],
731
+ () => Gesture.Race(zoomPinchGesture, Gesture.Exclusive(zoomPanGesture, tapGesture)),
732
+ [zoomPinchGesture, zoomPanGesture, tapGesture],
673
733
  );
674
734
 
675
735
  const animatedStyle = useAnimatedStyle(() => ({
@@ -693,39 +753,40 @@ export const useGestureViewer = <ItemT, LC>({
693
753
  return { opacity: baseOpacity };
694
754
  }
695
755
 
696
- const dismissOpacity = interpolate(translateY.value, [0, 200], [1, 0], 'clamp');
756
+ const dismissDistance = getDismissDistance(translateY.value, dismissOptions.direction);
757
+ const dismissOpacity = interpolate(dismissDistance, [0, 200], [1, 0], 'clamp');
697
758
 
698
759
  return { opacity: baseOpacity * dismissOpacity };
699
- }, [dismissOptions.fadeBackdrop]);
760
+ }, [dismissOptions.direction, dismissOptions.fadeBackdrop]);
700
761
 
701
762
  const nativeScrollGesture = useMemo(() => {
702
763
  return Gesture.Native().requireExternalGestureToFail(dismissGestureRef);
703
764
  }, []);
704
765
 
705
- const { onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebDoubleClick } =
706
- useGestureViewerPaging({
707
- adjustedInitialIndex,
708
- autoPlay,
709
- autoPlayInterval,
710
- currentIndex,
711
- dataLength,
712
- enableDoubleTapZoom,
713
- enableHorizontalSwipe,
714
- enableLoop,
715
- height,
716
- isRotated,
717
- isZoomed,
718
- itemSpacing,
719
- manager,
720
- maxZoomScale,
721
- scale,
722
- scrollTo,
723
- syncCurrentIndex,
724
- syncPendingIndex,
725
- translateX,
726
- translateY,
727
- width,
728
- });
766
+ const { onMomentumScrollEnd, onScroll, onScrollBeginDrag, onWebClick } = useGestureViewerPaging({
767
+ adjustedInitialIndex,
768
+ autoPlay,
769
+ autoPlayInterval,
770
+ currentIndex,
771
+ dataLength,
772
+ enableDoubleTapZoom,
773
+ enableHorizontalSwipe,
774
+ enableLoop,
775
+ height,
776
+ isRotated,
777
+ isZoomed,
778
+ itemSpacing,
779
+ manager,
780
+ maxZoomScale,
781
+ onSingleTap: emitSingleTap,
782
+ scale,
783
+ scrollTo,
784
+ syncCurrentIndex,
785
+ syncPendingIndex,
786
+ translateX,
787
+ translateY,
788
+ width,
789
+ });
729
790
 
730
791
  return {
731
792
  animatedStyle,
@@ -739,7 +800,7 @@ export const useGestureViewer = <ItemT, LC>({
739
800
  isZoomed,
740
801
  listRef,
741
802
  nativeScrollGesture,
742
- onWebDoubleClick,
803
+ onWebClick,
743
804
  onMomentumScrollEnd,
744
805
  onScroll,
745
806
 
@@ -8,7 +8,7 @@ import type { GestureViewerEventCallback, GestureViewerEventType } from './types
8
8
  * Hook for subscribing to GestureViewer events on the default instance.
9
9
  *
10
10
  * This hook allows you to listen to specific events from the default GestureViewer instance
11
- * (with ID 'default'), such as zoom changes or rotation changes. Events are automatically
11
+ * (with ID 'default'), such as zoom changes, rotation changes, or confirmed tap events. Events are automatically
12
12
  * throttled to prevent excessive callback invocations during gestures.
13
13
  *
14
14
  * @param eventType - The type of event to listen for
@@ -25,6 +25,13 @@ import type { GestureViewerEventCallback, GestureViewerEventType } from './types
25
25
  * useGestureViewerEvent('rotationChange', (data) => {
26
26
  * console.log(`Rotation changed from ${data.previousRotation}° to ${data.rotation}°`);
27
27
  * });
28
+ *
29
+ * // Listen to confirmed single taps on the default instance (ID: 'default')
30
+ * useGestureViewerEvent('tap', (data) => {
31
+ * if (data.kind === 'single') {
32
+ * console.log(`Tapped item ${data.index} at (${data.x}, ${data.y})`);
33
+ * }
34
+ * });
28
35
  * ```
29
36
  */
30
37
  export function useGestureViewerEvent<T extends GestureViewerEventType>(
@@ -53,6 +60,13 @@ export function useGestureViewerEvent<T extends GestureViewerEventType>(
53
60
  * useGestureViewerEvent('modal-viewer', 'rotationChange', (data) => {
54
61
  * updateRotationIndicator(data.rotation);
55
62
  * });
63
+ *
64
+ * // Listen to confirmed single taps on a specific instance
65
+ * useGestureViewerEvent('gallery', 'tap', (data) => {
66
+ * if (data.kind === 'single') {
67
+ * console.log(`Gallery tap on item ${data.index}`);
68
+ * }
69
+ * });
56
70
  * ```
57
71
  */
58
72
  export function useGestureViewerEvent<T extends GestureViewerEventType>(
@@ -123,6 +123,6 @@ export function useGestureViewerPaging({
123
123
  onMomentumScrollEnd,
124
124
  onScroll,
125
125
  onScrollBeginDrag,
126
- onWebDoubleClick: undefined,
126
+ onWebClick: undefined,
127
127
  };
128
128
  }
@@ -1,3 +1,4 @@
1
+ import type { MouseEvent } from 'react';
1
2
  import type { ScrollViewProps } from 'react-native';
2
3
  import type { SharedValue } from 'react-native-reanimated';
3
4
 
@@ -18,6 +19,7 @@ export type UseGestureViewerPagingArgs = {
18
19
  itemSpacing: number;
19
20
  manager: GestureViewerManager | null;
20
21
  maxZoomScale: number;
22
+ onSingleTap?: (x: number, y: number) => void;
21
23
  scale: SharedValue<number>;
22
24
  scrollTo: (index: number, animated: boolean) => void;
23
25
  syncCurrentIndex: (nextIndex: number) => void;
@@ -27,9 +29,13 @@ export type UseGestureViewerPagingArgs = {
27
29
  width: number;
28
30
  };
29
31
 
32
+ export type WebClickTarget = {
33
+ getBoundingClientRect: () => { left: number; top: number };
34
+ };
35
+
30
36
  export type UseGestureViewerPagingResult = {
31
37
  onMomentumScrollEnd?: ScrollViewProps['onMomentumScrollEnd'];
32
38
  onScroll?: ScrollViewProps['onScroll'];
33
39
  onScrollBeginDrag?: ScrollViewProps['onScrollBeginDrag'];
34
- onWebDoubleClick?: (event: any) => void;
40
+ onWebClick?: (event: MouseEvent<WebClickTarget>) => void;
35
41
  };
@@ -1,9 +1,10 @@
1
- import { useCallback, useEffect, useRef } from 'react';
1
+ import { useCallback, useEffect, useRef, type MouseEvent } from 'react';
2
2
  import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
3
3
 
4
4
  import type {
5
5
  UseGestureViewerPagingArgs,
6
6
  UseGestureViewerPagingResult,
7
+ WebClickTarget,
7
8
  } from './useGestureViewerPaging.types';
8
9
  import { applyTapZoomAtPoint } from './utils/tapZoom';
9
10
  import {
@@ -35,6 +36,7 @@ export function useGestureViewerPaging({
35
36
  enableHorizontalSwipe,
36
37
  enableLoop,
37
38
  height,
39
+ onSingleTap,
38
40
  isRotated,
39
41
  isZoomed,
40
42
  itemSpacing,
@@ -48,6 +50,8 @@ export function useGestureViewerPaging({
48
50
  translateY,
49
51
  width,
50
52
  }: UseGestureViewerPagingArgs): UseGestureViewerPagingResult {
53
+ const webSingleTapTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
54
+
51
55
  const webScrollRuntimeRef = useRef<WebScrollRuntime>({
52
56
  actor: 'idle',
53
57
  isAutoplayPausedByUser: false,
@@ -59,6 +63,13 @@ export function useGestureViewerPaging({
59
63
  resumeAutoplayTimer: null,
60
64
  });
61
65
 
66
+ const clearWebSingleTapTimer = useCallback(() => {
67
+ if (webSingleTapTimerRef.current) {
68
+ clearTimeout(webSingleTapTimerRef.current);
69
+ webSingleTapTimerRef.current = null;
70
+ }
71
+ }, []);
72
+
62
73
  const clearWebSettleTimer = useCallback(() => {
63
74
  const runtime = webScrollRuntimeRef.current;
64
75
 
@@ -232,10 +243,11 @@ export function useGestureViewerPaging({
232
243
 
233
244
  useEffect(() => {
234
245
  return () => {
246
+ clearWebSingleTapTimer();
235
247
  clearWebSettleTimer();
236
248
  clearWebAutoplayResumeTimer();
237
249
  };
238
- }, [clearWebAutoplayResumeTimer, clearWebSettleTimer]);
250
+ }, [clearWebAutoplayResumeTimer, clearWebSettleTimer, clearWebSingleTapTimer]);
239
251
 
240
252
  useEffect(() => {
241
253
  if (
@@ -351,50 +363,62 @@ export function useGestureViewerPaging({
351
363
  manager?.handleScrollBeginDrag();
352
364
  }, [beginWebUserInteraction, clearWebSettleTimer, manager]);
353
365
 
354
- const onWebDoubleClick = useCallback(
355
- (event: any) => {
356
- if (!enableDoubleTapZoom || event?.nativeEvent?.detail !== 2) {
366
+ const onWebClick = useCallback(
367
+ (event: MouseEvent<WebClickTarget>) => {
368
+ const detail = event.detail;
369
+ const rect = event.currentTarget.getBoundingClientRect();
370
+ const resolvedX = event.clientX - rect.left;
371
+ const resolvedY = event.clientY - rect.top;
372
+
373
+ if (!enableDoubleTapZoom) {
374
+ if (!onSingleTap) {
375
+ return;
376
+ }
377
+
378
+ clearWebSingleTapTimer();
379
+ onSingleTap(resolvedX, resolvedY);
357
380
  return;
358
381
  }
359
382
 
360
- pauseWebAutoplayWithoutPagingInteraction();
361
- scheduleWebAutoplayResume();
383
+ if (detail === 2) {
384
+ clearWebSingleTapTimer();
362
385
 
363
- const nativeEvent = event?.nativeEvent;
364
- const eventTarget = event?.currentTarget ?? nativeEvent?.currentTarget ?? nativeEvent?.target;
365
- let locationX = nativeEvent?.locationX;
366
- let locationY = nativeEvent?.locationY;
386
+ pauseWebAutoplayWithoutPagingInteraction();
387
+ scheduleWebAutoplayResume();
367
388
 
368
- if (
369
- (!Number.isFinite(locationX) || !Number.isFinite(locationY)) &&
370
- typeof eventTarget?.getBoundingClientRect === 'function' &&
371
- Number.isFinite(nativeEvent?.clientX) &&
372
- Number.isFinite(nativeEvent?.clientY)
373
- ) {
374
- const rect = eventTarget.getBoundingClientRect();
389
+ applyTapZoomAtPoint({
390
+ x: resolvedX,
391
+ y: resolvedY,
392
+ width,
393
+ height,
394
+ maxZoomScale,
395
+ scale,
396
+ translateX,
397
+ translateY,
398
+ });
399
+ return;
400
+ }
375
401
 
376
- locationX = nativeEvent.clientX - rect.left;
377
- locationY = nativeEvent.clientY - rect.top;
402
+ if (detail !== 1 || !onSingleTap) {
403
+ return;
378
404
  }
379
405
 
380
- applyTapZoomAtPoint({
381
- x: Number.isFinite(locationX) ? locationX : width / 2,
382
- y: Number.isFinite(locationY) ? locationY : height / 2,
383
- width,
384
- height,
385
- maxZoomScale,
386
- scale,
387
- translateX,
388
- translateY,
389
- });
406
+ clearWebSingleTapTimer();
407
+
408
+ webSingleTapTimerRef.current = setTimeout(() => {
409
+ onSingleTap(resolvedX, resolvedY);
410
+ webSingleTapTimerRef.current = null;
411
+ }, 250);
390
412
  },
391
413
  [
414
+ clearWebSingleTapTimer,
392
415
  enableDoubleTapZoom,
393
416
  height,
394
417
  maxZoomScale,
418
+ onSingleTap,
395
419
  pauseWebAutoplayWithoutPagingInteraction,
396
- scheduleWebAutoplayResume,
397
420
  scale,
421
+ scheduleWebAutoplayResume,
398
422
  translateX,
399
423
  translateY,
400
424
  width,
@@ -405,6 +429,6 @@ export function useGestureViewerPaging({
405
429
  onMomentumScrollEnd,
406
430
  onScroll,
407
431
  onScrollBeginDrag,
408
- onWebDoubleClick,
432
+ onWebClick,
409
433
  };
410
434
  }
@@ -0,0 +1,36 @@
1
+ import type { GestureViewerDismissDirection } from '../types';
2
+
3
+ export const shouldDismissByDirection = (
4
+ translationY: number,
5
+ threshold: number,
6
+ direction: GestureViewerDismissDirection,
7
+ ): boolean => {
8
+ 'worklet';
9
+
10
+ if (direction === 'up') {
11
+ return translationY < -threshold;
12
+ }
13
+
14
+ if (direction === 'both') {
15
+ return Math.abs(translationY) > threshold;
16
+ }
17
+
18
+ return translationY > threshold;
19
+ };
20
+
21
+ export const getDismissDistance = (
22
+ translationY: number,
23
+ direction: GestureViewerDismissDirection,
24
+ ): number => {
25
+ 'worklet';
26
+
27
+ if (direction === 'up') {
28
+ return Math.max(-translationY, 0);
29
+ }
30
+
31
+ if (direction === 'both') {
32
+ return Math.abs(translationY);
33
+ }
34
+
35
+ return Math.max(translationY, 0);
36
+ };