react-native-gesture-image-viewer 2.0.0-beta.3 → 2.0.0-beta.5

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 (45) hide show
  1. package/README.md +43 -19
  2. package/lib/module/GestureTrigger.js +82 -0
  3. package/lib/module/GestureTrigger.js.map +1 -0
  4. package/lib/module/GestureViewer.js +14 -8
  5. package/lib/module/GestureViewer.js.map +1 -1
  6. package/lib/module/GestureViewerManager.js +5 -5
  7. package/lib/module/GestureViewerManager.js.map +1 -1
  8. package/lib/module/GestureViewerRegistry.js +11 -0
  9. package/lib/module/GestureViewerRegistry.js.map +1 -1
  10. package/lib/module/index.js +2 -0
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/useGestureViewer.js +140 -71
  13. package/lib/module/useGestureViewer.js.map +1 -1
  14. package/lib/module/useGestureViewerController.js +17 -66
  15. package/lib/module/useGestureViewerController.js.map +1 -1
  16. package/lib/module/useGestureViewerState.js +78 -0
  17. package/lib/module/useGestureViewerState.js.map +1 -0
  18. package/lib/typescript/src/GestureTrigger.d.ts +55 -0
  19. package/lib/typescript/src/GestureTrigger.d.ts.map +1 -0
  20. package/lib/typescript/src/GestureViewer.d.ts +2 -2
  21. package/lib/typescript/src/GestureViewer.d.ts.map +1 -1
  22. package/lib/typescript/src/GestureViewerManager.d.ts +5 -8
  23. package/lib/typescript/src/GestureViewerManager.d.ts.map +1 -1
  24. package/lib/typescript/src/GestureViewerRegistry.d.ts +5 -0
  25. package/lib/typescript/src/GestureViewerRegistry.d.ts.map +1 -1
  26. package/lib/typescript/src/index.d.ts +4 -1
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/lib/typescript/src/types.d.ts +116 -58
  29. package/lib/typescript/src/types.d.ts.map +1 -1
  30. package/lib/typescript/src/useGestureViewer.d.ts +9 -8
  31. package/lib/typescript/src/useGestureViewer.d.ts.map +1 -1
  32. package/lib/typescript/src/useGestureViewerController.d.ts +10 -21
  33. package/lib/typescript/src/useGestureViewerController.d.ts.map +1 -1
  34. package/lib/typescript/src/useGestureViewerState.d.ts +35 -0
  35. package/lib/typescript/src/useGestureViewerState.d.ts.map +1 -0
  36. package/package.json +1 -1
  37. package/src/GestureTrigger.tsx +86 -0
  38. package/src/GestureViewer.tsx +15 -9
  39. package/src/GestureViewerManager.ts +9 -9
  40. package/src/GestureViewerRegistry.ts +16 -0
  41. package/src/index.tsx +4 -1
  42. package/src/types.ts +116 -60
  43. package/src/useGestureViewer.ts +180 -77
  44. package/src/useGestureViewerController.ts +19 -76
  45. package/src/useGestureViewerState.ts +85 -0
@@ -3,7 +3,7 @@ import type { GestureViewerController } from './types';
3
3
  * Hook to control the gesture viewer programmatically.
4
4
  *
5
5
  * @param id - Viewer instance identifier (default: 'default')
6
- * @returns Methods and state for controlling the viewer
6
+ * @returns Methods for controlling the viewer
7
7
  *
8
8
  * **Available methods:**
9
9
  * - `goToIndex(index: number)` - Navigate to specific index (0 to totalCount-1)
@@ -12,43 +12,32 @@ import type { GestureViewerController } from './types';
12
12
  * - `zoomIn(multiplier?: number)` - Zoom in (default: 0.25)
13
13
  * - `zoomOut(multiplier?: number)` - Zoom out (default: 0.25)
14
14
  * - `resetZoom(scale?: number)` - Reset zoom level (default: 1.0)
15
- * - `rotate(angle?: 0|90|180|270|360, clockwise?: boolean)` - Rotate content (default: 90° clockwise)
16
- *
17
- * **Available state:**
18
- * - `currentIndex: number` - Current active index (read-only)
19
- * - `totalCount: number` - Total number of items (read-only)
15
+ * - `rotate(angle?: RotationAngle, clockwise?: boolean)` - Rotate content (default: 90° clockwise)
20
16
  *
21
17
  * @example
