react-native-reorderable-list 0.13.0 → 0.14.0

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 (71) hide show
  1. package/README.md +69 -9
  2. package/lib/commonjs/components/NestedReorderableList.js +2 -0
  3. package/lib/commonjs/components/NestedReorderableList.js.map +1 -1
  4. package/lib/commonjs/components/ReorderableList.js +4 -3
  5. package/lib/commonjs/components/ReorderableList.js.map +1 -1
  6. package/lib/commonjs/components/{ReorderableListCore/useReorderableListCore.js → ReorderableListCore.js} +187 -96
  7. package/lib/commonjs/components/ReorderableListCore.js.map +1 -0
  8. package/lib/commonjs/components/ScrollViewContainer.js +26 -7
  9. package/lib/commonjs/components/ScrollViewContainer.js.map +1 -1
  10. package/lib/commonjs/components/{ReorderableListCore/animationDefaults.js → constants.js} +26 -2
  11. package/lib/commonjs/components/constants.js.map +1 -0
  12. package/lib/commonjs/contexts/ScrollViewContainerContext.js.map +1 -1
  13. package/lib/module/components/NestedReorderableList.js +2 -0
  14. package/lib/module/components/NestedReorderableList.js.map +1 -1
  15. package/lib/module/components/ReorderableList.js +4 -3
  16. package/lib/module/components/ReorderableList.js.map +1 -1
  17. package/lib/module/components/{ReorderableListCore/useReorderableListCore.js → ReorderableListCore.js} +188 -95
  18. package/lib/module/components/ReorderableListCore.js.map +1 -0
  19. package/lib/module/components/ScrollViewContainer.js +28 -8
  20. package/lib/module/components/ScrollViewContainer.js.map +1 -1
  21. package/lib/module/components/constants.js +52 -0
  22. package/lib/module/components/constants.js.map +1 -0
  23. package/lib/module/contexts/ScrollViewContainerContext.js.map +1 -1
  24. package/lib/typescript/components/NestedReorderableList.d.ts.map +1 -1
  25. package/lib/typescript/components/ReorderableList.d.ts.map +1 -1
  26. package/lib/typescript/components/{ReorderableListCore/ReorderableListCore.d.ts → ReorderableListCore.d.ts} +2 -1
  27. package/lib/typescript/components/ReorderableListCore.d.ts.map +1 -0
  28. package/lib/typescript/components/ScrollViewContainer.d.ts +2 -1
  29. package/lib/typescript/components/ScrollViewContainer.d.ts.map +1 -1
  30. package/lib/typescript/components/{ReorderableListCore/animationDefaults.d.ts → constants.d.ts} +6 -1
  31. package/lib/typescript/components/constants.d.ts.map +1 -0
  32. package/lib/typescript/contexts/ScrollViewContainerContext.d.ts +1 -0
  33. package/lib/typescript/contexts/ScrollViewContainerContext.d.ts.map +1 -1
  34. package/lib/typescript/types/props.d.ts +9 -0
  35. package/lib/typescript/types/props.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/components/NestedReorderableList.tsx +2 -0
  38. package/src/components/ReorderableList.tsx +3 -2
  39. package/src/components/{ReorderableListCore/useReorderableListCore.ts → ReorderableListCore.tsx} +321 -184
  40. package/src/components/ScrollViewContainer.tsx +44 -13
  41. package/src/components/{ReorderableListCore/animationDefaults.ts → constants.ts} +34 -0
  42. package/src/contexts/ScrollViewContainerContext.ts +1 -0
  43. package/src/types/props.ts +9 -0
  44. package/lib/commonjs/components/ReorderableListCore/ReorderableListCore.js +0 -121
  45. package/lib/commonjs/components/ReorderableListCore/ReorderableListCore.js.map +0 -1
  46. package/lib/commonjs/components/ReorderableListCore/animationDefaults.js.map +0 -1
  47. package/lib/commonjs/components/ReorderableListCore/autoscrollConfig.js +0 -31
  48. package/lib/commonjs/components/ReorderableListCore/autoscrollConfig.js.map +0 -1
  49. package/lib/commonjs/components/ReorderableListCore/index.js +0 -17
  50. package/lib/commonjs/components/ReorderableListCore/index.js.map +0 -1
  51. package/lib/commonjs/components/ReorderableListCore/useReorderableListCore.js.map +0 -1
  52. package/lib/module/components/ReorderableListCore/ReorderableListCore.js +0 -114
  53. package/lib/module/components/ReorderableListCore/ReorderableListCore.js.map +0 -1
  54. package/lib/module/components/ReorderableListCore/animationDefaults.js +0 -28
  55. package/lib/module/components/ReorderableListCore/animationDefaults.js.map +0 -1
  56. package/lib/module/components/ReorderableListCore/autoscrollConfig.js +0 -25
  57. package/lib/module/components/ReorderableListCore/autoscrollConfig.js.map +0 -1
  58. package/lib/module/components/ReorderableListCore/index.js +0 -2
  59. package/lib/module/components/ReorderableListCore/index.js.map +0 -1
  60. package/lib/module/components/ReorderableListCore/useReorderableListCore.js.map +0 -1
  61. package/lib/typescript/components/ReorderableListCore/ReorderableListCore.d.ts.map +0 -1
  62. package/lib/typescript/components/ReorderableListCore/animationDefaults.d.ts.map +0 -1
  63. package/lib/typescript/components/ReorderableListCore/autoscrollConfig.d.ts +0 -6
  64. package/lib/typescript/components/ReorderableListCore/autoscrollConfig.d.ts.map +0 -1
  65. package/lib/typescript/components/ReorderableListCore/index.d.ts +0 -2
  66. package/lib/typescript/components/ReorderableListCore/index.d.ts.map +0 -1
  67. package/lib/typescript/components/ReorderableListCore/useReorderableListCore.d.ts +0 -183
  68. package/lib/typescript/components/ReorderableListCore/useReorderableListCore.d.ts.map +0 -1
  69. package/src/components/ReorderableListCore/ReorderableListCore.tsx +0 -175
  70. package/src/components/ReorderableListCore/autoscrollConfig.ts +0 -31
  71. package/src/components/ReorderableListCore/index.ts +0 -1
