react-native-gesture-image-viewer 2.1.4 → 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 (56) hide show
  1. package/README.md +6 -1
  2. package/lib/module/GestureTrigger.js +22 -5
  3. package/lib/module/GestureTrigger.js.map +1 -1
  4. package/lib/module/GestureViewer.js +2 -2
  5. package/lib/module/GestureViewer.js.map +1 -1
  6. package/lib/module/GestureViewerManager.js +3 -0
  7. package/lib/module/GestureViewerManager.js.map +1 -1
  8. package/lib/module/GestureViewerRegistry.js +52 -8
  9. package/lib/module/GestureViewerRegistry.js.map +1 -1
  10. package/lib/module/index.js.map +1 -1
  11. package/lib/module/useGestureViewer.js +141 -43
  12. package/lib/module/useGestureViewer.js.map +1 -1
  13. package/lib/module/useGestureViewerEvent.js +15 -1
  14. package/lib/module/useGestureViewerEvent.js.map +1 -1
  15. package/lib/module/useGestureViewerPaging.js +16 -2
  16. package/lib/module/useGestureViewerPaging.js.map +1 -1
  17. package/lib/module/useGestureViewerPaging.web.js +62 -28
  18. package/lib/module/useGestureViewerPaging.web.js.map +1 -1
  19. package/lib/module/utils/dismiss.js +25 -0
  20. package/lib/module/utils/dismiss.js.map +1 -0
  21. package/lib/typescript/src/GestureTrigger.d.ts +9 -3
  22. package/lib/typescript/src/GestureTrigger.d.ts.map +1 -1
  23. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  24. package/lib/typescript/src/GestureViewerManager.d.ts +2 -1
  25. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  26. package/lib/typescript/src/GestureViewerRegistry.d.ts +11 -4
  27. package/lib/typescript/src/GestureViewerRegistry.d.ts.map +1 -1
  28. package/lib/typescript/src/index.d.ts +1 -1
  29. package/lib/typescript/src/index.d.ts.map +1 -1
  30. package/lib/typescript/src/types.d.ts +33 -4
  31. package/lib/typescript/src/types.d.ts.map +1 -1
  32. package/lib/typescript/src/useGestureViewer.d.ts +2 -2
  33. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  34. package/lib/typescript/src/useGestureViewerEvent.d.ts +15 -1
  35. package/lib/typescript/src/useGestureViewerEvent.d.ts.map +1 -1
  36. package/lib/typescript/src/useGestureViewerPaging.d.ts +1 -1
  37. package/lib/typescript/src/useGestureViewerPaging.d.ts.map +1 -1
  38. package/lib/typescript/src/useGestureViewerPaging.types.d.ts +10 -1
  39. package/lib/typescript/src/useGestureViewerPaging.types.d.ts.map +1 -1
  40. package/lib/typescript/src/useGestureViewerPaging.web.d.ts +1 -1
  41. package/lib/typescript/src/useGestureViewerPaging.web.d.ts.map +1 -1
  42. package/lib/typescript/src/utils/dismiss.d.ts +4 -0
  43. package/lib/typescript/src/utils/dismiss.d.ts.map +1 -0
  44. package/package.json +1 -1
  45. package/src/GestureTrigger.tsx +31 -6
  46. package/src/GestureViewer.tsx +4 -2
  47. package/src/GestureViewerManager.ts +4 -0
  48. package/src/GestureViewerRegistry.ts +67 -8
  49. package/src/index.tsx +1 -0
  50. package/src/types.ts +31 -4
  51. package/src/useGestureViewer.ts +219 -81
  52. package/src/useGestureViewerEvent.ts +15 -1
  53. package/src/useGestureViewerPaging.ts +18 -2
  54. package/src/useGestureViewerPaging.types.ts +8 -1
  55. package/src/useGestureViewerPaging.web.ts +77 -33
  56. package/src/utils/dismiss.ts +36 -0