22
18
  * ```tsx
23
- * const { goToIndex, goToNext, goToPrevious, zoomIn, zoomOut, resetZoom, rotate, currentIndex, totalCount } = useGestureViewerController();
19
+ * const controller = useGestureViewerController();
24
20
  *
25
21
  * // Navigate to specific index
26
- * goToIndex(2);
22
+ * controller.goToIndex(2);
27
23
  *
28
24
  * // Go to next image
29
- * goToNext();
25
+ * controller.goToNext();
30
26
  *
31
27
  * // Zoom in by 25%
32
- * zoomIn();
28
+ * controller.zoomIn();
33
29
  *
34
30
  * // Zoom out by 50%
35
- * zoomOut(0.5);
31
+ * controller.zoomOut(0.5);
36
32
  *
37
33
  * // Reset to original size
38
- * resetZoom();
34
+ * controller.resetZoom();
39
35
  *
40
36
  * // Rotate 90 degrees clockwise
41
- * rotate();
37
+ * controller.rotate();
42
38
  *
43
39
  * // Rotate 180 degrees
44
- * rotate(180);
45
- *
46
- * // Check current state
47
- * console.log(`Image ${currentIndex + 1} of ${totalCount}`);
48
- *
49
- * // Check navigation availability
50
- * const canGoNext = currentIndex < totalCount - 1;
51
- * const canGoPrevious = currentIndex > 0;
40
+ * controller.rotate(180);
52
41
  * ```
53
42
  */
54
43
  export declare const useGestureViewerController: (id?: string) => GestureViewerController;