@@ -1,14 +1,18 @@
1
- import React, {useCallback, useEffect, useMemo, useState} from 'react';
1
+ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2
2
  import {
3
+ CellRendererProps,
3
4
  FlatList,
5
+ FlatListProps,
4
6
  LayoutChangeEvent,
7
+ Platform,
5
8
  ScrollView,
6
- unstable_batchedUpdates,
7
9
  } from 'react-native';
8
10
 
9
11
  import {
10
12
  Gesture,
13
+ GestureDetector,
11
14
  GestureUpdateEvent,
15
+ NativeGesture,
12
16
  PanGestureHandlerEventPayload,
13
17
  State,
14
18
  } from 'react-native-gesture-handler';
@@ -16,87 +20,88 @@ import Animated, {
16
20
  AnimatedRef,
17
21
  Easing,
18
22
  SharedValue,
23
+ measure,
19
24
  runOnJS,
20
25
  runOnUI,
21
26
  scrollTo,
22
27
  useAnimatedReaction,
23
28
  useAnimatedRef,
24
29
  useAnimatedScrollHandler,
30
+ useComposedEventHandler,
31
+ useDerivedValue,
25
32
  useSharedValue,
26
33
  withDelay,
27
34
  withTiming,
28
35
  } from 'react-native-reanimated';
29
36
 