@@ -1 +1 @@
1
- {"version":3,"file":"useGestureViewerPaging.web.d.ts","sourceRoot":"","sources":["../../../src/useGestureViewerPaging.web.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,0BAA0B,EAC1B,4BAA4B,EAC7B,MAAM,gCAAgC,CAAC;AAqBxC,wBAAgB,sBAAsB,CAAC,EACrC,oBAAoB,EACpB,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,SAAS,EACT,QAAQ,EACR,WAAW,EACX,OAAO,EACP,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,KAAK,GACN,EAAE,0BAA0B,GAAG,4BAA4B,CAqV3D"}
1
+ {"version":3,"file":"useGestureViewerPaging.web.d.ts","sourceRoot":"","sources":["../../../src/useGestureViewerPaging.web.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,0BAA0B,EAC1B,4BAA4B,EAE7B,MAAM,gCAAgC,CAAC;AAqBxC,wBAAgB,sBAAsB,CAAC,EACrC,oBAAoB,EACpB,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,EACV,MAAM,EACN,WAAW,EACX,SAAS,EACT,QAAQ,EACR,WAAW,EACX,OAAO,EACP,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,KAAK,GACN,EAAE,0BAA0B,GAAG,4BAA4B,CA8X3D"}
@@ -0,0 +1,4 @@
1
+ import type { GestureViewerDismissDirection } from '../types';
2
+ export declare const shouldDismissByDirection: (translationY: number, threshold: number, direction: GestureViewerDismissDirection) => boolean;
3
+ export declare const getDismissDistance: (translationY: number, direction: GestureViewerDismissDirection) => number;
4
+ //# sourceMappingURL=dismiss.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dismiss.d.ts","sourceRoot":"","sources":["../../../../src/utils/dismiss.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,UAAU,CAAC;AAE9D,eAAO,MAAM,wBAAwB,GACnC,cAAc,MAAM,EACpB,WAAW,MAAM,EACjB,WAAW,6BAA6B,KACvC,OAYF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,cAAc,MAAM,EACpB,WAAW,6BAA6B,KACvC,MAYF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gesture-image-viewer",
3
- "version": "2.1.4",
3
+ "version": "2.3.0",
4
4
  "description": "🖼️ React Native Image Viewer - Reanimated-powered image gestures with full control",
5
5
  "keywords": [
6
6
  "android",
@@ -1,5 +1,5 @@
1
1
  import type { ReactElement } from 'react';
2
- import { Children, cloneElement, isValidElement, useMemo, useRef } from 'react';
2
+ import { Children, cloneElement, isValidElement, useCallback, useMemo, useRef } from 'react';
3
3
  import { View } from 'react-native';
4
4
 
5
5
  import { registry } from './GestureViewerRegistry';
@@ -16,11 +16,13 @@ type WithOnPress = { onPress?: (...args: unknown[]) => void };
16
16
  * @typeParam T - The child's props type. Must include an optional `onPress` handler.
17
17
  *
18
18
  * @property id - Optional identifier to associate this trigger with a `GestureViewer`.
19
+ * @property index - Optional item index used to align dismiss animations with the current viewer index.
19
20
  * @property children - A single React element whose props include `onPress` (e.g., `Pressable`, `Touchable*`).
20
21
  * @property onPress - Optional handler invoked after the child's own `onPress`.
21
22
  */
22
23
  export type GestureTriggerProps<T extends WithOnPress> = {
23
24
  id?: string;
25
+ index?: number;
24
26
  children: ReactElement<T>;
25
27
  onPress?: (...args: unknown[]) => void;
26
28
  };
@@ -30,15 +32,19 @@ export type GestureTriggerProps<T extends WithOnPress> = {
30
32
  *
31
33
  * @remarks
32
34
  * Behavior on press:
33
- * - Registers the child's native node to the internal registry under the given `id`.
35
+ * - Registers the child's native node as the active trigger under the given `id`.
34
36
  * - Invokes the child's own `onPress` first (if provided).
35
37
  * - Invokes the `onPress` passed to `GestureTrigger` next (if provided).
36
38
  *
39
+ * When `index` is provided, the trigger is also registered by item index while mounted,
40
+ * allowing dismiss animations to target the currently visible item instead of only the opening trigger.
41
+ *
37
42
  * Type parameters:
38
43
  * - `T` — The child's props type. Must include an optional `onPress` handler, ensuring the child is pressable.
39
44
  *
40
45
  * Props:
41
46
  * - `id` — Optional identifier to associate this trigger with a `GestureViewer`. Defaults to `"default"`.
47
+ * - `index` — Optional item index used to support dismiss animations back to the current item trigger.
42
48
  * - `children` — A single React element whose props include `onPress` (e.g., `Pressable`, `Touchable*`, custom button).
43
49
  * - `onPress` — Optional handler invoked after the child's `onPress`. Receives the same arguments as the child's handler.
44
50
  *
@@ -48,7 +54,7 @@ export type GestureTriggerProps<T extends WithOnPress> = {
48
54
  *
49
55
  * Example:
50
56
  * ```tsx
51
- * <GestureTrigger id="gallery" onPress={() => openModal(index)}>
57
+ * <GestureTrigger id="gallery" index={index} onPress={() => openModal(index)}>
52
58
  * <Pressable style={styles.thumb}>
53
59
  * <Image source={{ uri }} style={styles.thumbImage} />
54
60
  * </Pressable>
@@ -57,10 +63,29 @@ export type GestureTriggerProps<T extends WithOnPress> = {
57
63
  */
58
64
  export function GestureTrigger<T extends WithOnPress>({
59
65
  id = 'default',
66
+ index,
60
67
  children,
61
68
  onPress,
62
69
  }: GestureTriggerProps<T>) {
63
- const ref = useRef<View>(null);
70
+ const ref = useRef<View | null>(null);
71
+
72
+ const handleRef = useCallback(
73
+ (node: View | null) => {
74
+ ref.current = node;
75
+
76
+ if (index === undefined) {
77
+ return;
78
+ }
79
+
80
+ if (node) {
81
+ registry.setIndexedTriggerNode(id, index, node);
82
+ return;
83
+ }
84
+
85
+ registry.clearIndexedTriggerNode(id, index);
86
+ },
87
+ [id, index],
88
+ );
64
89
 
65
90
  const wrapped = useMemo(() => {
66
91
  const child = Children.only(children);
@@ -72,7 +97,7 @@ export function GestureTrigger<T extends WithOnPress>({
72
97
  const originalOnPress = child.props?.onPress;
73
98
 
74
99
  const handlePress = (...args: unknown[]) => {
75
- registry.setTriggerNode(id, ref.current);
100
+ registry.setActiveTriggerNode(id, ref.current);
76
101
  originalOnPress?.(...args);
77
102
  onPress?.(...args);
78
103
 
@@ -90,7 +115,7 @@ export function GestureTrigger<T extends WithOnPress>({
90
115
  }, [id, onPress, children]);
91
116
 
92
117
  return (
93
- <View ref={ref} collapsable={false}>
118
+ <View ref={handleRef} collapsable={false}>
94
119
  {wrapped}
95
120
  </View>
96
121
  );
@@ -59,7 +59,7 @@ export function GestureViewer<ItemT, LC>({
59
59
  dismissGesture,
60
60
  zoomGesture,
61
61
  nativeScrollGesture,
62
- onWebDoubleClick,
62
+ onWebClick,
63
63
  onMomentumScrollEnd,
64
64
  onScroll,
65
65
  onScrollBeginDrag,
@@ -191,7 +191,9 @@ export function GestureViewer<ItemT, LC>({
191
191
  <Animated.View style={[styles.background, backdropStyleProps, backdropStyle]} />
192
192
  <Animated.View
193
193
  style={[styles.content, animatedStyle]}
194
- {...(Platform.OS === 'web' && { onClick: onWebDoubleClick })}
194
+ {...(Platform.OS === 'web' && {
195
+ onClick: onWebClick,
196
+ })}
195
197
  {...(Platform.OS === 'web' &&
196
198
  isFlashListLike(Component) && { dataSet: { 'flash-list-paging-enabled-fix': true } })}
197
199
  >
@@ -86,6 +86,10 @@ class GestureViewerManager {
86
86
  this.emitEvent('rotationChange', { rotation, previousRotation });
87
87
  };
88
88
 
89
+ emitTap = (data: GestureViewerEventData['tap']) => {
90
+ this.emitEvent('tap', data);
91
+ };
92
+
89
93
  getState(): GestureViewerState {
90
94
  return {
91
95
  currentIndex: this.currentIndex,
@@ -5,7 +5,9 @@ import GestureViewerManager from './GestureViewerManager';
5
5
  class GestureViewerRegistry {
6
6
  private managers = new Map<string, GestureViewerManager>();
7
7
  private subscribers = new Map<string, Set<(manager: GestureViewerManager | null) => void>>();
8
- private triggers = new Map<string, View | null>();
8
+ private activeTriggerSubscribers = new Map<string, Set<(node: View | null) => void>>();
9
+ private activeTriggers = new Map<string, View | null>();
10
+ private indexedTriggers = new Map<string, Map<number, View | null>>();
9
11
 
10
12
  subscribeToManager(id: string, callback: (manager: GestureViewerManager | null) => void) {
11
13
  if (!this.subscribers.has(id)) {
@@ -29,6 +31,26 @@ class GestureViewerRegistry {
29
31
  };
30
32
  }
31
33
 
34
+ subscribeToActiveTrigger(id: string, callback: (node: View | null) => void) {
35
+ if (!this.activeTriggerSubscribers.has(id)) {
36
+ this.activeTriggerSubscribers.set(id, new Set());
37
+ }
38
+
39
+ this.activeTriggerSubscribers.get(id)?.add(callback);
40
+
41
+ callback(this.getActiveTriggerNode(id));
42
+
43
+ return () => {
44
+ const subscribers = this.activeTriggerSubscribers.get(id);
45
+
46
+ subscribers?.delete(callback);
47
+
48
+ if (subscribers && subscribers.size === 0) {
49
+ this.activeTriggerSubscribers.delete(id);
50
+ }
51
+ };
52
+ }
53
+
32
54
  createManager(id: string): GestureViewerManager | null {
33
55
  if (this.managers.has(id)) {
34
56
  return this.managers.get(id) || null;
@@ -55,7 +77,8 @@ class GestureViewerRegistry {
55
77
 
56
78
  this.notifySubscribers(id, null);
57
79
 
58
- this.triggers.delete(id);
80
+ this.activeTriggers.delete(id);
81
+ this.notifyActiveTriggerSubscribers(id, null);
59
82
  }
60
83
  }
61
84
 
@@ -67,16 +90,52 @@ class GestureViewerRegistry {
67
90
  }
68
91
  }
69
92
 
70
- setTriggerNode(id: string, node: View | null) {
71
- this.triggers.set(id, node);
93
+ notifyActiveTriggerSubscribers(id: string, node: View | null) {
94
+ const listeners = this.activeTriggerSubscribers.get(id);
95
+
96
+ if (listeners) {
97
+ [...listeners].forEach((callback) => callback(node));
98
+ }
72
99
  }
73
100
 
74
- getTriggerNode(id: string): View | null {
75
- return this.triggers.get(id) ?? null;
101
+ setActiveTriggerNode(id: string, node: View | null) {
102
+ this.activeTriggers.set(id, node);
103
+ this.notifyActiveTriggerSubscribers(id, node);
76
104
  }
77
105
 
78
- clearTriggerNode(id: string) {
79
- this.triggers.delete(id);
106
+ getActiveTriggerNode(id: string): View | null {
107
+ return this.activeTriggers.get(id) ?? null;
108
+ }
109
+
110
+ clearActiveTriggerNode(id: string) {
111
+ this.activeTriggers.delete(id);
112
+ this.notifyActiveTriggerSubscribers(id, null);
113
+ }
114
+
115
+ setIndexedTriggerNode(id: string, index: number, node: View | null) {
116
+ if (!this.indexedTriggers.has(id)) {
117
+ this.indexedTriggers.set(id, new Map());
118
+ }
119
+
120
+ this.indexedTriggers.get(id)?.set(index, node);
121
+ }
122
+
123
+ getIndexedTriggerNode(id: string, index: number): View | null {
124
+ return this.indexedTriggers.get(id)?.get(index) ?? null;
125
+ }
126
+
127
+ clearIndexedTriggerNode(id: string, index: number) {
128
+ const triggersByIndex = this.indexedTriggers.get(id);
129
+
130
+ if (!triggersByIndex) {
131
+ return;
132
+ }
133
+
134
+ triggersByIndex.delete(index);
135
+
136
+ if (triggersByIndex.size === 0) {
137
+ this.indexedTriggers.delete(id);
138
+ }
80
139
  }
81
140
  }
82
141
 
package/src/index.tsx CHANGED
@@ -8,6 +8,7 @@ export type {
8
8
  GestureViewerEventType,
9
9
  GestureViewerProps,
10
10
  GestureViewerState,
11
+ GestureViewerSingleTapEvent,
11
12
  } from './types';
12
13
  export { useGestureViewerController } from './useGestureViewerController';
13
14
  export { useGestureViewerEvent } from './useGestureViewerEvent';
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> = (