@souscheflabs/reanimated-flashlist 0.1.7

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 (104) hide show
  1. package/README.md +282 -0
  2. package/lib/AnimatedFlashList.d.ts +6 -0
  3. package/lib/AnimatedFlashList.d.ts.map +1 -0
  4. package/lib/AnimatedFlashList.js +207 -0
  5. package/lib/AnimatedFlashListItem.d.ts +33 -0
  6. package/lib/AnimatedFlashListItem.d.ts.map +1 -0
  7. package/lib/AnimatedFlashListItem.js +155 -0
  8. package/lib/__tests__/utils/test-utils.d.ts +82 -0
  9. package/lib/__tests__/utils/test-utils.d.ts.map +1 -0
  10. package/lib/__tests__/utils/test-utils.js +115 -0
  11. package/lib/constants/animations.d.ts +39 -0
  12. package/lib/constants/animations.d.ts.map +1 -0
  13. package/lib/constants/animations.js +100 -0
  14. package/lib/constants/drag.d.ts +11 -0
  15. package/lib/constants/drag.d.ts.map +1 -0
  16. package/lib/constants/drag.js +47 -0
  17. package/lib/constants/index.d.ts +3 -0
  18. package/lib/constants/index.d.ts.map +1 -0
  19. package/lib/constants/index.js +18 -0
  20. package/lib/contexts/DragStateContext.d.ts +73 -0
  21. package/lib/contexts/DragStateContext.d.ts.map +1 -0
  22. package/lib/contexts/DragStateContext.js +148 -0
  23. package/lib/contexts/ListAnimationContext.d.ts +104 -0
  24. package/lib/contexts/ListAnimationContext.d.ts.map +1 -0
  25. package/lib/contexts/ListAnimationContext.js +184 -0
  26. package/lib/contexts/index.d.ts +5 -0
  27. package/lib/contexts/index.d.ts.map +1 -0
  28. package/lib/contexts/index.js +10 -0
  29. package/lib/hooks/animations/index.d.ts +9 -0
  30. package/lib/hooks/animations/index.d.ts.map +1 -0
  31. package/lib/hooks/animations/index.js +13 -0
  32. package/lib/hooks/animations/useListEntryAnimation.d.ts +38 -0
  33. package/lib/hooks/animations/useListEntryAnimation.d.ts.map +1 -0
  34. package/lib/hooks/animations/useListEntryAnimation.js +90 -0
  35. package/lib/hooks/animations/useListExitAnimation.d.ts +67 -0
  36. package/lib/hooks/animations/useListExitAnimation.d.ts.map +1 -0
  37. package/lib/hooks/animations/useListExitAnimation.js +146 -0
  38. package/lib/hooks/drag/index.d.ts +20 -0
  39. package/lib/hooks/drag/index.d.ts.map +1 -0
  40. package/lib/hooks/drag/index.js +26 -0
  41. package/lib/hooks/drag/useDragAnimatedStyle.d.ts +33 -0
  42. package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -0
  43. package/lib/hooks/drag/useDragAnimatedStyle.js +61 -0
  44. package/lib/hooks/drag/useDragGesture.d.ts +30 -0
  45. package/lib/hooks/drag/useDragGesture.d.ts.map +1 -0
  46. package/lib/hooks/drag/useDragGesture.js +189 -0
  47. package/lib/hooks/drag/useDragShift.d.ts +21 -0
  48. package/lib/hooks/drag/useDragShift.d.ts.map +1 -0
  49. package/lib/hooks/drag/useDragShift.js +85 -0
  50. package/lib/hooks/drag/useDropCompensation.d.ts +27 -0
  51. package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -0
  52. package/lib/hooks/drag/useDropCompensation.js +90 -0
  53. package/lib/hooks/index.d.ts +8 -0
  54. package/lib/hooks/index.d.ts.map +1 -0
  55. package/lib/hooks/index.js +18 -0
  56. package/lib/index.d.ts +42 -0
  57. package/lib/index.d.ts.map +1 -0
  58. package/lib/index.js +69 -0
  59. package/lib/types/animations.d.ts +71 -0
  60. package/lib/types/animations.d.ts.map +1 -0
  61. package/lib/types/animations.js +2 -0
  62. package/lib/types/drag.d.ts +94 -0
  63. package/lib/types/drag.d.ts.map +1 -0
  64. package/lib/types/drag.js +2 -0
  65. package/lib/types/index.d.ts +4 -0
  66. package/lib/types/index.d.ts.map +1 -0
  67. package/lib/types/index.js +19 -0
  68. package/lib/types/list.d.ts +136 -0
  69. package/lib/types/list.d.ts.map +1 -0
  70. package/lib/types/list.js +2 -0
  71. package/package.json +73 -0
  72. package/src/AnimatedFlashList.tsx +411 -0
  73. package/src/AnimatedFlashListItem.tsx +212 -0
  74. package/src/__tests__/components/AnimatedFlashList.test.tsx +365 -0
  75. package/src/__tests__/components/AnimatedFlashListItem.test.tsx +371 -0
  76. package/src/__tests__/contexts/DragStateContext.test.tsx +169 -0
  77. package/src/__tests__/contexts/ListAnimationContext.test.tsx +324 -0
  78. package/src/__tests__/hooks/useDragAnimatedStyle.test.tsx +118 -0
  79. package/src/__tests__/hooks/useDragGesture.test.tsx +169 -0
  80. package/src/__tests__/hooks/useDragShift.test.tsx +94 -0
  81. package/src/__tests__/hooks/useDropCompensation.test.tsx +182 -0
  82. package/src/__tests__/hooks/useListEntryAnimation.test.tsx +135 -0
  83. package/src/__tests__/hooks/useListExitAnimation.test.tsx +175 -0
  84. package/src/__tests__/utils/test-utils.tsx +159 -0
  85. package/src/constants/animations.ts +107 -0
  86. package/src/constants/drag.ts +51 -0
  87. package/src/constants/index.ts +2 -0
  88. package/src/contexts/DragStateContext.tsx +197 -0
  89. package/src/contexts/ListAnimationContext.tsx +302 -0
  90. package/src/contexts/index.ts +9 -0
  91. package/src/hooks/animations/index.ts +9 -0
  92. package/src/hooks/animations/useListEntryAnimation.ts +108 -0
  93. package/src/hooks/animations/useListExitAnimation.ts +197 -0
  94. package/src/hooks/drag/index.ts +20 -0
  95. package/src/hooks/drag/useDragAnimatedStyle.ts +80 -0
  96. package/src/hooks/drag/useDragGesture.ts +267 -0
  97. package/src/hooks/drag/useDragShift.ts +119 -0
  98. package/src/hooks/drag/useDropCompensation.ts +120 -0
  99. package/src/hooks/index.ts +16 -0
  100. package/src/index.ts +105 -0
  101. package/src/types/animations.ts +76 -0
  102. package/src/types/drag.ts +101 -0
  103. package/src/types/index.ts +3 -0
  104. package/src/types/list.ts +178 -0