37
+ import {ReorderableListContext} from '../contexts';
38
+ import {ReorderableListProps, ReorderableListState} from '../types';
30
39
  import {
40
+ AUTOSCROLL_CONFIG,
31
41
  OPACITY_ANIMATION_CONFIG_DEFAULT,
32
42
  SCALE_ANIMATION_CONFIG_DEFAULT,
33
- } from './animationDefaults';
34
- import {AUTOSCROLL_CONFIG} from './autoscrollConfig';
35
- import {
36
- ReorderableListCellAnimations,
37
- ReorderableListDragEndEvent,
38
- ReorderableListDragStartEvent,
39
- ReorderableListIndexChangeEvent,
40
- ReorderableListState,
41
- } from '../../types';
42
- import type {ReorderableListReorderEvent} from '../../types';
43
-
44
- const version = React.version.split('.');
45
- const hasAutomaticBatching = version.length
46
- ? parseInt(version[0], 10) >= 18
47
- : false;
48
-
49
- interface UseReorderableListCoreArgs<T> {
50
- ref: React.ForwardedRef<FlatList<T>>;
51
- autoscrollThreshold: number;
52
- autoscrollThresholdOffset: {top?: number; bottom?: number} | undefined;
53
- autoscrollSpeedScale: number;
54
- autoscrollDelay: number;
55
- autoscrollActivationDelta: number;
56
- animationDuration: number;
57
- onReorder: (event: ReorderableListReorderEvent) => void;
58
- onDragStart?: (event: ReorderableListDragStartEvent) => void;
59
- onDragEnd?: (event: ReorderableListDragEndEvent) => void;
60
- onIndexChange?: (event: ReorderableListIndexChangeEvent) => void;
61
- onLayout?: (event: LayoutChangeEvent) => void;
43
+ } from './constants';
44
+ import {ReorderableListCell} from './ReorderableListCell';
45
+
46
+ const AnimatedFlatList = Animated.createAnimatedComponent(
47
+ FlatList,
48
+ ) as unknown as <T>(
49
+ props: FlatListProps<T> & {ref?: React.Ref<FlatList<T>>},
50
+ ) => React.ReactElement;
51
+
52
+ interface ReorderableListCoreProps<T> extends ReorderableListProps<T> {
53
+ // Not optional but undefined to force passing the prop.
62
54
  scrollViewContainerRef: React.RefObject<ScrollView> | undefined;
55
+ scrollViewPageY: SharedValue<number> | undefined;
63
56
  scrollViewHeightY: SharedValue<number> | undefined;
64
57
  scrollViewScrollOffsetY: SharedValue<number> | undefined;
65
58
  scrollViewScrollEnabled: SharedValue<boolean> | undefined;
66
- initialScrollEnabled: boolean | undefined;
59
+ outerScrollGesture: NativeGesture | undefined;
67
60
  initialScrollViewScrollEnabled: boolean | undefined;
68
- nestedScrollable: boolean | undefined;
69
- cellAnimations: ReorderableListCellAnimations | undefined;
70
- shouldUpdateActiveItem: boolean | undefined;
71
- panEnabled: boolean;
72
- panActivateAfterLongPress: number | undefined;
61
+ scrollable: boolean | undefined;
62
+ scrollEnabled: boolean | undefined;
73
63
  }
74
64
 
