@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.
- package/README.md +282 -0
- package/lib/AnimatedFlashList.d.ts +6 -0
- package/lib/AnimatedFlashList.d.ts.map +1 -0
- package/lib/AnimatedFlashList.js +207 -0
- package/lib/AnimatedFlashListItem.d.ts +33 -0
- package/lib/AnimatedFlashListItem.d.ts.map +1 -0
- package/lib/AnimatedFlashListItem.js +155 -0
- package/lib/__tests__/utils/test-utils.d.ts +82 -0
- package/lib/__tests__/utils/test-utils.d.ts.map +1 -0
- package/lib/__tests__/utils/test-utils.js +115 -0
- package/lib/constants/animations.d.ts +39 -0
- package/lib/constants/animations.d.ts.map +1 -0
- package/lib/constants/animations.js +100 -0
- package/lib/constants/drag.d.ts +11 -0
- package/lib/constants/drag.d.ts.map +1 -0
- package/lib/constants/drag.js +47 -0
- package/lib/constants/index.d.ts +3 -0
- package/lib/constants/index.d.ts.map +1 -0
- package/lib/constants/index.js +18 -0
- package/lib/contexts/DragStateContext.d.ts +73 -0
- package/lib/contexts/DragStateContext.d.ts.map +1 -0
- package/lib/contexts/DragStateContext.js +148 -0
- package/lib/contexts/ListAnimationContext.d.ts +104 -0
- package/lib/contexts/ListAnimationContext.d.ts.map +1 -0
- package/lib/contexts/ListAnimationContext.js +184 -0
- package/lib/contexts/index.d.ts +5 -0
- package/lib/contexts/index.d.ts.map +1 -0
- package/lib/contexts/index.js +10 -0
- package/lib/hooks/animations/index.d.ts +9 -0
- package/lib/hooks/animations/index.d.ts.map +1 -0
- package/lib/hooks/animations/index.js +13 -0
- package/lib/hooks/animations/useListEntryAnimation.d.ts +38 -0
- package/lib/hooks/animations/useListEntryAnimation.d.ts.map +1 -0
- package/lib/hooks/animations/useListEntryAnimation.js +90 -0
- package/lib/hooks/animations/useListExitAnimation.d.ts +67 -0
- package/lib/hooks/animations/useListExitAnimation.d.ts.map +1 -0
- package/lib/hooks/animations/useListExitAnimation.js +146 -0
- package/lib/hooks/drag/index.d.ts +20 -0
- package/lib/hooks/drag/index.d.ts.map +1 -0
- package/lib/hooks/drag/index.js +26 -0
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts +33 -0
- package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -0
- package/lib/hooks/drag/useDragAnimatedStyle.js +61 -0
- package/lib/hooks/drag/useDragGesture.d.ts +30 -0
- package/lib/hooks/drag/useDragGesture.d.ts.map +1 -0
- package/lib/hooks/drag/useDragGesture.js +189 -0
- package/lib/hooks/drag/useDragShift.d.ts +21 -0
- package/lib/hooks/drag/useDragShift.d.ts.map +1 -0
- package/lib/hooks/drag/useDragShift.js +85 -0
- package/lib/hooks/drag/useDropCompensation.d.ts +27 -0
- package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -0
- package/lib/hooks/drag/useDropCompensation.js +90 -0
- package/lib/hooks/index.d.ts +8 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +18 -0
- package/lib/index.d.ts +42 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +69 -0
- package/lib/types/animations.d.ts +71 -0
- package/lib/types/animations.d.ts.map +1 -0
- package/lib/types/animations.js +2 -0
- package/lib/types/drag.d.ts +94 -0
- package/lib/types/drag.d.ts.map +1 -0
- package/lib/types/drag.js +2 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +19 -0
- package/lib/types/list.d.ts +136 -0
- package/lib/types/list.d.ts.map +1 -0
- package/lib/types/list.js +2 -0
- package/package.json +73 -0
- package/src/AnimatedFlashList.tsx +411 -0
- package/src/AnimatedFlashListItem.tsx +212 -0
- package/src/__tests__/components/AnimatedFlashList.test.tsx +365 -0
- package/src/__tests__/components/AnimatedFlashListItem.test.tsx +371 -0
- package/src/__tests__/contexts/DragStateContext.test.tsx +169 -0
- package/src/__tests__/contexts/ListAnimationContext.test.tsx +324 -0
- package/src/__tests__/hooks/useDragAnimatedStyle.test.tsx +118 -0
- package/src/__tests__/hooks/useDragGesture.test.tsx +169 -0
- package/src/__tests__/hooks/useDragShift.test.tsx +94 -0
- package/src/__tests__/hooks/useDropCompensation.test.tsx +182 -0
- package/src/__tests__/hooks/useListEntryAnimation.test.tsx +135 -0
- package/src/__tests__/hooks/useListExitAnimation.test.tsx +175 -0
- package/src/__tests__/utils/test-utils.tsx +159 -0
- package/src/constants/animations.ts +107 -0
- package/src/constants/drag.ts +51 -0
- package/src/constants/index.ts +2 -0
- package/src/contexts/DragStateContext.tsx +197 -0
- package/src/contexts/ListAnimationContext.tsx +302 -0
- package/src/contexts/index.ts +9 -0
- package/src/hooks/animations/index.ts +9 -0
- package/src/hooks/animations/useListEntryAnimation.ts +108 -0
- package/src/hooks/animations/useListExitAnimation.ts +197 -0
- package/src/hooks/drag/index.ts +20 -0
- package/src/hooks/drag/useDragAnimatedStyle.ts +80 -0
- package/src/hooks/drag/useDragGesture.ts +267 -0
- package/src/hooks/drag/useDragShift.ts +119 -0
- package/src/hooks/drag/useDropCompensation.ts +120 -0
- package/src/hooks/index.ts +16 -0
- package/src/index.ts +105 -0
- package/src/types/animations.ts +76 -0
- package/src/types/drag.ts +101 -0
- package/src/types/index.ts +3 -0
- 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
|
+
});
|