@@ -0,0 +1,212 @@
1
+ import React, { useLayoutEffect, useMemo, useCallback, useRef } from 'react';
2
+ import Animated, { useAnimatedRef } from 'react-native-reanimated';
3
+ import type { ViewStyle, LayoutChangeEvent } from 'react-native';
4
+ import {
5
+ useDragGesture,
6
+ useDragShift,
7
+ useDragAnimatedStyle,
8
+ useDropCompensation,
9
+ useListExitAnimation,
10
+ useListEntryAnimation,
11
+ } from './hooks';
12
+ import { useListAnimationOptional } from './contexts';
13
+ import type {
14
+ AnimatedListItem,
15
+ AnimatedRenderItemInfo,
16
+ HapticFeedbackType,
17
+ } from './types';
18
+
19
+ interface AnimatedFlashListItemProps<T extends AnimatedListItem> {
20
+ /** The item data */
21
+ item: T;
22
+ /** Current index in list */
23
+ index: number;
24
+ /** Total number of items */
25
+ totalItems: number;
26
+ /** Whether drag is enabled for this item */
27
+ isDragEnabled: boolean;
28
+ /** Render function from parent */
29
+ renderItem: (info: AnimatedRenderItemInfo<T>) => React.ReactElement;
30
+ /** Callback when reorder occurs */
31
+ onReorderByDelta?: (itemId: string, delta: number) => void;
32
+ /** Optional haptic feedback callback */
33
+ onHapticFeedback?: (type: HapticFeedbackType) => void;
34
+ }
35
+
36
+ /**
37
+ * Internal item wrapper that provides all animation functionality.
38
+ *
39
+ * This component:
40
+ * 1. Sets up drag gesture and shift animations
41
+ * 2. Sets up entry/exit animations
42
+ * 3. Combines all animated styles
43
+ * 4. Passes everything to the consumer's renderItem function
44
+ *
45
+ * @internal
46
+ */
47
+ function AnimatedFlashListItemInner<T extends AnimatedListItem>({
48
+ item,
49
+ index,
50
+ totalItems,
51
+ isDragEnabled,
52
+ renderItem,
53
+ onReorderByDelta,
54
+ onHapticFeedback,
55
+ }: AnimatedFlashListItemProps<T>): React.ReactElement | null {
56
+ // Animated ref for measuring item height on drag start
57
+ const containerRef = useAnimatedRef<Animated.View>();
58
+
59
+ // Track measured height for layout compensation
60
+ const measuredHeightRef = useRef<number>(0);
61
+
62
+ // List animation context for subscription-triggered animations and layout compensation
63
+ const animationContext = useListAnimationOptional();
64
+
65
+ // === DRAG HOOKS ===
66
+
67
+ // Pan gesture for drag-to-reorder
68
+ const { panGesture, isDragging, translateY } = useDragGesture(
69
+ {
70
+ itemId: item.id,
71
+ index,
72
+ totalItems,
73
+ enabled: isDragEnabled,
74
+ containerRef,
75
+ },
76
+ {
77
+ onReorderByDelta,
78
+ onHapticFeedback,
79
+ },
80
+ );
81
+
82
+ // Shift animation for non-dragged items
83
+ const { shiftY } = useDragShift({ itemId: item.id, index });
84
+
85
+ // Handle index changes after cache updates (drop compensation)
86
+ useDropCompensation({ itemId: item.id, index, translateY, shiftY });
87
+
88
+ // Animated style for drag transforms
89
+ const { dragAnimatedStyle } = useDragAnimatedStyle(
90
+ item.id,
91
+ isDragging,
92
+ translateY,
93
+ shiftY,
94
+ );
95
+
96
+ // === ANIMATION HOOKS ===
97
+
98
+ // Callbacks for layout compensation (register/unregister exiting items)
99
+ const onExitStart = useCallback(
100
+ (exitIndex: number, height: number) => {
101
+ animationContext?.registerExitingItem(item.id, exitIndex, height);
102
+ },
103
+ [animationContext, item.id],
104
+ );
105
+
106
+ const onExitComplete = useCallback(() => {
107
+ animationContext?.unregisterExitingItem(item.id);
108
+ }, [animationContext, item.id]);
109
+
110
+ // Exit animation for smooth slide-out (with layout compensation callbacks)
111
+ const { exitAnimatedStyle, triggerExit, resetAnimation } =
112
+ useListExitAnimation(item.id, {
113
+ index,
114
+ measuredHeight: measuredHeightRef.current,
115
+ onExitStart,
116
+ onExitComplete,
117
+ });
118
+
119
+ // Entry animation for items appearing
120
+ const { entryAnimatedStyle } = useListEntryAnimation(item.id);
121
+
122
+ // Register exit animation trigger (O(1) direct calls from subscriptions)
123
+ useLayoutEffect(() => {
124
+ if (!animationContext) return;
125
+ animationContext.registerAnimationTrigger(item.id, triggerExit);
126
+ return () => animationContext.unregisterAnimationTrigger(item.id);
127
+ }, [item.id, triggerExit, animationContext]);
128
+
129
+ // Track measured height for layout compensation
130
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
131
+ measuredHeightRef.current = event.nativeEvent.layout.height;
132
+ }, []);
133
+
134
+ // Create combined animated style
135
+ const combinedAnimatedStyle = useMemo<ViewStyle>(() => {
136
+ // We can't directly combine animated styles here since they're worklet-based
137
+ // Instead, we'll let the consumer apply them via the render prop
138
+ return {};
139
+ }, []);
140
+
141
+ // Create drag handle props
142
+ const dragHandleProps = useMemo(
143
+ () =>
144
+ isDragEnabled
145
+ ? {
146
+ gesture: panGesture,
147
+ isDragging,
148
+ }
149
+ : null,
150
+ [isDragEnabled, panGesture, isDragging],
151
+ );
152
+
153
+ // Trigger exit animation wrapper
154
+ const triggerExitAnimation = useCallback(
155
+ (
156
+ direction: 1 | -1,
157
+ onComplete: () => void,
158
+ preset?: 'default' | 'fast',
159
+ ) => {
160
+ triggerExit(direction, onComplete, preset);
161
+ },
162
+ [triggerExit],
163
+ );
164
+
165
+ // Create render info
166
+ const renderInfo = useMemo<AnimatedRenderItemInfo<T>>(
167
+ () => ({
168
+ item,
169
+ index,
170
+ totalItems,
171
+ animatedStyle: combinedAnimatedStyle,
172
+ dragHandleProps,
173
+ isDragging: false, // This is a SharedValue, consumer should use dragHandleProps.isDragging
174
+ isDragEnabled,
175
+ triggerExitAnimation,
176
+ resetExitAnimation: resetAnimation,
177
+ }),
178
+ [
179
+ item,
180
+ index,
181
+ totalItems,
182
+ combinedAnimatedStyle,
183
+ dragHandleProps,
184
+ isDragEnabled,
185
+ triggerExitAnimation,
186
+ resetAnimation,
187
+ ],
188
+ );
189
+
190
+ // Render the item with animations applied
191
+ // The consumer's renderItem gets wrapped in our animated container
192
+ const renderedItem = renderItem(renderInfo);
193
+
194
+ return (
195
+ <Animated.View
196
+ ref={containerRef}
197
+ onLayout={handleLayout}
198
+ style={[
199
+ exitAnimatedStyle,
200
+ entryAnimatedStyle,
201
+ isDragEnabled && dragAnimatedStyle,
202
+ ]}
203
+ >
204
+ {renderedItem}
205
+ </Animated.View>
206
+ );
207
+ }
208
+
209
+ // Memoize to prevent unnecessary re-renders
210
+ export const AnimatedFlashListItem = React.memo(
211
+ AnimatedFlashListItemInner,
212
+ ) as typeof AnimatedFlashListItemInner;
@@ -0,0 +1,365 @@
1
+ import React from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import { render } from '@testing-library/react-native';
4
+ import { AnimatedFlashList } from '../../AnimatedFlashList';
5
+ import type { AnimatedRenderItemInfo } from '../../types';
6
+
7
+ interface TestItem {
8
+ id: string;
9
+ title: string;
10
+ }
11
+
12
+ const createTestItems = (count: number): TestItem[] =>
13
+ Array.from({ length: count }, (_, i) => ({
14
+ id: `item-${i + 1}`,
15
+ title: `Test Item ${i + 1}`,
16
+ }));
17
+
18
+ describe('AnimatedFlashList', () => {
19
+ const defaultRenderItem = ({ item }: AnimatedRenderItemInfo<TestItem>) => (
20
+ <View testID={`item-${item.id}`}>
21
+ <Text>{item.title}</Text>
22
+ </View>
23
+ );
24
+
25
+ const defaultKeyExtractor = (item: TestItem) => item.id;
26
+
27
+ it('renders without crashing', () => {
28
+ const data = createTestItems(3);
29
+
30
+ expect(() => {
31
+ render(
32
+ <AnimatedFlashList<TestItem>
33
+ data={data}
34
+ keyExtractor={defaultKeyExtractor}
35
+ renderItem={defaultRenderItem}
36
+ estimatedItemSize={80}
37
+ />,
38
+ );
39
+ }).not.toThrow();
40
+ });
41
+
42
+ it('renders items from data array', () => {
43
+ const data = createTestItems(3);
44
+
45
+ const { getByText } = render(
46
+ <AnimatedFlashList<TestItem>
47
+ data={data}
48
+ keyExtractor={defaultKeyExtractor}
49
+ renderItem={defaultRenderItem}
50
+ estimatedItemSize={80}
51
+ />,
52
+ );
53
+
54
+ expect(getByText('Test Item 1')).toBeTruthy();
55
+ expect(getByText('Test Item 2')).toBeTruthy();
56
+ expect(getByText('Test Item 3')).toBeTruthy();
57
+ });
58
+
59
+ it('returns null for empty data when no ListFooterComponent', () => {
60
+ const { toJSON } = render(
61
+ <AnimatedFlashList<TestItem>
62
+ data={[]}
63
+ keyExtractor={defaultKeyExtractor}
64
+ renderItem={defaultRenderItem}
65
+ estimatedItemSize={80}
66
+ />,
67
+ );
68
+
69
+ expect(toJSON()).toBeNull();
70
+ });
71
+
72
+ it('renders ListFooterComponent for empty data', () => {
73
+ const FooterComponent = () => (
74
+ <View testID="footer">
75
+ <Text>No items</Text>
76
+ </View>
77
+ );
78
+
79
+ const { getByText } = render(
80
+ <AnimatedFlashList<TestItem>
81
+ data={[]}
82
+ keyExtractor={defaultKeyExtractor}
83
+ renderItem={defaultRenderItem}
84
+ estimatedItemSize={80}
85
+ ListFooterComponent={FooterComponent}
86
+ />,
87
+ );
88
+
89
+ expect(getByText('No items')).toBeTruthy();
90
+ });
91
+
92
+ it('supports dragEnabled prop', () => {
93
+ const data = createTestItems(3);
94
+
95
+ expect(() => {
96
+ render(
97
+ <AnimatedFlashList<TestItem>
98
+ data={data}
99
+ keyExtractor={defaultKeyExtractor}
100
+ renderItem={defaultRenderItem}
101
+ dragEnabled
102
+ estimatedItemSize={80}
103
+ />,
104
+ );
105
+ }).not.toThrow();
106
+ });
107
+
108
+ it('supports dragEnabled=false (disabled)', () => {
109
+ const data = createTestItems(3);
110
+
111
+ expect(() => {
112
+ render(
113
+ <AnimatedFlashList<TestItem>
114
+ data={data}
115
+ keyExtractor={defaultKeyExtractor}
116
+ renderItem={defaultRenderItem}
117
+ dragEnabled={false}
118
+ estimatedItemSize={80}
119
+ />,
120
+ );
121
+ }).not.toThrow();
122
+ });
123
+
124
+ it('calls onReorder callback when provided', () => {
125
+ const data = createTestItems(3);
126
+ const onReorder = jest.fn();
127
+
128
+ render(
129
+ <AnimatedFlashList<TestItem>
130
+ data={data}
131
+ keyExtractor={defaultKeyExtractor}
132
+ renderItem={defaultRenderItem}
133
+ dragEnabled
134
+ onReorder={onReorder}
135
+ estimatedItemSize={80}
136
+ />,
137
+ );
138
+
139
+ // onReorder should be provided but not called until user drags
140
+ expect(onReorder).not.toHaveBeenCalled();
141
+ });
142
+
143
+ it('supports canDrag filter function', () => {
144
+ const data = createTestItems(5);
145
+ const canDrag = (item: TestItem, index: number) => index % 2 === 0;
146
+
147
+ expect(() => {
148
+ render(
149
+ <AnimatedFlashList<TestItem>
150
+ data={data}
151
+ keyExtractor={defaultKeyExtractor}
152
+ renderItem={defaultRenderItem}
153
+ dragEnabled
154
+ canDrag={canDrag}
155
+ estimatedItemSize={80}
156
+ />,
157
+ );
158
+ }).not.toThrow();
159
+ });
160
+
161
+ it('supports custom drag config', () => {
162
+ const data = createTestItems(3);
163
+
164
+ expect(() => {
165
+ render(
166
+ <AnimatedFlashList<TestItem>
167
+ data={data}
168
+ keyExtractor={defaultKeyExtractor}
169
+ renderItem={defaultRenderItem}
170
+ dragEnabled
171
+ config={{
172
+ drag: {
173
+ longPressDuration: 500,
174
+ dragScale: 1.1,
175
+ itemHeight: 100,
176
+ },
177
+ }}
178
+ estimatedItemSize={80}
179
+ />,
180
+ );
181
+ }).not.toThrow();
182
+ });
183
+
184
+ it('supports haptic feedback callback', () => {
185
+ const data = createTestItems(3);
186
+ const onHapticFeedback = jest.fn();
187
+
188
+ render(
189
+ <AnimatedFlashList<TestItem>
190
+ data={data}
191
+ keyExtractor={defaultKeyExtractor}
192
+ renderItem={defaultRenderItem}
193
+ dragEnabled
194
+ onHapticFeedback={onHapticFeedback}
195
+ estimatedItemSize={80}
196
+ />,
197
+ );
198
+
199
+ // Haptic feedback should not be called until drag starts
200
+ expect(onHapticFeedback).not.toHaveBeenCalled();
201
+ });
202
+
203
+ it('supports onRefresh and refreshing props', () => {
204
+ const data = createTestItems(3);
205
+ const onRefresh = jest.fn();
206
+
207
+ expect(() => {
208
+ render(
209
+ <AnimatedFlashList<TestItem>
210
+ data={data}
211
+ keyExtractor={defaultKeyExtractor}
212
+ renderItem={defaultRenderItem}
213
+ onRefresh={onRefresh}
214
+ refreshing={false}
215
+ estimatedItemSize={80}
216
+ />,
217
+ );
218
+ }).not.toThrow();
219
+ });
220
+
221
+ it('supports onEndReached prop', () => {
222
+ const data = createTestItems(3);
223
+ const onEndReached = jest.fn();
224
+
225
+ expect(() => {
226
+ render(
227
+ <AnimatedFlashList<TestItem>
228
+ data={data}
229
+ keyExtractor={defaultKeyExtractor}
230
+ renderItem={defaultRenderItem}
231
+ onEndReached={onEndReached}
232
+ onEndReachedThreshold={0.5}
233
+ estimatedItemSize={80}
234
+ />,
235
+ );
236
+ }).not.toThrow();
237
+ });
238
+
239
+ it('supports contentContainerStyle prop', () => {
240
+ const data = createTestItems(3);
241
+
242
+ expect(() => {
243
+ render(
244
+ <AnimatedFlashList<TestItem>
245
+ data={data}
246
+ keyExtractor={defaultKeyExtractor}
247
+ renderItem={defaultRenderItem}
248
+ contentContainerStyle={{ padding: 16 }}
249
+ estimatedItemSize={80}
250
+ />,
251
+ );
252
+ }).not.toThrow();
253
+ });
254
+
255
+ it('re-renders when data changes', () => {
256
+ const data1 = createTestItems(2);
257
+ const data2 = createTestItems(4);
258
+
259
+ const { rerender, getByText, queryByText } = render(
260
+ <AnimatedFlashList<TestItem>
261
+ data={data1}
262
+ keyExtractor={defaultKeyExtractor}
263
+ renderItem={defaultRenderItem}
264
+ estimatedItemSize={80}
265
+ />,
266
+ );
267
+
268
+ expect(getByText('Test Item 1')).toBeTruthy();
269
+ expect(getByText('Test Item 2')).toBeTruthy();
270
+ expect(queryByText('Test Item 3')).toBeNull();
271
+
272
+ rerender(
273
+ <AnimatedFlashList<TestItem>
274
+ data={data2}
275
+ keyExtractor={defaultKeyExtractor}
276
+ renderItem={defaultRenderItem}
277
+ estimatedItemSize={80}
278
+ />,
279
+ );
280
+
281
+ expect(getByText('Test Item 3')).toBeTruthy();
282
+ expect(getByText('Test Item 4')).toBeTruthy();
283
+ });
284
+
285
+ it('passes dragHandleProps when drag is enabled', () => {
286
+ const data = createTestItems(1);
287
+ let receivedDragHandleProps: unknown = null;
288
+
289
+ const renderItem = ({ item, dragHandleProps }: AnimatedRenderItemInfo<TestItem>) => {
290
+ receivedDragHandleProps = dragHandleProps;
291
+ return (
292
+ <View>
293
+ <Text>{item.title}</Text>
294
+ </View>
295
+ );
296
+ };
297
+
298
+ render(
299
+ <AnimatedFlashList<TestItem>
300
+ data={data}
301
+ keyExtractor={defaultKeyExtractor}
302
+ renderItem={renderItem}
303
+ dragEnabled
304
+ estimatedItemSize={80}
305
+ />,
306
+ );
307
+
308
+ expect(receivedDragHandleProps).not.toBeNull();
309
+ expect((receivedDragHandleProps as any).gesture).toBeDefined();
310
+ });
311
+
312
+ it('passes null dragHandleProps when drag is disabled', () => {
313
+ const data = createTestItems(1);
314
+ let receivedDragHandleProps: unknown = 'initial';
315
+
316
+ const renderItem = ({ item, dragHandleProps }: AnimatedRenderItemInfo<TestItem>) => {
317
+ receivedDragHandleProps = dragHandleProps;
318
+ return (
319
+ <View>
320
+ <Text>{item.title}</Text>
321
+ </View>
322
+ );
323
+ };
324
+
325
+ render(
326
+ <AnimatedFlashList<TestItem>
327
+ data={data}
328
+ keyExtractor={defaultKeyExtractor}
329
+ renderItem={renderItem}
330
+ dragEnabled={false}
331
+ estimatedItemSize={80}
332
+ />,
333
+ );
334
+
335
+ expect(receivedDragHandleProps).toBeNull();
336
+ });
337
+
338
+ it('provides triggerExitAnimation function', () => {
339
+ const data = createTestItems(1);
340
+ let receivedTriggerExit: unknown = null;
341
+
342
+ const renderItem = ({
343
+ item,
344
+ triggerExitAnimation,
345
+ }: AnimatedRenderItemInfo<TestItem>) => {
346
+ receivedTriggerExit = triggerExitAnimation;
347
+ return (
348
+ <View>
349
+ <Text>{item.title}</Text>
350
+ </View>
351
+ );
352
+ };
353
+
354
+ render(
355
+ <AnimatedFlashList<TestItem>
356
+ data={data}
357
+ keyExtractor={defaultKeyExtractor}
358
+ renderItem={renderItem}
359
+ estimatedItemSize={80}
360
+ />,
361
+ );
362
+
363
+ expect(typeof receivedTriggerExit).toBe('function');
364
+ });
365
+ });