@@ -1 +1 @@
1
- {"version":3,"file":"useGestureViewerController.d.ts","sourceRoot":"","sources":["../../../src/useGestureViewerController.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAgC,MAAM,SAAS,CAAC;AAErF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,0BAA0B,GAAI,WAAc,KAAG,uBAmF3D,CAAC"}
1
+ {"version":3,"file":"useGestureViewerController.d.ts","sourceRoot":"","sources":["../../../src/useGestureViewerController.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,0BAA0B,GAAI,WAAc,KAAG,uBAqC3D,CAAC"}
@@ -0,0 +1,35 @@
1
+ import type { GestureViewerState } from './types';
2
+ /**
3
+ * Hook to access the current state of the gesture viewer.
4
+ *
5
+ * @param id - Viewer instance identifier (default: 'default')
6
+ * @returns Current state of the viewer
7
+ *
8
+ * **Available state:**
9
+ * - `currentIndex: number` - Current active index (read-only)
10
+ * - `totalCount: number` - Total number of items (read-only)
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const { currentIndex, totalCount } = useGestureViewerState();
15
+ *
16
+ * // Display current position
17
+ * return (
18
+ * <Text>
19
+ * {currentIndex + 1} / {totalCount}
20
+ * </Text>
21
+ * );
22
+ *
23
+ * // React to index changes
24
+ * useEffect(() => {
25
+ * console.log(`Moved to image ${currentIndex + 1}`);
26
+ * trackPageView(currentIndex);
27
+ * }, [currentIndex]);
28
+ *
29
+ * // Check navigation availability
30
+ * const canGoNext = currentIndex < totalCount - 1;
31
+ * const canGoPrevious = currentIndex > 0;
32
+ * ```
33
+ */
34
+ export declare const useGestureViewerState: (id?: string) => GestureViewerState;
35
+ //# sourceMappingURL=useGestureViewerState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGestureViewerState.d.ts","sourceRoot":"","sources":["../../../src/useGestureViewerState.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,qBAAqB,GAAI,WAAc,KAAG,kBAgDtD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gesture-image-viewer",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.5",
4
4
  "description": "🖼️ A highly customizable and easy-to-use React Native image viewer with gesture support and external controls",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -0,0 +1,86 @@
1
+ import type { ReactElement } from 'react';
2
+ import { Children, cloneElement, isValidElement, useMemo, useRef } from 'react';
3
+ import { View } from 'react-native';
4
+ import { registry } from './GestureViewerRegistry';
5
+
6
+ /**
7
+ * Minimal contract for a pressable child's props.
8
+ * The child must optionally accept an `onPress` handler.
9
+ */
10
+ type WithOnPress = { onPress?: (...args: unknown[]) => void };
11
+
12
+ /**
13
+ * Props for `GestureTrigger`.
14
+ *
15
+ * @typeParam T - The child's props type. Must include an optional `onPress` handler.
16
+ *
17
+ * @property id - Optional identifier to associate this trigger with a `GestureViewer`.
18
+ * @property children - A single React element whose props include `onPress` (e.g., `Pressable`, `Touchable*`).
19
+ * @property onPress - Optional handler invoked after the child's own `onPress`.
20
+ */
21
+ export type GestureTriggerProps<T extends WithOnPress> = {
22
+ id?: string;
23
+ children: ReactElement<T>;
24
+ onPress?: (...args: unknown[]) => void;
25
+ };
26
+
27
+ /**
28
+ * Wraps a pressable child element and registers its native view as a trigger for `GestureViewer`.
29
+ *
30
+ * @remark
31
+ * Behavior on press:
32
+ * - Registers the child's native node to the internal registry under the given `id`.
33
+ * - Invokes the child's own `onPress` first (if provided).
34
+ * - Invokes the `onPress` passed to `GestureTrigger` next (if provided).
35
+ *
36
+ * Type parameters:
37
+ * - `T` — The child's props type. Must include an optional `onPress` handler, ensuring the child is pressable.
38
+ *
39
+ * Props:
40
+ * - `id` — Optional identifier to associate this trigger with a `GestureViewer`. Defaults to `"default"`.
41
+ * - `children` — A single React element whose props include `onPress` (e.g., `Pressable`, `Touchable*`, custom button).
42
+ * - `onPress` — Optional handler invoked after the child's `onPress`. Receives the same arguments as the child's handler.
43
+ *
44
+ * Notes:
45
+ * - If neither the child nor this component provides `onPress`, a dev warning is logged and nothing happens on press.
46
+ * - Only a single child is allowed; wrap lists using `React.Children.map` when needed.
47
+ *
48
+ * Example:
49
+ * ```tsx
50
+ * <GestureTrigger id="gallery" onPress={() => openModal(index)}>
51
+ * <Pressable style={styles.thumb}>
52
+ * <Image source={{ uri }} style={styles.thumbImage} />
53
+ * </Pressable>
54
+ * </GestureTrigger>
55
+ * ```
56
+ */
57
+ export function GestureTrigger<T extends WithOnPress>({ id = 'default', children, onPress }: GestureTriggerProps<T>) {
58
+ const ref = useRef<View>(null);
59
+
60
+ const wrapped = useMemo(() => {
61
+ const child = Children.only(children);
62
+
63
+ if (!isValidElement(child)) {
64
+ return children;
65
+ }
66
+
67
+ const originalOnPress = child.props?.onPress;
68
+
69
+ const handlePress = (...args: unknown[]) => {
70
+ registry.setTriggerNode(id, ref.current);
71
+ originalOnPress?.(...args);
72
+ onPress?.(...args);
73
+
74
+ if (__DEV__ && !originalOnPress && !onPress) {
75
+ console.warn('[GestureTrigger] No onPress found on child or props. Nothing will happen on press.');
76
+ }
77
+ };
78
+
79
+ return cloneElement(child, {
80
+ ...child.props,
81
+ onPress: handlePress,
82
+ });
83
+ }, [id, onPress, children]);
84
+
85
+ return <View ref={ref}>{wrapped}</View>;
86
+ }
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from 'react';
2
- import { type FlatList, Platform, type ScrollViewProps, StyleSheet, useWindowDimensions, View } from 'react-native';
2
+ import { Platform, type ScrollView, type ScrollViewProps, StyleSheet, useWindowDimensions, View } from 'react-native';
3
3
  import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
4
4
  import Animated from 'react-native-reanimated';
5
5
  import { registry } from './GestureViewerRegistry';
@@ -8,7 +8,7 @@ import { useGestureViewer } from './useGestureViewer';
8
8
  import { createLoopData, isFlashListLike, isFlatListLike, isScrollViewLike } from './utils';
9
9
  import WebPagingFixStyle from './WebPagingFixStyle';
10
10
 
11
- export function GestureViewer<T = any, LC = typeof FlatList>({
11
+ export function GestureViewer<T = any, LC = typeof ScrollView>({
12
12
  id = 'default',
13
13
  data,
14
14
  renderItem: renderItemProp,
@@ -20,18 +20,17 @@ export function GestureViewer<T = any, LC = typeof FlatList>({
20
20
  containerStyle,
21
21
  initialIndex = 0,
22
22
  itemSpacing = 0,
23
- useSnap = false,
23
+ enableSnapMode = false,
24
24
  enableLoop = false,
25
25
  ...props
26
26
  }: GestureViewerProps<T, LC>) {
27
27
  const Component = ListComponent as React.ComponentType<any>;
28
28
 
29
29
  const dataRef = useRef(data);
30
- dataRef.current = data;
31
30
 
32
31
  const { width: screenWidth } = useWindowDimensions();
33
32
 
34
- const width = useSnap ? customWidth || screenWidth : screenWidth;
33
+ const width = enableSnapMode ? customWidth || screenWidth : screenWidth;
35
34
 
36
35
  const loopData = useMemo(() => createLoopData(dataRef, enableLoop), [enableLoop]);
37
36
 
@@ -47,13 +46,14 @@ export function GestureViewer<T = any, LC = typeof FlatList>({
47
46
  onScrollBeginDrag,
48
47
  animatedStyle,
49
48
  backdropStyle,
49
+ handleDismiss,
50
50
  } = useGestureViewer({
51
51
  id,
52
52
  data,
53
53
  width,
54
54
  initialIndex,
55
55
  itemSpacing,
56
- useSnap,
56
+ enableSnapMode,
57
57
  enableLoop,
58
58
  ...props,
59
59
  });
@@ -104,6 +104,10 @@ export function GestureViewer<T = any, LC = typeof FlatList>({
104
104
  return Gesture.Race(dismissGesture, zoomGesture);
105
105
  }, [zoomGesture, dismissGesture]);
106
106
 
107
+ useEffect(() => {
108
+ dataRef.current = data;
109
+ }, [data]);
110
+
107
111
  useEffect(() => {
108
112
  registry.createManager(id);
109
113
 
@@ -118,7 +122,7 @@ export function GestureViewer<T = any, LC = typeof FlatList>({
118
122
  showsHorizontalScrollIndicator: false,
119
123
  onMomentumScrollEnd: onMomentumScrollEnd,
120
124
  onScrollBeginDrag,
121
- ...(useSnap
125
+ ...(enableSnapMode
122
126
  ? {
123
127
  snapToInterval: width + itemSpacing,
124
128
  snapToAlignment: 'center',
@@ -130,9 +134,11 @@ export function GestureViewer<T = any, LC = typeof FlatList>({
130
134
  scrollEventThrottle: 16,
131
135
  removeClippedSubviews: true,
132
136
  }) satisfies ScrollViewProps,
133
- [width, itemSpacing, isZoomed, isRotated, onMomentumScrollEnd, onScrollBeginDrag, useSnap],
137
+ [width, itemSpacing, isZoomed, isRotated, onMomentumScrollEnd, onScrollBeginDrag, enableSnapMode],
134
138
  );
135
139
 
140
+ const control = useMemo(() => ({ dismiss: handleDismiss }), [handleDismiss]);
141
+
136
142
  const listComponent = (
137
143
  <GestureHandlerRootView>
138
144
  <GestureDetector gesture={gesture}>
@@ -174,7 +180,7 @@ export function GestureViewer<T = any, LC = typeof FlatList>({
174
180
  </GestureHandlerRootView>
175
181
  );
176
182
 
177
- return renderContainer ? renderContainer(listComponent) : listComponent;
183
+ return renderContainer ? renderContainer(listComponent, control) : listComponent;
178
184
  }
179
185
 
180
186
  const styles = StyleSheet.create({
@@ -1,9 +1,9 @@
1
1
  import { type SharedValue, withTiming } from 'react-native-reanimated';
2
2
  import type {
3
- GestureViewerControllerState,
4
3
  GestureViewerEventCallback,
5
4
  GestureViewerEventData,
6
5
  GestureViewerEventType,
6
+ GestureViewerState,
7
7
  } from './types';
8
8
  import { createBoundsConstraint, createScrollAction } from './utils';
9
9
 
@@ -13,7 +13,7 @@ class GestureViewerManager {
13
13
  private width = 0;
14
14
  private height = 0;
15
15
  private maxZoomScale = 2;
16
- private enableSwipeGesture = true;
16
+ private enableHorizontalSwipe = true;
17
17
  private enableLoop = false;
18
18
  private listRef: any | null = null;
19
19
 
@@ -24,7 +24,7 @@ class GestureViewerManager {
24
24
 
25
25
  private loopCallback: (() => void) | null = null;
26
26
 
27
- private listeners = new Set<(state: GestureViewerControllerState) => void>();
27
+ private listeners = new Set<(state: GestureViewerState) => void>();
28
28
  private eventListeners = new Map<GestureViewerEventType, Set<(data: any) => void>>();
29
29
 
30
30
  private notifyListeners() {
@@ -33,7 +33,7 @@ class GestureViewerManager {
33
33
  this.listeners.forEach((listener) => listener(state));
34
34
  }
35
35
 
36
- subscribe(listener: (state: GestureViewerControllerState) => void) {
36
+ subscribe(listener: (state: GestureViewerState) => void) {
37
37
  this.listeners.add(listener);
38
38
 
39
39
  return () => {
@@ -77,7 +77,7 @@ class GestureViewerManager {
77
77
  this.emitEvent('rotationChange', { rotation, previousRotation });
78
78
  };
79
79
 
80
- getState() {
80
+ getState(): GestureViewerState {
81
81
  return {
82
82
  currentIndex: this.currentIndex,
83
83
  totalCount: this.dataLength,
@@ -104,8 +104,8 @@ class GestureViewerManager {
104
104
  this.dataLength = length;
105
105
  }
106
106
 
107
- setEnableSwipeGesture(enabled: boolean) {
108
- this.enableSwipeGesture = enabled;
107
+ setEnableHorizontalSwipe(enabled: boolean) {
108
+ this.enableHorizontalSwipe = enabled;
109
109
  }
110
110
 
111
111
  setCurrentIndex(index: number) {
@@ -224,7 +224,7 @@ class GestureViewerManager {
224
224
  };
225
225
 
226
226
  goToIndex = (index: number) => {
227
- if (!this.enableSwipeGesture || !this.listRef) {
227
+ if (!this.enableHorizontalSwipe || !this.listRef) {
228
228
  return;
229
229
  }
230
230
 
@@ -299,7 +299,7 @@ class GestureViewerManager {
299
299
  this.loopCallback = null;
300
300
  this.listeners.clear();
301
301
  this.listRef = null;
302
- this.enableSwipeGesture = true;
302
+ this.enableHorizontalSwipe = true;
303
303
  this.currentIndex = 0;
304
304
  this.dataLength = 0;
305
305
  this.maxZoomScale = 2;
@@ -1,8 +1,10 @@
1
+ import type { View } from 'react-native';
1
2
  import GestureViewerManager from './GestureViewerManager';
2
3
 
3
4
  class GestureViewerRegistry {
4
5
  private managers = new Map<string, GestureViewerManager>();
5
6
  private subscribers = new Map<string, Set<(manager: GestureViewerManager | null) => void>>();
7
+ private triggers = new Map<string, View | null>();
6
8
 
7
9
  subscribeToManager(id: string, callback: (manager: GestureViewerManager | null) => void) {
8
10
  if (!this.subscribers.has(id)) {
@@ -51,6 +53,8 @@ class GestureViewerRegistry {
51
53
  this.managers.delete(id);
52
54
 
53
55
  this.notifySubscribers(id, null);
56
+
57
+ this.triggers.delete(id);
54
58
  }
55
59
  }
56
60
 
@@ -61,6 +65,18 @@ class GestureViewerRegistry {
61
65
  [...listeners].forEach((callback) => callback(manager));
62
66
  }
63
67
  }
68
+
69
+ setTriggerNode(id: string, node: View | null) {
70
+ this.triggers.set(id, node);
71
+ }
72
+
73
+ getTriggerNode(id: string): View | null {
74
+ return this.triggers.get(id) ?? null;
75
+ }
76
+
77
+ clearTriggerNode(id: string) {
78
+ this.triggers.delete(id);
79
+ }
64
80
  }
65
81
 
66
82
  export const registry = new GestureViewerRegistry();
package/src/index.tsx CHANGED
@@ -1,11 +1,14 @@
1
+ export type { GestureTriggerProps } from './GestureTrigger';
2
+ export { GestureTrigger } from './GestureTrigger';
1
3
  export { GestureViewer } from './GestureViewer';
2
4
  export type {
3
5
  GestureViewerController,
4
- GestureViewerControllerState,
5
6
  GestureViewerEventCallback,
6
7
  GestureViewerEventData,
7
8
  GestureViewerEventType,
8
9
  GestureViewerProps,
10
+ GestureViewerState,
9
11
  } from './types';
10
12
  export { useGestureViewerController } from './useGestureViewerController';
11
13
  export { useGestureViewerEvent } from './useGestureViewerEvent';
14
+ export { useGestureViewerState } from './useGestureViewerState';