75
- export const useReorderableListCore = <T>({
76
- ref,
77
- autoscrollThreshold,
78
- autoscrollThresholdOffset,
79
- autoscrollSpeedScale,
80
- autoscrollDelay,
81
- autoscrollActivationDelta,
82
- animationDuration,
83
- onReorder,
84
- onDragStart,
85
- onDragEnd,
86
- onLayout,
87
- onIndexChange,
88
- scrollViewContainerRef,
89
- scrollViewHeightY,
90
- scrollViewScrollOffsetY,
91
- scrollViewScrollEnabled,
92
- initialScrollEnabled,
93
- initialScrollViewScrollEnabled,
94
- nestedScrollable,
95
- cellAnimations,
96
- shouldUpdateActiveItem,
97
- panActivateAfterLongPress,
98
- panEnabled,
99
- }: UseReorderableListCoreArgs<T>) => {
65
+ const ReorderableListCore = <T,>(
66
+ {
67
+ autoscrollThreshold = 0.1,
68
+ autoscrollThresholdOffset,
69
+ autoscrollSpeedScale = 1,
70
+ autoscrollDelay = AUTOSCROLL_CONFIG.delay,
71
+ autoscrollActivationDelta = 5,
72
+ animationDuration = 200,
73
+ onLayout,
74
+ onReorder,
75
+ onScroll,
76
+ onDragStart,
77
+ onDragEnd,
78
+ onIndexChange,
79
+ scrollViewContainerRef,
80
+ scrollViewPageY,
81
+ scrollViewHeightY,
82
+ scrollViewScrollOffsetY,
83
+ scrollViewScrollEnabled,
84
+ scrollable,
85
+ outerScrollGesture,
86
+ cellAnimations,
87
+ shouldUpdateActiveItem,
88
+ panGesture,
89
+ panEnabled = true,
90
+ panActivateAfterLongPress,
91
+ data,
92
+ ...rest
93
+ }: ReorderableListCoreProps<T>,
94
+ ref: React.ForwardedRef<FlatList<T>>,
95
+ ) => {
96
+ // FlatList will default to true if we pass explicitly undefined,
97
+ // but internally we would treat it as false, so we force true.
98
+ const initialScrollEnabled =
99
+ typeof rest.scrollEnabled === 'undefined' ? true : rest.scrollEnabled;
100
+ const initialScrollViewScrollEnabled =
101
+ typeof rest.initialScrollViewScrollEnabled === 'undefined'
102
+ ? true
103
+ : rest.initialScrollViewScrollEnabled;
104
+
100
105
  const flatListRef = useAnimatedRef<FlatList>();
101
106
  const [activeIndex, setActiveIndex] = useState(-1);
102
107
  const scrollEnabled = useSharedValue(initialScrollEnabled);
@@ -107,7 +112,7 @@ export const useReorderableListCore = <T>({
107
112
  const startItemDragCenterY = useSharedValue<number>(0);
108
113
  const flatListScrollOffsetY = useSharedValue(0);
109
114
  const flatListHeightY = useSharedValue(0);
110
- const nestedFlatListPositionY = useSharedValue(0);
115
+ const flatListPageY = useSharedValue(0);
111
116
  // The scroll y translation of the list since drag start
112
117
  const dragScrollTranslationY = useSharedValue(0);
113
118
  // The initial scroll offset y of the list on drag start
@@ -119,6 +124,9 @@ export const useReorderableListCore = <T>({
119
124
  const draggedHeight = useSharedValue(0);
120
125
  const itemOffset = useSharedValue<number[]>([]);
121
126
  const itemHeight = useSharedValue<number[]>([]);
127
+ // We need to track data length since itemOffset and itemHeight might contain more data than we need.
128
+ // e.g. items are removed from the list, in which case layout data for those items is set to 0.
129
+ const itemCount = useSharedValue(data.length);
122
130
  const autoscrollTrigger = useSharedValue(-1);
123
131
  const lastAutoscrollTrigger = useSharedValue(-1);
124
132
  const dragY = useSharedValue(0);
@@ -135,11 +143,41 @@ export const useReorderableListCore = <T>({
135
143
  const dragDirection = useSharedValue(0);
136
144
  const lastDragDirectionPivot = useSharedValue<number | null>(null);
137
145
  const autoscrollDelta = useSharedValue(autoscrollActivationDelta);
146
+ const prevItemCount = useRef(data.length);
147
+
148
+ // Position of the list relative to the scroll container
149
+ const nestedFlatListPositionY = useDerivedValue(
150
+ () => flatListPageY.value - (scrollViewPageY?.value || 0),
151
+ );
138
152
 
139
153
  useEffect(() => {
140
154
  duration.value = animationDuration;
141
155
  autoscrollDelta.value = autoscrollActivationDelta;
142
- }, [duration, animationDuration, autoscrollDelta, autoscrollActivationDelta]);
156
+ }, [
157
+ duration,
158
+ animationDuration,
159
+ autoscrollDelta,
160
+ autoscrollActivationDelta,
161
+ itemCount,
162
+ ]);
163
+
164
+ useEffect(() => {
165
+ itemCount.value = data.length;
166
+
167
+ // This could be done unmount of the removed cell, however it leads to bugs.
168
+ // Surprisingly the unmount gets sometimes called after the onLayout event
169
+ // setting all layout data to 0 and breaking the list. So we solve it like this.
170
+ if (data.length < prevItemCount.current) {
171
+ for (let i = data.length; i < prevItemCount.current; i++) {
172
+ runOnUI(() => {
173
+ itemHeight.value[i] = 0;
174
+ itemOffset.value[i] = 0;
175
+ })();
176
+ }
177
+ }
178
+
179
+ prevItemCount.current = data.length;
180
+ }, [data.length, itemHeight, itemOffset, itemCount]);
143
181
 
144
182
  const listContextValue = useMemo(
145
183
  () => ({
@@ -234,8 +272,10 @@ export const useReorderableListCore = <T>({
234
272
 
235
273
  const panGestureHandler = useMemo(
236
274
  () =>
237
- Gesture.Pan()
275
+ (panGesture || Gesture.Pan())
238
276
  .onBegin(e => {
277
+ 'worklet';
278
+
239
279
  // prevent new dragging until item is completely released
240
280
  if (state.value === ReorderableListState.IDLE) {
241
281
  startY.value = e.y;
@@ -246,6 +286,8 @@ export const useReorderableListCore = <T>({
246
286
  }
247
287
  })
248
288
  .onUpdate(e => {
289
+ 'worklet';
290
+
249
291
  if (state.value === ReorderableListState.DRAGGED) {
250
292
  setDragDirection(e);
251
293
  }
@@ -262,9 +304,18 @@ export const useReorderableListCore = <T>({
262
304
  gestureState.value = e.state;
263
305
  }
264
306
  })
265
- .onEnd(e => (gestureState.value = e.state))
266
- .onFinalize(e => (gestureState.value = e.state)),
307
+ .onEnd(e => {
308
+ 'worklet';
309
+
310
+ gestureState.value = e.state;
311
+ })
312
+ .onFinalize(e => {
313
+ 'worklet';
314
+
315
+ gestureState.value = e.state;
316
+ }),
267
317
  [
318
+ panGesture,
268
319
  state,
269
320
  startY,
270
321
  currentY,
@@ -278,7 +329,7 @@ export const useReorderableListCore = <T>({
278
329
  ],
279
330
  );
280
331
 
281
- const panGestureHandlerWithOptions = useMemo(() => {
332
+ const panGestureHandlerWithPropOptions = useMemo(() => {
282
333
  if (typeof panActivateAfterLongPress === 'number') {
283
334
  panGestureHandler.activateAfterLongPress(panActivateAfterLongPress);
284
335
  }
@@ -291,8 +342,9 @@ export const useReorderableListCore = <T>({
291
342
  }, [panActivateAfterLongPress, panEnabled, panGestureHandler]);
292
343
 
293
344
  const gestureHandler = useMemo(
294
- () => Gesture.Simultaneous(Gesture.Native(), panGestureHandlerWithOptions),
295
- [panGestureHandlerWithOptions],
345
+ () =>
346
+ Gesture.Simultaneous(Gesture.Native(), panGestureHandlerWithPropOptions),
347
+ [panGestureHandlerWithPropOptions],
296
348
  );
297
349
 
298
350
  const setScrollEnabled = useCallback(
@@ -354,15 +406,7 @@ export const useReorderableListCore = <T>({
354
406
  runOnUI(resetSharedValues)();
355
407
 
356
408
  if (fromIndex !== toIndex) {
357
- const updateState = () => {
358
- onReorder({from: fromIndex, to: toIndex});
359
- };
360
-
361
- if (!hasAutomaticBatching) {
362
- unstable_batchedUpdates(updateState);
363
- } else {
364
- updateState();
365
- }
409
+ onReorder({from: fromIndex, to: toIndex});
366
410
  }
367
411
  };
368
412
 
@@ -414,7 +458,7 @@ export const useReorderableListCore = <T>({
414
458
  const currentHeight = itemHeight.value[currentIndex.value];
415
459
  const currentCenter = currentOffset + currentHeight * 0.5;
416
460
 
417
- const max = itemOffset.value.length;
461
+ const max = itemCount.value;
418
462
  const possibleIndex =
419
463
  relativeDragCenterY < currentCenter
420
464
  ? Math.max(0, currentIndex.value - 1)
@@ -441,6 +485,7 @@ export const useReorderableListCore = <T>({
441
485
  }, [
442
486
  currentIndex,
443
487
  currentItemDragCenterY,
488
+ itemCount,
444
489
  itemOffset,
445
490
  itemHeight,
446
491
  flatListScrollOffsetY,
@@ -543,7 +588,7 @@ export const useReorderableListCore = <T>({
543
588
  },
544
589
  );
545
590
 
546
- const calculateHiddenArea = useCallback(() => {
591
+ const computeHiddenArea = useCallback(() => {
547
592
  'worklet';
548
593
  if (!scrollViewScrollOffsetY || !scrollViewHeightY) {
549
594
  return {top: 0, bottom: 0};
@@ -569,103 +614,121 @@ export const useReorderableListCore = <T>({
569
614
  flatListHeightY,
570
615
  ]);
571
616
 
572
- const calculateThresholdArea = useCallback(
573
- (hiddenArea: {top: number; bottom: number}) => {
574
- 'worklet';
575
- const offsetTop = Math.max(0, autoscrollThresholdOffset?.top || 0);
576
- const offsetBottom = Math.max(0, autoscrollThresholdOffset?.bottom || 0);
577
- const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
578
- const visibleHeight =
579
- flatListHeightY.value -
580
- (hiddenArea.top + hiddenArea.bottom) -
581
- (offsetTop + offsetBottom);
617
+ const computeThresholdArea = useCallback(() => {
618
+ 'worklet';
582
619
 
583
- const area = visibleHeight * threshold;
584
- const top = area + offsetTop;
585
- const bottom = flatListHeightY.value - area - offsetBottom;
620
+ const hiddenArea = computeHiddenArea();
586
621
 
587
- return {top, bottom};
588
- },
589
- [autoscrollThreshold, autoscrollThresholdOffset, flatListHeightY],
590
- );
622
+ const offsetTop = Math.max(0, autoscrollThresholdOffset?.top || 0);
623
+ const offsetBottom = Math.max(0, autoscrollThresholdOffset?.bottom || 0);
624
+ const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
625
+ const visibleHeight =
626
+ flatListHeightY.value -
627
+ (hiddenArea.top + hiddenArea.bottom) -
628
+ (offsetTop + offsetBottom);
591
629
 
592
- const calculateThresholdAreaParent = useCallback(
593
- (hiddenArea: {top: number; bottom: number}) => {
594
- 'worklet';
595
- const offsetTop = Math.max(0, autoscrollThresholdOffset?.top || 0);
596
- const offsetBottom = Math.max(0, autoscrollThresholdOffset?.bottom || 0);
597
- const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
598
-
599
- const area = flatListHeightY.value * threshold;
600
- const top = area + offsetTop;
601
- const bottom = flatListHeightY.value - area - offsetBottom;
602
-
603
- // if the hidden area is 0 then we don't have a threshold area
604
- // we might have floating errors like 0.0001 which we should ignore
605
- return {
606
- top: hiddenArea.top > 0.1 ? top + hiddenArea.top : 0,
607
- bottom: hiddenArea.bottom > 0.1 ? bottom - hiddenArea.bottom : 0,
608
- };
609
- },
610
- [autoscrollThreshold, autoscrollThresholdOffset, flatListHeightY],
611
- );
630
+ const area = visibleHeight * threshold;
631
+ const top = area + offsetTop;
632
+ const bottom = flatListHeightY.value - area - offsetBottom;
633
+
634
+ return {top, bottom};
635
+ }, [
636
+ computeHiddenArea,
637
+ autoscrollThreshold,
638
+ autoscrollThresholdOffset,
639
+ flatListHeightY,
640
+ ]);
641
+
642
+ const computeContainerThresholdArea = useCallback(() => {
643
+ 'worklet';
644
+ if (!scrollViewHeightY) {
645
+ return {top: -Infinity, bottom: Infinity};
646
+ }
612
647
 
613
- const shouldScrollParent = useCallback(
648
+ const offsetTop = Math.max(0, autoscrollThresholdOffset?.top || 0);
649
+ const offsetBottom = Math.max(0, autoscrollThresholdOffset?.bottom || 0);
650
+ const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
651
+ const visibleHeight = scrollViewHeightY.value - (offsetTop + offsetBottom);
652
+
653
+ const area = visibleHeight * threshold;
654
+ const top = area + offsetTop;
655
+ const bottom = visibleHeight - area - offsetBottom;
656
+
657
+ return {top, bottom};
658
+ }, [autoscrollThreshold, autoscrollThresholdOffset, scrollViewHeightY]);
659
+
660
+ const shouldScrollContainer = useCallback(
614
661
  (y: number) => {
615
662
  'worklet';
616
- const hiddenArea = calculateHiddenArea();
617
- const thresholdAreaParent = calculateThresholdAreaParent(hiddenArea);
663
+ const containerThresholdArea = computeContainerThresholdArea();
664
+ const nestedListHiddenArea = computeHiddenArea();
618
665
 
619
- // we might have floating errors like 0.0001 which we should ignore
666
+ // We should scroll the container if there's a hidden part of the nested list.
667
+ // We might have floating errors like 0.0001 which we should ignore.
620
668
  return (
621
- (hiddenArea.top > 0.1 && y <= thresholdAreaParent.top) ||
622
- (hiddenArea.bottom > 0.1 && y >= thresholdAreaParent.bottom)
669
+ (nestedListHiddenArea.top > 0.01 && y <= containerThresholdArea.top) ||
670
+ (nestedListHiddenArea.bottom > 0.01 &&
671
+ y >= containerThresholdArea.bottom)
623
672
  );
624
673
  },
625
- [calculateHiddenArea, calculateThresholdAreaParent],
674
+ [computeHiddenArea, computeContainerThresholdArea],
626
675
  );
627
676
 
628
- const scrollDirection = useCallback(
629
- (y: number) => {
630
- 'worklet';
631
- const hiddenArea = calculateHiddenArea();
677
+ const getRelativeContainerY = useCallback(() => {
678
+ 'worklet';
632
679
 
633
- if (shouldScrollParent(y)) {
634
- const thresholdAreaParent = calculateThresholdAreaParent(hiddenArea);
635
- if (y <= thresholdAreaParent.top) {
636
- return -1;
637
- }
680
+ return (
681
+ currentY.value +
682
+ nestedFlatListPositionY.value -
683
+ scrollViewDragInitialScrollOffsetY.value
684
+ );
685
+ }, [currentY, nestedFlatListPositionY, scrollViewDragInitialScrollOffsetY]);
638
686
 
639
- if (y >= thresholdAreaParent.bottom) {
640
- return 1;
641
- }
687
+ const getRelativeListY = useCallback(() => {
688
+ 'worklet';
642
689
 
643
- return 0;
644
- } else if (nestedScrollable) {
645
- const thresholdArea = calculateThresholdArea(hiddenArea);
646
- if (y <= thresholdArea.top) {
647
- return -1;
648
- }
690
+ return currentY.value + scrollViewDragScrollTranslationY.value;
691
+ }, [currentY, scrollViewDragScrollTranslationY]);
649
692
 
650
- if (y >= thresholdArea.bottom) {
651
- return 1;
652
- }
693
+ const scrollDirection = useCallback(() => {
694
+ 'worklet';
695
+
696
+ const relativeContainerY = getRelativeContainerY();
697
+ if (shouldScrollContainer(relativeContainerY)) {
698
+ const containerThresholdArea = computeContainerThresholdArea();
699
+ if (relativeContainerY <= containerThresholdArea.top) {
700
+ return -1;
653
701
  }
654
702
 
655
- return 0;
656
- },
657
- [
658
- nestedScrollable,
659
- shouldScrollParent,
660
- calculateHiddenArea,
661
- calculateThresholdArea,
662
- calculateThresholdAreaParent,
663
- ],
664
- );
703
+ if (relativeContainerY >= containerThresholdArea.bottom) {
704
+ return 1;
705
+ }
706
+ } else if (scrollable) {
707
+ const relativeListY = getRelativeListY();
708
+ const thresholdArea = computeThresholdArea();
709
+
710
+ if (relativeListY <= thresholdArea.top) {
711
+ return -1;
712
+ }
713
+
714
+ if (relativeListY >= thresholdArea.bottom) {
715
+ return 1;
716
+ }
717
+ }
718
+
719
+ return 0;
720
+ }, [
721
+ shouldScrollContainer,
722
+ computeThresholdArea,
723
+ computeContainerThresholdArea,
724
+ getRelativeContainerY,
725
+ getRelativeListY,
726
+ scrollable,
727
+ ]);
665
728
 
666
729
  useAnimatedReaction(
667
- () => currentY.value + scrollViewDragScrollTranslationY.value,
668
- y => {
730
+ () => currentY.value,
731
+ () => {
669
732
  if (
670
733
  state.value === ReorderableListState.DRAGGED ||
671
734
  state.value === ReorderableListState.AUTOSCROLL
@@ -676,7 +739,7 @@ export const useReorderableListCore = <T>({
676
739
  // 1. Within the threshold area (top or bottom of list)
677
740
  // 2. Have dragged in the same direction as the scroll
678
741
  // 3. Not already in autoscroll mode
679
- if (dragDirection.value === scrollDirection(y)) {
742
+ if (dragDirection.value === scrollDirection()) {
680
743
  // When the first two conditions are met and it's already in autoscroll mode, we let it continue (no-op)
681
744
  if (state.value !== ReorderableListState.AUTOSCROLL) {
682
745
  state.value = ReorderableListState.AUTOSCROLL;
@@ -697,9 +760,8 @@ export const useReorderableListCore = <T>({
697
760
  autoscrollTrigger.value !== lastAutoscrollTrigger.value &&
698
761
  state.value === ReorderableListState.AUTOSCROLL
699
762
  ) {
700
- let y = currentY.value + scrollViewDragScrollTranslationY.value;
701
763
  const autoscrollIncrement =
702
- scrollDirection(y) *
764
+ dragDirection.value *
703
765
  AUTOSCROLL_CONFIG.increment *
704
766
  autoscrollSpeedScale;
705
767
 
@@ -708,7 +770,13 @@ export const useReorderableListCore = <T>({
708
770
  let listRef =
709
771
  flatListRef as unknown as AnimatedRef<Animated.ScrollView>;
710
772
 
711
- if (shouldScrollParent(y) && scrollViewScrollOffsetY) {
773
+ // Checking on every autoscroll whether to scroll the container,
774
+ // this allows to smoothly pass the scroll from the container to the nested list
775
+ // without any gesture input.
776
+ if (
777
+ scrollViewScrollOffsetY &&
778
+ shouldScrollContainer(getRelativeContainerY())
779
+ ) {
712
780
  scrollOffset = scrollViewScrollOffsetY.value;
713
781
  listRef =
714
782
  scrollViewContainerRef as unknown as AnimatedRef<Animated.ScrollView>;
@@ -750,7 +818,7 @@ export const useReorderableListCore = <T>({
750
818
  }
751
819
  });
752
820
 
753
- // parent scroll handler
821
+ // container scroll handler
754
822
  useAnimatedReaction(
755
823
  () => scrollViewScrollOffsetY?.value,
756
824
  value => {
@@ -826,35 +894,104 @@ export const useReorderableListCore = <T>({
826
894
 
827
895
  const handleFlatListLayout = useCallback(
828
896
  (e: LayoutChangeEvent) => {
829
- nestedFlatListPositionY.value = e.nativeEvent.layout.y;
830
897
  flatListHeightY.value = e.nativeEvent.layout.height;
831
898
 
899
+ // If nested in a scroll container.
900
+ if (scrollViewScrollOffsetY) {
901
+ // Timeout fixes a bug where measure returns height 0.
902
+ setTimeout(() => {
903
+ runOnUI(() => {
904
+ const measurement = measure(flatListRef);
905
+ if (!measurement) {
906
+ return;
907
+ }
908
+
909
+ flatListPageY.value = measurement.pageY;
910
+ })();
911
+ }, 100);
912
+ }
913
+
832
914
  onLayout?.(e);
833
915
  },
834
- [nestedFlatListPositionY, flatListHeightY, onLayout],
916
+ [
917
+ flatListRef,
918
+ flatListPageY,
919
+ flatListHeightY,
920
+ scrollViewScrollOffsetY,
921
+ onLayout,
922
+ ],
835
923
  );
836
924
 
837
- const handleRef = (value: FlatList<T>) => {
838
- flatListRef(value);
925
+ const handleRef = useCallback(
926
+ (value: FlatList<T>) => {
927
+ flatListRef(value);
928
+
929
+ if (typeof ref === 'function') {
930
+ ref(value);
931
+ } else if (ref) {
932
+ ref.current = value;
933
+ }
934
+ },
935
+ [flatListRef, ref],
936
+ );
839
937
 
840
- if (typeof ref === 'function') {
841
- ref(value);
842
- } else if (ref) {
843
- ref.current = value;
938
+ const combinedGesture = useMemo(() => {
939
+ // android is able to handle nested scroll view, but not the full height ones like iOS
940
+ if (outerScrollGesture && !(Platform.OS === 'android' && scrollable)) {
941
+ return Gesture.Simultaneous(outerScrollGesture, gestureHandler);
844
942
  }
845
- };
846
943
 
847
- return {
848
- gestureHandler,
944
+ return gestureHandler;
945
+ }, [scrollable, outerScrollGesture, gestureHandler]);
946
+
947
+ const composedScrollHandler = useComposedEventHandler([
849
948
  handleScroll,
850
- handleFlatListLayout,
851
- handleRef,
852
- startDrag,
853
- listContextValue,
854
- itemOffset,
855
- itemHeight,
856
- draggedIndex,
857
- dragY,
858
- duration,
859
- };
949
+ onScroll || null,
950
+ ]);
951
+
952
+ const renderAnimatedCell = useCallback(
953
+ ({cellKey, ...props}: CellRendererProps<T>) => (
954
+ <ReorderableListCell
955
+ {...props}
956
+ // forces remount with key change on reorder
957
+ key={`${cellKey}+${props.index}`}
958
+ itemOffset={itemOffset}
959
+ itemHeight={itemHeight}
960
+ dragY={dragY}
961
+ draggedIndex={draggedIndex}
962
+ animationDuration={duration}
963
+ startDrag={startDrag}
964
+ />
965
+ ),
966
+ [itemOffset, itemHeight, dragY, draggedIndex, duration, startDrag],
967
+ );
968
+
969
+ return (
970
+ <ReorderableListContext.Provider value={listContextValue}>
971
+ <GestureDetector gesture={combinedGesture}>
972
+ <AnimatedFlatList
973
+ {...rest}
974
+ ref={handleRef}
975
+ data={data}
976
+ CellRendererComponent={renderAnimatedCell}
977
+ onLayout={handleFlatListLayout}
978
+ onScroll={composedScrollHandler}
979
+ scrollEventThrottle={1}
980
+ horizontal={false}
981
+ removeClippedSubviews={false}
982
+ numColumns={1}
983
+ />
984
+ </GestureDetector>
985
+ </ReorderableListContext.Provider>
986
+ );
860
987
  };
988
+
989
+ const MemoizedReorderableListCore = React.memo(
990
+ React.forwardRef(ReorderableListCore),
991
+ ) as <T>(
992
+ props: ReorderableListCoreProps<T> & {
993
+ ref?: React.ForwardedRef<FlatList<T> | null>;
994
+ },
995
+ ) => React.ReactElement;
996
+
997
+ export {MemoizedReorderableListCore as ReorderableListCore};