@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,136 @@
|
|
|
1
|
+
import type { ReactElement, ComponentType, JSXElementConstructor } from 'react';
|
|
2
|
+
import type { ViewStyle } from 'react-native';
|
|
3
|
+
import type { FlashListProps } from '@shopify/flash-list';
|
|
4
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
5
|
+
import type { GestureType } from 'react-native-gesture-handler';
|
|
6
|
+
import type { DragConfig } from './drag';
|
|
7
|
+
import type { AnimationDirection, ExitAnimationPreset, ExitAnimationConfig, EntryAnimationConfig, HapticFeedbackType } from './animations';
|
|
8
|
+
/**
|
|
9
|
+
* Base item interface - consumers extend this
|
|
10
|
+
*/
|
|
11
|
+
export interface AnimatedListItem {
|
|
12
|
+
id: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Props passed to drag handle wrapper
|
|
16
|
+
*/
|
|
17
|
+
export interface DragHandleProps {
|
|
18
|
+
/** The pan gesture to attach */
|
|
19
|
+
gesture: GestureType;
|
|
20
|
+
/** Whether dragging is currently active */
|
|
21
|
+
isDragging: SharedValue<boolean>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Render item info passed to renderItem callback
|
|
25
|
+
*/
|
|
26
|
+
export interface AnimatedRenderItemInfo<T extends AnimatedListItem> {
|
|
27
|
+
/** The item data */
|
|
28
|
+
item: T;
|
|
29
|
+
/** Current index in list */
|
|
30
|
+
index: number;
|
|
31
|
+
/** Total number of items */
|
|
32
|
+
totalItems: number;
|
|
33
|
+
/** Combined animated style (drag + entry/exit) - apply to outer Animated.View */
|
|
34
|
+
animatedStyle: ViewStyle;
|
|
35
|
+
/** Drag handle props - spread onto GestureDetector wrapping your drag handle */
|
|
36
|
+
dragHandleProps: DragHandleProps | null;
|
|
37
|
+
/** Whether this item is currently being dragged */
|
|
38
|
+
isDragging: boolean;
|
|
39
|
+
/** Whether drag is enabled for this item */
|
|
40
|
+
isDragEnabled: boolean;
|
|
41
|
+
/** Trigger exit animation programmatically */
|
|
42
|
+
triggerExitAnimation: (direction: AnimationDirection, onComplete: () => void, preset?: ExitAnimationPreset) => void;
|
|
43
|
+
/** Reset exit animation state */
|
|
44
|
+
resetExitAnimation: () => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Configuration for the AnimatedFlashList
|
|
48
|
+
*/
|
|
49
|
+
export interface AnimatedFlashListConfig {
|
|
50
|
+
/** Drag behavior configuration */
|
|
51
|
+
drag?: Partial<DragConfig>;
|
|
52
|
+
/** Exit animation configuration */
|
|
53
|
+
exitAnimation?: Partial<ExitAnimationConfig>;
|
|
54
|
+
/** Entry animation configuration */
|
|
55
|
+
entryAnimation?: Partial<EntryAnimationConfig>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Props for AnimatedFlashList component
|
|
59
|
+
*/
|
|
60
|
+
export interface AnimatedFlashListProps<T extends AnimatedListItem> extends Omit<FlashListProps<T>, 'data' | 'renderItem' | 'keyExtractor' | 'ref'> {
|
|
61
|
+
/** List data */
|
|
62
|
+
data: T[];
|
|
63
|
+
/** Key extractor function */
|
|
64
|
+
keyExtractor: (item: T, index: number) => string;
|
|
65
|
+
/**
|
|
66
|
+
* Render function for each item
|
|
67
|
+
* Receives item data and animation utilities
|
|
68
|
+
*/
|
|
69
|
+
renderItem: (info: AnimatedRenderItemInfo<T>) => ReactElement;
|
|
70
|
+
/**
|
|
71
|
+
* Whether drag-to-reorder is enabled
|
|
72
|
+
* @default false
|
|
73
|
+
*/
|
|
74
|
+
dragEnabled?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Callback when an item is reordered
|
|
77
|
+
* @param itemId - ID of the moved item
|
|
78
|
+
* @param fromIndex - Original index
|
|
79
|
+
* @param toIndex - New index
|
|
80
|
+
*/
|
|
81
|
+
onReorder?: (itemId: string, fromIndex: number, toIndex: number) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Callback with neighbor-based reorder info (for fractional indexing)
|
|
84
|
+
* @param itemId - ID of the moved item
|
|
85
|
+
* @param afterItemId - ID of item that should come before (null if moving to start)
|
|
86
|
+
* @param beforeItemId - ID of item that should come after (null if moving to end)
|
|
87
|
+
*/
|
|
88
|
+
onReorderByNeighbors?: (itemId: string, afterItemId: string | null, beforeItemId: string | null) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Function to determine if an item can be dragged
|
|
91
|
+
* @default () => true when dragEnabled is true
|
|
92
|
+
*/
|
|
93
|
+
canDrag?: (item: T, index: number) => boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Optional haptic feedback callback
|
|
96
|
+
* Called during drag operations
|
|
97
|
+
*/
|
|
98
|
+
onHapticFeedback?: (type: HapticFeedbackType) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Configuration overrides
|
|
101
|
+
*/
|
|
102
|
+
config?: AnimatedFlashListConfig;
|
|
103
|
+
/**
|
|
104
|
+
* Called when FlashList needs to prepare for layout animation
|
|
105
|
+
* (before items are added/removed)
|
|
106
|
+
*/
|
|
107
|
+
onPrepareLayoutAnimation?: () => void;
|
|
108
|
+
/** Footer component */
|
|
109
|
+
ListFooterComponent?: ReactElement<unknown, string | JSXElementConstructor<unknown>> | ComponentType<unknown> | null;
|
|
110
|
+
/** Refresh callback */
|
|
111
|
+
onRefresh?: () => void | Promise<void>;
|
|
112
|
+
/** Whether currently refreshing */
|
|
113
|
+
refreshing?: boolean;
|
|
114
|
+
/** Pagination callback */
|
|
115
|
+
onEndReached?: () => void;
|
|
116
|
+
/** Pagination threshold */
|
|
117
|
+
onEndReachedThreshold?: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Ref handle for AnimatedFlashList
|
|
121
|
+
*/
|
|
122
|
+
export interface AnimatedFlashListRef {
|
|
123
|
+
/**
|
|
124
|
+
* Prepare FlashList for layout animation before items are removed/added
|
|
125
|
+
*/
|
|
126
|
+
prepareForLayoutAnimation: () => void;
|
|
127
|
+
/**
|
|
128
|
+
* Scroll to a specific offset
|
|
129
|
+
*/
|
|
130
|
+
scrollToOffset: (offset: number, animated?: boolean) => void;
|
|
131
|
+
/**
|
|
132
|
+
* Scroll to a specific index
|
|
133
|
+
*/
|
|
134
|
+
scrollToIndex: (index: number, animated?: boolean) => void;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/types/list.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@souscheflabs/reanimated-flashlist",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "A high-performance animated FlashList with drag-to-reorder and entry/exit animations (New Architecture)",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"module": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"source": "src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"lib",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"build": "tsc -p tsconfig.build.json",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:watch": "jest --watch",
|
|
19
|
+
"test:coverage": "jest --coverage",
|
|
20
|
+
"tag": "./scripts/tag.sh",
|
|
21
|
+
"release:patch": "./scripts/release.sh patch",
|
|
22
|
+
"release:minor": "./scripts/release.sh minor",
|
|
23
|
+
"release:major": "./scripts/release.sh major",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@shopify/flash-list": "^2.0.0",
|
|
28
|
+
"react": ">=18.0.0",
|
|
29
|
+
"react-native": ">=0.74.0",
|
|
30
|
+
"react-native-gesture-handler": ">=2.14.0",
|
|
31
|
+
"react-native-reanimated": "^4.0.0",
|
|
32
|
+
"react-native-worklets": ">=0.7.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@babel/core": "^7.24.0",
|
|
36
|
+
"@babel/preset-env": "^7.24.0",
|
|
37
|
+
"@babel/preset-react": "^7.24.0",
|
|
38
|
+
"@babel/preset-typescript": "^7.24.0",
|
|
39
|
+
"@shopify/flash-list": "^2.0.0",
|
|
40
|
+
"@testing-library/react-native": "^12.4.0",
|
|
41
|
+
"@types/jest": "^29.5.0",
|
|
42
|
+
"@types/react": "^18.2.0",
|
|
43
|
+
"@types/react-native": "^0.72.0",
|
|
44
|
+
"babel-jest": "^29.7.0",
|
|
45
|
+
"jest": "^29.7.0",
|
|
46
|
+
"react": "^18.2.0",
|
|
47
|
+
"react-native": "^0.76.0",
|
|
48
|
+
"react-native-gesture-handler": "^2.14.0",
|
|
49
|
+
"react-native-reanimated": "^4.0.0",
|
|
50
|
+
"react-test-renderer": "^18.2.0",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"react-native",
|
|
55
|
+
"flashlist",
|
|
56
|
+
"animated",
|
|
57
|
+
"drag-to-reorder",
|
|
58
|
+
"reanimated",
|
|
59
|
+
"layout-animation",
|
|
60
|
+
"new-architecture",
|
|
61
|
+
"turbomodules"
|
|
62
|
+
],
|
|
63
|
+
"author": "SousChef Labs",
|
|
64
|
+
"license": "MIT",
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "https://github.com/SousChefLabs/reanimated-flashlist.git"
|
|
68
|
+
},
|
|
69
|
+
"bugs": {
|
|
70
|
+
"url": "https://github.com/SousChefLabs/reanimated-flashlist/issues"
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://github.com/SousChefLabs/reanimated-flashlist#readme"
|
|
73
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useRef,
|
|
4
|
+
useMemo,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
useEffect,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
RefreshControl,
|
|
12
|
+
type NativeSyntheticEvent,
|
|
13
|
+
type NativeScrollEvent,
|
|
14
|
+
type LayoutChangeEvent,
|
|
15
|
+
type ViewStyle,
|
|
16
|
+
} from 'react-native';
|
|
17
|
+
import { FlashList, type ListRenderItemInfo, type FlashListRef } from '@shopify/flash-list';
|
|
18
|
+
import { DragStateProvider, ListAnimationProvider, useDragState } from './contexts';
|
|
19
|
+
import { AnimatedFlashListItem } from './AnimatedFlashListItem';
|
|
20
|
+
import { DEFAULT_DRAG_CONFIG } from './constants';
|
|
21
|
+
import type {
|
|
22
|
+
AnimatedListItem,
|
|
23
|
+
AnimatedFlashListProps,
|
|
24
|
+
AnimatedFlashListRef,
|
|
25
|
+
HapticFeedbackType,
|
|
26
|
+
} from './types';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Item wrapper component for FlashList
|
|
30
|
+
* Uses ref for totalItems to avoid renderItem callback recreation
|
|
31
|
+
*/
|
|
32
|
+
interface ItemWrapperProps<T extends AnimatedListItem> {
|
|
33
|
+
item: T;
|
|
34
|
+
index: number;
|
|
35
|
+
totalItemsRef: React.RefObject<number>;
|
|
36
|
+
renderItem: AnimatedFlashListProps<T>['renderItem'];
|
|
37
|
+
canDrag?: (item: T, index: number) => boolean;
|
|
38
|
+
dragEnabled: boolean;
|
|
39
|
+
onReorderByDelta?: (itemId: string, delta: number) => void;
|
|
40
|
+
onHapticFeedback?: (type: HapticFeedbackType) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ItemWrapper = React.memo(function ItemWrapper<T extends AnimatedListItem>({
|
|
44
|
+
item,
|
|
45
|
+
index,
|
|
46
|
+
totalItemsRef,
|
|
47
|
+
renderItem,
|
|
48
|
+
canDrag,
|
|
49
|
+
dragEnabled,
|
|
50
|
+
onReorderByDelta,
|
|
51
|
+
onHapticFeedback,
|
|
52
|
+
}: ItemWrapperProps<T>) {
|
|
53
|
+
const isDragEnabled = dragEnabled && (canDrag ? canDrag(item, index) : true);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<AnimatedFlashListItem
|
|
57
|
+
item={item}
|
|
58
|
+
index={index}
|
|
59
|
+
totalItems={totalItemsRef.current ?? 0}
|
|
60
|
+
isDragEnabled={isDragEnabled}
|
|
61
|
+
renderItem={renderItem}
|
|
62
|
+
onReorderByDelta={onReorderByDelta}
|
|
63
|
+
onHapticFeedback={onHapticFeedback}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}) as <T extends AnimatedListItem>(props: ItemWrapperProps<T>) => React.ReactElement;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Inner FlashList component that uses DragStateContext for scroll tracking
|
|
70
|
+
*/
|
|
71
|
+
interface InnerFlashListProps<T extends AnimatedListItem> {
|
|
72
|
+
data: T[];
|
|
73
|
+
totalItemsRef: React.RefObject<number>;
|
|
74
|
+
flashListRef: React.RefObject<FlashListRef<T> | null>;
|
|
75
|
+
renderItem: AnimatedFlashListProps<T>['renderItem'];
|
|
76
|
+
keyExtractor: (item: T, index: number) => string;
|
|
77
|
+
canDrag?: (item: T, index: number) => boolean;
|
|
78
|
+
dragEnabled: boolean;
|
|
79
|
+
onReorderByDelta?: (itemId: string, delta: number) => void;
|
|
80
|
+
onHapticFeedback?: (type: HapticFeedbackType) => void;
|
|
81
|
+
itemHeight: number;
|
|
82
|
+
ListFooterComponent?: AnimatedFlashListProps<T>['ListFooterComponent'];
|
|
83
|
+
onEndReached?: () => void;
|
|
84
|
+
onEndReachedThreshold?: number;
|
|
85
|
+
onRefresh?: () => void | Promise<void>;
|
|
86
|
+
refreshing?: boolean;
|
|
87
|
+
refreshTintColor?: string;
|
|
88
|
+
contentContainerStyle?: AnimatedFlashListProps<T>['contentContainerStyle'];
|
|
89
|
+
drawDistance?: number;
|
|
90
|
+
showsVerticalScrollIndicator?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function InnerFlashList<T extends AnimatedListItem>({
|
|
94
|
+
data,
|
|
95
|
+
totalItemsRef,
|
|
96
|
+
flashListRef,
|
|
97
|
+
renderItem,
|
|
98
|
+
keyExtractor,
|
|
99
|
+
canDrag,
|
|
100
|
+
dragEnabled,
|
|
101
|
+
onReorderByDelta,
|
|
102
|
+
onHapticFeedback,
|
|
103
|
+
itemHeight,
|
|
104
|
+
ListFooterComponent,
|
|
105
|
+
onEndReached,
|
|
106
|
+
onEndReachedThreshold,
|
|
107
|
+
onRefresh,
|
|
108
|
+
refreshing,
|
|
109
|
+
refreshTintColor,
|
|
110
|
+
contentContainerStyle,
|
|
111
|
+
drawDistance = 500,
|
|
112
|
+
showsVerticalScrollIndicator = true,
|
|
113
|
+
}: InnerFlashListProps<T>): React.ReactElement {
|
|
114
|
+
// Get drag state context for scroll tracking
|
|
115
|
+
const { scrollOffset, contentHeight, visibleHeight, listTopY, setListRef } =
|
|
116
|
+
useDragState();
|
|
117
|
+
|
|
118
|
+
// Register FlashList ref with drag context for autoscroll
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
// Cast to unknown to satisfy the generic constraint
|
|
121
|
+
setListRef(flashListRef.current as FlashListRef<unknown> | null);
|
|
122
|
+
return () => setListRef(null);
|
|
123
|
+
}, [setListRef, flashListRef]);
|
|
124
|
+
|
|
125
|
+
// Update scroll offset on scroll
|
|
126
|
+
const handleScroll = useCallback(
|
|
127
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
128
|
+
scrollOffset.value = event.nativeEvent.contentOffset.y;
|
|
129
|
+
},
|
|
130
|
+
[scrollOffset],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Update content height when list content changes
|
|
134
|
+
const handleContentSizeChange = useCallback(
|
|
135
|
+
(_width: number, height: number) => {
|
|
136
|
+
contentHeight.value = height;
|
|
137
|
+
},
|
|
138
|
+
[contentHeight],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Update visible height and list position when layout changes
|
|
142
|
+
const handleLayout = useCallback(
|
|
143
|
+
(event: LayoutChangeEvent) => {
|
|
144
|
+
visibleHeight.value = event.nativeEvent.layout.height;
|
|
145
|
+
const nativeRef = flashListRef.current?.getNativeScrollRef?.() as
|
|
146
|
+
| { measureInWindow?: (cb: (x: number, y: number) => void) => void }
|
|
147
|
+
| undefined;
|
|
148
|
+
if (nativeRef?.measureInWindow) {
|
|
149
|
+
nativeRef.measureInWindow((_x, y) => {
|
|
150
|
+
listTopY.value = y;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
[visibleHeight, listTopY, flashListRef],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Render item for FlashList
|
|
158
|
+
const flashListRenderItem = useCallback(
|
|
159
|
+
({ item, index }: ListRenderItemInfo<T>) => (
|
|
160
|
+
<ItemWrapper
|
|
161
|
+
item={item}
|
|
162
|
+
index={index}
|
|
163
|
+
totalItemsRef={totalItemsRef}
|
|
164
|
+
renderItem={renderItem}
|
|
165
|
+
canDrag={canDrag}
|
|
166
|
+
dragEnabled={dragEnabled}
|
|
167
|
+
onReorderByDelta={onReorderByDelta}
|
|
168
|
+
onHapticFeedback={onHapticFeedback}
|
|
169
|
+
/>
|
|
170
|
+
),
|
|
171
|
+
[
|
|
172
|
+
totalItemsRef,
|
|
173
|
+
renderItem,
|
|
174
|
+
canDrag,
|
|
175
|
+
dragEnabled,
|
|
176
|
+
onReorderByDelta,
|
|
177
|
+
onHapticFeedback,
|
|
178
|
+
],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// getItemType for FlashList recycling optimization
|
|
182
|
+
const getItemType = useCallback(() => 'animated-item', []);
|
|
183
|
+
|
|
184
|
+
// Override item layout for consistent drag calculations
|
|
185
|
+
// Note: We cast the layout to include size for drag calculations
|
|
186
|
+
const overrideItemLayout = useCallback(
|
|
187
|
+
(layout: { span?: number }, _item: T, _index: number) => {
|
|
188
|
+
// FlashList v2 uses this for span, but we extend for size in drag calculations
|
|
189
|
+
(layout as { size?: number }).size = itemHeight;
|
|
190
|
+
},
|
|
191
|
+
[itemHeight],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<FlashList
|
|
196
|
+
ref={flashListRef as React.RefObject<FlashListRef<T>>}
|
|
197
|
+
data={data}
|
|
198
|
+
renderItem={flashListRenderItem}
|
|
199
|
+
keyExtractor={keyExtractor}
|
|
200
|
+
getItemType={getItemType}
|
|
201
|
+
overrideItemLayout={overrideItemLayout}
|
|
202
|
+
onScroll={handleScroll}
|
|
203
|
+
onContentSizeChange={handleContentSizeChange}
|
|
204
|
+
onLayout={handleLayout}
|
|
205
|
+
scrollEventThrottle={16}
|
|
206
|
+
drawDistance={drawDistance}
|
|
207
|
+
maintainVisibleContentPosition={{ disabled: true }}
|
|
208
|
+
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
|
|
209
|
+
contentContainerStyle={contentContainerStyle}
|
|
210
|
+
ListFooterComponent={ListFooterComponent ?? undefined}
|
|
211
|
+
onEndReached={onEndReached}
|
|
212
|
+
onEndReachedThreshold={onEndReachedThreshold}
|
|
213
|
+
refreshControl={
|
|
214
|
+
onRefresh ? (
|
|
215
|
+
<RefreshControl
|
|
216
|
+
refreshing={refreshing ?? false}
|
|
217
|
+
onRefresh={onRefresh}
|
|
218
|
+
tintColor={refreshTintColor}
|
|
219
|
+
colors={refreshTintColor ? [refreshTintColor] : undefined}
|
|
220
|
+
/>
|
|
221
|
+
) : undefined
|
|
222
|
+
}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* AnimatedFlashList - High-performance animated list with drag-to-reorder
|
|
229
|
+
*
|
|
230
|
+
* A wrapper around @shopify/flash-list that provides:
|
|
231
|
+
* - Smooth drag-to-reorder with autoscroll
|
|
232
|
+
* - Entry animations for new items
|
|
233
|
+
* - Exit animations for removed items
|
|
234
|
+
* - Full TypeScript generics support
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```tsx
|
|
238
|
+
* <AnimatedFlashList<MyItem>
|
|
239
|
+
* data={items}
|
|
240
|
+
* keyExtractor={(item) => item.id}
|
|
241
|
+
* renderItem={({ item, animatedStyle, dragHandleProps }) => (
|
|
242
|
+
* <Animated.View style={animatedStyle}>
|
|
243
|
+
* <MyItem item={item} />
|
|
244
|
+
* {dragHandleProps && (
|
|
245
|
+
* <GestureDetector gesture={dragHandleProps.gesture}>
|
|
246
|
+
* <DragHandle />
|
|
247
|
+
* </GestureDetector>
|
|
248
|
+
* )}
|
|
249
|
+
* </Animated.View>
|
|
250
|
+
* )}
|
|
251
|
+
* dragEnabled
|
|
252
|
+
* onReorder={(itemId, from, to) => reorderItems(itemId, from, to)}
|
|
253
|
+
* />
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
function AnimatedFlashListInner<T extends AnimatedListItem>(
|
|
257
|
+
props: AnimatedFlashListProps<T>,
|
|
258
|
+
ref: React.ForwardedRef<AnimatedFlashListRef>,
|
|
259
|
+
): React.ReactElement | null {
|
|
260
|
+
const {
|
|
261
|
+
data,
|
|
262
|
+
keyExtractor,
|
|
263
|
+
renderItem,
|
|
264
|
+
dragEnabled = false,
|
|
265
|
+
onReorder,
|
|
266
|
+
onReorderByNeighbors,
|
|
267
|
+
canDrag,
|
|
268
|
+
onHapticFeedback,
|
|
269
|
+
config,
|
|
270
|
+
onPrepareLayoutAnimation,
|
|
271
|
+
ListFooterComponent,
|
|
272
|
+
onRefresh,
|
|
273
|
+
refreshing = false,
|
|
274
|
+
onEndReached,
|
|
275
|
+
onEndReachedThreshold = 0.5,
|
|
276
|
+
contentContainerStyle,
|
|
277
|
+
...flashListProps
|
|
278
|
+
} = props;
|
|
279
|
+
|
|
280
|
+
// Merge config with defaults
|
|
281
|
+
const dragConfig = useMemo(
|
|
282
|
+
() => ({
|
|
283
|
+
...DEFAULT_DRAG_CONFIG,
|
|
284
|
+
...config?.drag,
|
|
285
|
+
}),
|
|
286
|
+
[config?.drag],
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// Ref to FlashList
|
|
290
|
+
const flashListRef = useRef<FlashListRef<T>>(null);
|
|
291
|
+
|
|
292
|
+
// Expose methods to parent via ref
|
|
293
|
+
useImperativeHandle(ref, () => ({
|
|
294
|
+
prepareForLayoutAnimation: () => {
|
|
295
|
+
flashListRef.current?.prepareForLayoutAnimationRender();
|
|
296
|
+
onPrepareLayoutAnimation?.();
|
|
297
|
+
},
|
|
298
|
+
scrollToOffset: (offset: number, animated = true) => {
|
|
299
|
+
flashListRef.current?.scrollToOffset({ offset, animated });
|
|
300
|
+
},
|
|
301
|
+
scrollToIndex: (index: number, animated = true) => {
|
|
302
|
+
flashListRef.current?.scrollToIndex({ index, animated });
|
|
303
|
+
},
|
|
304
|
+
}));
|
|
305
|
+
|
|
306
|
+
// Keep valid items in ref for reorder callback
|
|
307
|
+
const dataRef = useRef<T[]>([]);
|
|
308
|
+
dataRef.current = data;
|
|
309
|
+
|
|
310
|
+
// Handle reorder by index delta - converts to various callback formats
|
|
311
|
+
const handleReorderByDelta = useCallback(
|
|
312
|
+
(itemId: string, indexDelta: number) => {
|
|
313
|
+
if (indexDelta === 0) return;
|
|
314
|
+
|
|
315
|
+
const currentItems = dataRef.current;
|
|
316
|
+
const currentIndex = currentItems.findIndex(item => item.id === itemId);
|
|
317
|
+
if (currentIndex === -1) return;
|
|
318
|
+
|
|
319
|
+
const newIndex = Math.max(
|
|
320
|
+
0,
|
|
321
|
+
Math.min(currentItems.length - 1, currentIndex + indexDelta),
|
|
322
|
+
);
|
|
323
|
+
if (newIndex === currentIndex) return;
|
|
324
|
+
|
|
325
|
+
// Call onReorder if provided
|
|
326
|
+
if (onReorder) {
|
|
327
|
+
onReorder(itemId, currentIndex, newIndex);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Call onReorderByNeighbors if provided (for fractional indexing)
|
|
331
|
+
if (onReorderByNeighbors) {
|
|
332
|
+
let afterItemId: string | null = null;
|
|
333
|
+
let beforeItemId: string | null = null;
|
|
334
|
+
|
|
335
|
+
if (indexDelta > 0) {
|
|
336
|
+
afterItemId = currentItems[newIndex]?.id ?? null;
|
|
337
|
+
beforeItemId =
|
|
338
|
+
newIndex < currentItems.length - 1
|
|
339
|
+
? currentItems[newIndex + 1]?.id ?? null
|
|
340
|
+
: null;
|
|
341
|
+
} else {
|
|
342
|
+
afterItemId =
|
|
343
|
+
newIndex > 0 ? currentItems[newIndex - 1]?.id ?? null : null;
|
|
344
|
+
beforeItemId = currentItems[newIndex]?.id ?? null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
onReorderByNeighbors(itemId, afterItemId, beforeItemId);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
[onReorder, onReorderByNeighbors],
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Use ref for totalItems to avoid renderItem callback recreation
|
|
354
|
+
const totalItemsRef = useRef(data.length);
|
|
355
|
+
totalItemsRef.current = data.length;
|
|
356
|
+
|
|
357
|
+
// Early return for empty data
|
|
358
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
359
|
+
if (ListFooterComponent) {
|
|
360
|
+
return (
|
|
361
|
+
<View style={containerStyle}>
|
|
362
|
+
{React.isValidElement(ListFooterComponent)
|
|
363
|
+
? ListFooterComponent
|
|
364
|
+
: React.createElement(ListFooterComponent as React.ComponentType)}
|
|
365
|
+
</View>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<ListAnimationProvider>
|
|
373
|
+
<DragStateProvider config={dragConfig}>
|
|
374
|
+
<View style={containerStyle}>
|
|
375
|
+
<InnerFlashList
|
|
376
|
+
data={data}
|
|
377
|
+
totalItemsRef={totalItemsRef}
|
|
378
|
+
flashListRef={flashListRef}
|
|
379
|
+
renderItem={renderItem}
|
|
380
|
+
keyExtractor={keyExtractor}
|
|
381
|
+
canDrag={canDrag}
|
|
382
|
+
dragEnabled={dragEnabled}
|
|
383
|
+
onReorderByDelta={
|
|
384
|
+
onReorder || onReorderByNeighbors ? handleReorderByDelta : undefined
|
|
385
|
+
}
|
|
386
|
+
onHapticFeedback={onHapticFeedback}
|
|
387
|
+
itemHeight={dragConfig.itemHeight}
|
|
388
|
+
ListFooterComponent={ListFooterComponent}
|
|
389
|
+
onEndReached={onEndReached}
|
|
390
|
+
onEndReachedThreshold={onEndReachedThreshold}
|
|
391
|
+
onRefresh={onRefresh}
|
|
392
|
+
refreshing={refreshing}
|
|
393
|
+
contentContainerStyle={contentContainerStyle}
|
|
394
|
+
{...flashListProps}
|
|
395
|
+
/>
|
|
396
|
+
</View>
|
|
397
|
+
</DragStateProvider>
|
|
398
|
+
</ListAnimationProvider>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const containerStyle: ViewStyle = {
|
|
403
|
+
flex: 1,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// Forward ref with generic support
|
|
407
|
+
export const AnimatedFlashList = forwardRef(AnimatedFlashListInner) as <
|
|
408
|
+
T extends AnimatedListItem,
|
|
409
|
+
>(
|
|
410
|
+
props: AnimatedFlashListProps<T> & { ref?: React.ForwardedRef<AnimatedFlashListRef> },
|
|
411
|
+
) => React.ReactElement | null;
|