@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
package/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# @souschef/reanimated-flashlist
|
|
2
|
+
|
|
3
|
+
A high-performance animated FlashList with drag-to-reorder and entry/exit animations for React Native.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Drag-to-reorder** with smooth animations and autoscroll
|
|
8
|
+
- **Entry animations** when items appear in the list
|
|
9
|
+
- **Exit animations** with configurable slide/fade/scale effects
|
|
10
|
+
- **Layout animations** for remaining items when one is removed
|
|
11
|
+
- **60fps performance** via Reanimated UI-thread animations
|
|
12
|
+
- **Full TypeScript support** with generics
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### From npm (when published)
|
|
17
|
+
```bash
|
|
18
|
+
npm install @souschef/reanimated-flashlist
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### From GitHub (private repo)
|
|
22
|
+
|
|
23
|
+
Using HTTPS (will prompt for credentials):
|
|
24
|
+
```bash
|
|
25
|
+
npm install github:SousChefLabs/reanimated-flashlist#v0.1.1
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Using SSH (recommended for CI/private repos):
|
|
29
|
+
```bash
|
|
30
|
+
npm install git+ssh://git@github.com:SousChefLabs/reanimated-flashlist.git#v0.1.1
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or add to `package.json`:
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@souschef/reanimated-flashlist": "github:SousChefLabs/reanimated-flashlist#v0.1.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Replace `v0.1.1` with the desired version tag.
|
|
43
|
+
|
|
44
|
+
### Requirements
|
|
45
|
+
|
|
46
|
+
**React Native New Architecture required.** This library uses TurboModules and JSI for optimal performance.
|
|
47
|
+
|
|
48
|
+
- React Native 0.74.0+ with [New Architecture enabled](https://reactnative.dev/architecture/landing-page)
|
|
49
|
+
- `@shopify/flash-list` v2.0.0+
|
|
50
|
+
- `react-native-reanimated` v4.0.0+
|
|
51
|
+
- `react-native-gesture-handler` v2.14.0+
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install @shopify/flash-list react-native-reanimated react-native-gesture-handler
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { AnimatedFlashList, type AnimatedListItem } from '@souschef/reanimated-flashlist';
|
|
61
|
+
import { GestureDetector } from 'react-native-gesture-handler';
|
|
62
|
+
|
|
63
|
+
interface MyItem extends AnimatedListItem {
|
|
64
|
+
title: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function MyList() {
|
|
68
|
+
const [items, setItems] = useState<MyItem[]>([
|
|
69
|
+
{ id: '1', title: 'Item 1' },
|
|
70
|
+
{ id: '2', title: 'Item 2' },
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<AnimatedFlashList<MyItem>
|
|
75
|
+
data={items}
|
|
76
|
+
keyExtractor={(item) => item.id}
|
|
77
|
+
renderItem={({ item, dragHandleProps, triggerExitAnimation }) => (
|
|
78
|
+
<View style={styles.item}>
|
|
79
|
+
<Text>{item.title}</Text>
|
|
80
|
+
|
|
81
|
+
{/* Drag handle */}
|
|
82
|
+
{dragHandleProps && (
|
|
83
|
+
<GestureDetector gesture={dragHandleProps.gesture}>
|
|
84
|
+
<View style={styles.dragHandle}>
|
|
85
|
+
<DragIcon />
|
|
86
|
+
</View>
|
|
87
|
+
</GestureDetector>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Delete button with exit animation */}
|
|
91
|
+
<TouchableOpacity
|
|
92
|
+
onPress={() => {
|
|
93
|
+
triggerExitAnimation(1, () => {
|
|
94
|
+
setItems(prev => prev.filter(i => i.id !== item.id));
|
|
95
|
+
}, 'fast');
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<DeleteIcon />
|
|
99
|
+
</TouchableOpacity>
|
|
100
|
+
</View>
|
|
101
|
+
)}
|
|
102
|
+
dragEnabled
|
|
103
|
+
onReorder={(itemId, fromIndex, toIndex) => {
|
|
104
|
+
setItems(prev => {
|
|
105
|
+
const newItems = [...prev];
|
|
106
|
+
const [removed] = newItems.splice(fromIndex, 1);
|
|
107
|
+
newItems.splice(toIndex, 0, removed);
|
|
108
|
+
return newItems;
|
|
109
|
+
});
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### AnimatedFlashListProps
|
|
119
|
+
|
|
120
|
+
| Prop | Type | Default | Description |
|
|
121
|
+
|------|------|---------|-------------|
|
|
122
|
+
| `data` | `T[]` | Required | Array of items (must extend `AnimatedListItem`) |
|
|
123
|
+
| `keyExtractor` | `(item: T, index: number) => string` | Required | Extract unique key for each item |
|
|
124
|
+
| `renderItem` | `(info: AnimatedRenderItemInfo<T>) => ReactElement` | Required | Render function for items |
|
|
125
|
+
| `dragEnabled` | `boolean` | `false` | Enable drag-to-reorder |
|
|
126
|
+
| `onReorder` | `(itemId, fromIndex, toIndex) => void` | - | Called when item is reordered |
|
|
127
|
+
| `onReorderByNeighbors` | `(itemId, afterId, beforeId) => void` | - | Alternative callback with neighbor IDs (for fractional indexing) |
|
|
128
|
+
| `canDrag` | `(item: T, index: number) => boolean` | `() => true` | Control which items can be dragged |
|
|
129
|
+
| `onHapticFeedback` | `(type: HapticFeedbackType) => void` | - | Haptic feedback callback |
|
|
130
|
+
| `config` | `AnimatedFlashListConfig` | - | Configuration overrides |
|
|
131
|
+
|
|
132
|
+
### AnimatedRenderItemInfo
|
|
133
|
+
|
|
134
|
+
Props passed to your `renderItem` function:
|
|
135
|
+
|
|
136
|
+
| Prop | Type | Description |
|
|
137
|
+
|------|------|-------------|
|
|
138
|
+
| `item` | `T` | The item data |
|
|
139
|
+
| `index` | `number` | Current index |
|
|
140
|
+
| `totalItems` | `number` | Total items in list |
|
|
141
|
+
| `dragHandleProps` | `{ gesture, isDragging } \| null` | Spread onto GestureDetector for drag handle |
|
|
142
|
+
| `isDragEnabled` | `boolean` | Whether drag is enabled for this item |
|
|
143
|
+
| `triggerExitAnimation` | `(direction, onComplete, preset?) => void` | Trigger exit animation |
|
|
144
|
+
| `resetExitAnimation` | `() => void` | Reset exit animation state |
|
|
145
|
+
|
|
146
|
+
## Recipes
|
|
147
|
+
|
|
148
|
+
### Checkbox Toggle with Exit Animation
|
|
149
|
+
|
|
150
|
+
When a checkbox is toggled and the item should exit:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
renderItem={({ item, triggerExitAnimation }) => (
|
|
154
|
+
<View style={styles.item}>
|
|
155
|
+
<Checkbox
|
|
156
|
+
checked={item.completed}
|
|
157
|
+
onToggle={() => {
|
|
158
|
+
// Trigger exit animation, then remove item
|
|
159
|
+
triggerExitAnimation(1, () => {
|
|
160
|
+
removeItem(item.id);
|
|
161
|
+
}, 'fast'); // 'fast' preset for quick actions
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
<Text>{item.title}</Text>
|
|
165
|
+
</View>
|
|
166
|
+
)}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Swipe to Delete
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
renderItem={({ item, triggerExitAnimation }) => (
|
|
173
|
+
<Swipeable
|
|
174
|
+
onSwipeRight={() => {
|
|
175
|
+
triggerExitAnimation(1, () => deleteItem(item.id));
|
|
176
|
+
}}
|
|
177
|
+
onSwipeLeft={() => {
|
|
178
|
+
triggerExitAnimation(-1, () => deleteItem(item.id));
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<ItemContent item={item} />
|
|
182
|
+
</Swipeable>
|
|
183
|
+
)}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Custom Drag Handle
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
renderItem={({ item, dragHandleProps }) => (
|
|
190
|
+
<View style={styles.item}>
|
|
191
|
+
<Text>{item.title}</Text>
|
|
192
|
+
|
|
193
|
+
{dragHandleProps && (
|
|
194
|
+
<GestureDetector gesture={dragHandleProps.gesture}>
|
|
195
|
+
<Animated.View
|
|
196
|
+
style={[
|
|
197
|
+
styles.dragHandle,
|
|
198
|
+
// Optional: visual feedback during drag
|
|
199
|
+
useAnimatedStyle(() => ({
|
|
200
|
+
opacity: dragHandleProps.isDragging.value ? 0.5 : 1,
|
|
201
|
+
})),
|
|
202
|
+
]}
|
|
203
|
+
>
|
|
204
|
+
<GripIcon />
|
|
205
|
+
</Animated.View>
|
|
206
|
+
</GestureDetector>
|
|
207
|
+
)}
|
|
208
|
+
</View>
|
|
209
|
+
)}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Configuration
|
|
213
|
+
|
|
214
|
+
### Drag Configuration
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
<AnimatedFlashList
|
|
218
|
+
config={{
|
|
219
|
+
drag: {
|
|
220
|
+
itemHeight: 80, // Item height for drag calculations
|
|
221
|
+
dragScale: 1.05, // Scale when dragging
|
|
222
|
+
longPressDuration: 200, // ms to activate drag
|
|
223
|
+
edgeThreshold: 80, // px from edge to trigger autoscroll
|
|
224
|
+
maxScrollSpeed: 10, // Autoscroll speed
|
|
225
|
+
},
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Animation Presets
|
|
231
|
+
|
|
232
|
+
Exit animations have two presets:
|
|
233
|
+
|
|
234
|
+
- **`'default'`** (300ms): Standard exit with staggered fade/scale
|
|
235
|
+
- **`'fast'`** (200ms): Quick exit for checkbox toggles
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
// Use fast preset for quick actions
|
|
239
|
+
triggerExitAnimation(1, onComplete, 'fast');
|
|
240
|
+
|
|
241
|
+
// Use default preset for deliberate actions
|
|
242
|
+
triggerExitAnimation(-1, onComplete, 'default');
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Layout Animations
|
|
246
|
+
|
|
247
|
+
When an item exits the list, remaining items automatically animate to fill the gap. This uses React Native's `LayoutAnimation` API and is enabled by default.
|
|
248
|
+
|
|
249
|
+
The animation is triggered automatically when `triggerExitAnimation` is called - no additional setup required.
|
|
250
|
+
|
|
251
|
+
## Advanced Usage
|
|
252
|
+
|
|
253
|
+
### Using Individual Hooks
|
|
254
|
+
|
|
255
|
+
For custom implementations, you can use the hooks directly:
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
import {
|
|
259
|
+
useDragGesture,
|
|
260
|
+
useDragShift,
|
|
261
|
+
useDragAnimatedStyle,
|
|
262
|
+
useListExitAnimation,
|
|
263
|
+
useListEntryAnimation,
|
|
264
|
+
} from '@souschef/reanimated-flashlist';
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Context Providers
|
|
268
|
+
|
|
269
|
+
For building custom list implementations:
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
import {
|
|
273
|
+
DragStateProvider,
|
|
274
|
+
ListAnimationProvider,
|
|
275
|
+
useDragState,
|
|
276
|
+
useListAnimation,
|
|
277
|
+
} from '@souschef/reanimated-flashlist';
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AnimatedListItem, AnimatedFlashListProps, AnimatedFlashListRef } from './types';
|
|
3
|
+
export declare const AnimatedFlashList: <T extends AnimatedListItem>(props: AnimatedFlashListProps<T> & {
|
|
4
|
+
ref?: React.ForwardedRef<AnimatedFlashListRef>;
|
|
5
|
+
}) => React.ReactElement | null;
|
|
6
|
+
//# sourceMappingURL=AnimatedFlashList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnimatedFlashList.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAaf,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EAErB,MAAM,SAAS,CAAC;AA6XjB,eAAO,MAAM,iBAAiB,EAAyC,CACrE,CAAC,SAAS,gBAAgB,EAE1B,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAA;CAAE,KAClF,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AnimatedFlashList = void 0;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const react_native_1 = require("react-native");
|
|
39
|
+
const flash_list_1 = require("@shopify/flash-list");
|
|
40
|
+
const contexts_1 = require("./contexts");
|
|
41
|
+
const AnimatedFlashListItem_1 = require("./AnimatedFlashListItem");
|
|
42
|
+
const constants_1 = require("./constants");
|
|
43
|
+
const ItemWrapper = react_1.default.memo(function ItemWrapper({ item, index, totalItemsRef, renderItem, canDrag, dragEnabled, onReorderByDelta, onHapticFeedback, }) {
|
|
44
|
+
const isDragEnabled = dragEnabled && (canDrag ? canDrag(item, index) : true);
|
|
45
|
+
return (<AnimatedFlashListItem_1.AnimatedFlashListItem item={item} index={index} totalItems={totalItemsRef.current ?? 0} isDragEnabled={isDragEnabled} renderItem={renderItem} onReorderByDelta={onReorderByDelta} onHapticFeedback={onHapticFeedback}/>);
|
|
46
|
+
});
|
|
47
|
+
function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtractor, canDrag, dragEnabled, onReorderByDelta, onHapticFeedback, itemHeight, ListFooterComponent, onEndReached, onEndReachedThreshold, onRefresh, refreshing, refreshTintColor, contentContainerStyle, drawDistance = 500, showsVerticalScrollIndicator = true, }) {
|
|
48
|
+
// Get drag state context for scroll tracking
|
|
49
|
+
const { scrollOffset, contentHeight, visibleHeight, listTopY, setListRef } = (0, contexts_1.useDragState)();
|
|
50
|
+
// Register FlashList ref with drag context for autoscroll
|
|
51
|
+
(0, react_1.useEffect)(() => {
|
|
52
|
+
// Cast to unknown to satisfy the generic constraint
|
|
53
|
+
setListRef(flashListRef.current);
|
|
54
|
+
return () => setListRef(null);
|
|
55
|
+
}, [setListRef, flashListRef]);
|
|
56
|
+
// Update scroll offset on scroll
|
|
57
|
+
const handleScroll = (0, react_1.useCallback)((event) => {
|
|
58
|
+
scrollOffset.value = event.nativeEvent.contentOffset.y;
|
|
59
|
+
}, [scrollOffset]);
|
|
60
|
+
// Update content height when list content changes
|
|
61
|
+
const handleContentSizeChange = (0, react_1.useCallback)((_width, height) => {
|
|
62
|
+
contentHeight.value = height;
|
|
63
|
+
}, [contentHeight]);
|
|
64
|
+
// Update visible height and list position when layout changes
|
|
65
|
+
const handleLayout = (0, react_1.useCallback)((event) => {
|
|
66
|
+
visibleHeight.value = event.nativeEvent.layout.height;
|
|
67
|
+
const nativeRef = flashListRef.current?.getNativeScrollRef?.();
|
|
68
|
+
if (nativeRef?.measureInWindow) {
|
|
69
|
+
nativeRef.measureInWindow((_x, y) => {
|
|
70
|
+
listTopY.value = y;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}, [visibleHeight, listTopY, flashListRef]);
|
|
74
|
+
// Render item for FlashList
|
|
75
|
+
const flashListRenderItem = (0, react_1.useCallback)(({ item, index }) => (<ItemWrapper item={item} index={index} totalItemsRef={totalItemsRef} renderItem={renderItem} canDrag={canDrag} dragEnabled={dragEnabled} onReorderByDelta={onReorderByDelta} onHapticFeedback={onHapticFeedback}/>), [
|
|
76
|
+
totalItemsRef,
|
|
77
|
+
renderItem,
|
|
78
|
+
canDrag,
|
|
79
|
+
dragEnabled,
|
|
80
|
+
onReorderByDelta,
|
|
81
|
+
onHapticFeedback,
|
|
82
|
+
]);
|
|
83
|
+
// getItemType for FlashList recycling optimization
|
|
84
|
+
const getItemType = (0, react_1.useCallback)(() => 'animated-item', []);
|
|
85
|
+
// Override item layout for consistent drag calculations
|
|
86
|
+
// Note: We cast the layout to include size for drag calculations
|
|
87
|
+
const overrideItemLayout = (0, react_1.useCallback)((layout, _item, _index) => {
|
|
88
|
+
// FlashList v2 uses this for span, but we extend for size in drag calculations
|
|
89
|
+
layout.size = itemHeight;
|
|
90
|
+
}, [itemHeight]);
|
|
91
|
+
return (<flash_list_1.FlashList ref={flashListRef} data={data} renderItem={flashListRenderItem} keyExtractor={keyExtractor} getItemType={getItemType} overrideItemLayout={overrideItemLayout} onScroll={handleScroll} onContentSizeChange={handleContentSizeChange} onLayout={handleLayout} scrollEventThrottle={16} drawDistance={drawDistance} maintainVisibleContentPosition={{ disabled: true }} showsVerticalScrollIndicator={showsVerticalScrollIndicator} contentContainerStyle={contentContainerStyle} ListFooterComponent={ListFooterComponent ?? undefined} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} refreshControl={onRefresh ? (<react_native_1.RefreshControl refreshing={refreshing ?? false} onRefresh={onRefresh} tintColor={refreshTintColor} colors={refreshTintColor ? [refreshTintColor] : undefined}/>) : undefined}/>);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* AnimatedFlashList - High-performance animated list with drag-to-reorder
|
|
95
|
+
*
|
|
96
|
+
* A wrapper around @shopify/flash-list that provides:
|
|
97
|
+
* - Smooth drag-to-reorder with autoscroll
|
|
98
|
+
* - Entry animations for new items
|
|
99
|
+
* - Exit animations for removed items
|
|
100
|
+
* - Full TypeScript generics support
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* <AnimatedFlashList<MyItem>
|
|
105
|
+
* data={items}
|
|
106
|
+
* keyExtractor={(item) => item.id}
|
|
107
|
+
* renderItem={({ item, animatedStyle, dragHandleProps }) => (
|
|
108
|
+
* <Animated.View style={animatedStyle}>
|
|
109
|
+
* <MyItem item={item} />
|
|
110
|
+
* {dragHandleProps && (
|
|
111
|
+
* <GestureDetector gesture={dragHandleProps.gesture}>
|
|
112
|
+
* <DragHandle />
|
|
113
|
+
* </GestureDetector>
|
|
114
|
+
* )}
|
|
115
|
+
* </Animated.View>
|
|
116
|
+
* )}
|
|
117
|
+
* dragEnabled
|
|
118
|
+
* onReorder={(itemId, from, to) => reorderItems(itemId, from, to)}
|
|
119
|
+
* />
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
function AnimatedFlashListInner(props, ref) {
|
|
123
|
+
const { data, keyExtractor, renderItem, dragEnabled = false, onReorder, onReorderByNeighbors, canDrag, onHapticFeedback, config, onPrepareLayoutAnimation, ListFooterComponent, onRefresh, refreshing = false, onEndReached, onEndReachedThreshold = 0.5, contentContainerStyle, ...flashListProps } = props;
|
|
124
|
+
// Merge config with defaults
|
|
125
|
+
const dragConfig = (0, react_1.useMemo)(() => ({
|
|
126
|
+
...constants_1.DEFAULT_DRAG_CONFIG,
|
|
127
|
+
...config?.drag,
|
|
128
|
+
}), [config?.drag]);
|
|
129
|
+
// Ref to FlashList
|
|
130
|
+
const flashListRef = (0, react_1.useRef)(null);
|
|
131
|
+
// Expose methods to parent via ref
|
|
132
|
+
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
133
|
+
prepareForLayoutAnimation: () => {
|
|
134
|
+
flashListRef.current?.prepareForLayoutAnimationRender();
|
|
135
|
+
onPrepareLayoutAnimation?.();
|
|
136
|
+
},
|
|
137
|
+
scrollToOffset: (offset, animated = true) => {
|
|
138
|
+
flashListRef.current?.scrollToOffset({ offset, animated });
|
|
139
|
+
},
|
|
140
|
+
scrollToIndex: (index, animated = true) => {
|
|
141
|
+
flashListRef.current?.scrollToIndex({ index, animated });
|
|
142
|
+
},
|
|
143
|
+
}));
|
|
144
|
+
// Keep valid items in ref for reorder callback
|
|
145
|
+
const dataRef = (0, react_1.useRef)([]);
|
|
146
|
+
dataRef.current = data;
|
|
147
|
+
// Handle reorder by index delta - converts to various callback formats
|
|
148
|
+
const handleReorderByDelta = (0, react_1.useCallback)((itemId, indexDelta) => {
|
|
149
|
+
if (indexDelta === 0)
|
|
150
|
+
return;
|
|
151
|
+
const currentItems = dataRef.current;
|
|
152
|
+
const currentIndex = currentItems.findIndex(item => item.id === itemId);
|
|
153
|
+
if (currentIndex === -1)
|
|
154
|
+
return;
|
|
155
|
+
const newIndex = Math.max(0, Math.min(currentItems.length - 1, currentIndex + indexDelta));
|
|
156
|
+
if (newIndex === currentIndex)
|
|
157
|
+
return;
|
|
158
|
+
// Call onReorder if provided
|
|
159
|
+
if (onReorder) {
|
|
160
|
+
onReorder(itemId, currentIndex, newIndex);
|
|
161
|
+
}
|
|
162
|
+
// Call onReorderByNeighbors if provided (for fractional indexing)
|
|
163
|
+
if (onReorderByNeighbors) {
|
|
164
|
+
let afterItemId = null;
|
|
165
|
+
let beforeItemId = null;
|
|
166
|
+
if (indexDelta > 0) {
|
|
167
|
+
afterItemId = currentItems[newIndex]?.id ?? null;
|
|
168
|
+
beforeItemId =
|
|
169
|
+
newIndex < currentItems.length - 1
|
|
170
|
+
? currentItems[newIndex + 1]?.id ?? null
|
|
171
|
+
: null;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
afterItemId =
|
|
175
|
+
newIndex > 0 ? currentItems[newIndex - 1]?.id ?? null : null;
|
|
176
|
+
beforeItemId = currentItems[newIndex]?.id ?? null;
|
|
177
|
+
}
|
|
178
|
+
onReorderByNeighbors(itemId, afterItemId, beforeItemId);
|
|
179
|
+
}
|
|
180
|
+
}, [onReorder, onReorderByNeighbors]);
|
|
181
|
+
// Use ref for totalItems to avoid renderItem callback recreation
|
|
182
|
+
const totalItemsRef = (0, react_1.useRef)(data.length);
|
|
183
|
+
totalItemsRef.current = data.length;
|
|
184
|
+
// Early return for empty data
|
|
185
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
186
|
+
if (ListFooterComponent) {
|
|
187
|
+
return (<react_native_1.View style={containerStyle}>
|
|
188
|
+
{react_1.default.isValidElement(ListFooterComponent)
|
|
189
|
+
? ListFooterComponent
|
|
190
|
+
: react_1.default.createElement(ListFooterComponent)}
|
|
191
|
+
</react_native_1.View>);
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return (<contexts_1.ListAnimationProvider>
|
|
196
|
+
<contexts_1.DragStateProvider config={dragConfig}>
|
|
197
|
+
<react_native_1.View style={containerStyle}>
|
|
198
|
+
<InnerFlashList data={data} totalItemsRef={totalItemsRef} flashListRef={flashListRef} renderItem={renderItem} keyExtractor={keyExtractor} canDrag={canDrag} dragEnabled={dragEnabled} onReorderByDelta={onReorder || onReorderByNeighbors ? handleReorderByDelta : undefined} onHapticFeedback={onHapticFeedback} itemHeight={dragConfig.itemHeight} ListFooterComponent={ListFooterComponent} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} onRefresh={onRefresh} refreshing={refreshing} contentContainerStyle={contentContainerStyle} {...flashListProps}/>
|
|
199
|
+
</react_native_1.View>
|
|
200
|
+
</contexts_1.DragStateProvider>
|
|
201
|
+
</contexts_1.ListAnimationProvider>);
|
|
202
|
+
}
|
|
203
|
+
const containerStyle = {
|
|
204
|
+
flex: 1,
|
|
205
|
+
};
|
|
206
|
+
// Forward ref with generic support
|
|
207
|
+
exports.AnimatedFlashList = (0, react_1.forwardRef)(AnimatedFlashListInner);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AnimatedListItem, AnimatedRenderItemInfo, HapticFeedbackType } from './types';
|
|
3
|
+
interface AnimatedFlashListItemProps<T extends AnimatedListItem> {
|
|
4
|
+
/** The item data */
|
|
5
|
+
item: T;
|
|
6
|
+
/** Current index in list */
|
|
7
|
+
index: number;
|
|
8
|
+
/** Total number of items */
|
|
9
|
+
totalItems: number;
|
|
10
|
+
/** Whether drag is enabled for this item */
|
|
11
|
+
isDragEnabled: boolean;
|
|
12
|
+
/** Render function from parent */
|
|
13
|
+
renderItem: (info: AnimatedRenderItemInfo<T>) => React.ReactElement;
|
|
14
|
+
/** Callback when reorder occurs */
|
|
15
|
+
onReorderByDelta?: (itemId: string, delta: number) => void;
|
|
16
|
+
/** Optional haptic feedback callback */
|
|
17
|
+
onHapticFeedback?: (type: HapticFeedbackType) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Internal item wrapper that provides all animation functionality.
|
|
21
|
+
*
|
|
22
|
+
* This component:
|
|
23
|
+
* 1. Sets up drag gesture and shift animations
|
|
24
|
+
* 2. Sets up entry/exit animations
|
|
25
|
+
* 3. Combines all animated styles
|
|
26
|
+
* 4. Passes everything to the consumer's renderItem function
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
declare function AnimatedFlashListItemInner<T extends AnimatedListItem>({ item, index, totalItems, isDragEnabled, renderItem, onReorderByDelta, onHapticFeedback, }: AnimatedFlashListItemProps<T>): React.ReactElement | null;
|
|
31
|
+
export declare const AnimatedFlashListItem: typeof AnimatedFlashListItemInner;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=AnimatedFlashListItem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwD,MAAM,OAAO,CAAC;AAY7E,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,UAAU,0BAA0B,CAAC,CAAC,SAAS,gBAAgB;IAC7D,oBAAoB;IACpB,IAAI,EAAE,CAAC,CAAC;IACR,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IACvB,kCAAkC;IAClC,UAAU,EAAE,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,YAAY,CAAC;IACpE,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACvD;AAED;;;;;;;;;;GAUG;AACH,iBAAS,0BAA0B,CAAC,CAAC,SAAS,gBAAgB,EAAE,EAC9D,IAAI,EACJ,KAAK,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAwJ3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
|