react-native-reorderable-list 0.12.0 → 0.13.1

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 (47) hide show
  1. package/README.md +0 -1
  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/ReorderableListCell.js +2 -2
  7. package/lib/commonjs/components/ReorderableListCell.js.map +1 -1
  8. package/lib/commonjs/components/ReorderableListCore/ReorderableListCore.js +3 -3
  9. package/lib/commonjs/components/ReorderableListCore/ReorderableListCore.js.map +1 -1
  10. package/lib/commonjs/components/ReorderableListCore/useReorderableListCore.js +160 -99
  11. package/lib/commonjs/components/ReorderableListCore/useReorderableListCore.js.map +1 -1
  12. package/lib/commonjs/components/ScrollViewContainer.js +14 -3
  13. package/lib/commonjs/components/ScrollViewContainer.js.map +1 -1
  14. package/lib/commonjs/contexts/ScrollViewContainerContext.js.map +1 -1
  15. package/lib/module/components/NestedReorderableList.js +2 -0
  16. package/lib/module/components/NestedReorderableList.js.map +1 -1
  17. package/lib/module/components/ReorderableList.js +4 -3
  18. package/lib/module/components/ReorderableList.js.map +1 -1
  19. package/lib/module/components/ReorderableListCell.js +2 -2
  20. package/lib/module/components/ReorderableListCell.js.map +1 -1
  21. package/lib/module/components/ReorderableListCore/ReorderableListCore.js +3 -3
  22. package/lib/module/components/ReorderableListCore/ReorderableListCore.js.map +1 -1
  23. package/lib/module/components/ReorderableListCore/useReorderableListCore.js +161 -100
  24. package/lib/module/components/ReorderableListCore/useReorderableListCore.js.map +1 -1
  25. package/lib/module/components/ScrollViewContainer.js +16 -5
  26. package/lib/module/components/ScrollViewContainer.js.map +1 -1
  27. package/lib/module/contexts/ScrollViewContainerContext.js.map +1 -1
  28. package/lib/typescript/components/NestedReorderableList.d.ts.map +1 -1
  29. package/lib/typescript/components/ReorderableList.d.ts.map +1 -1
  30. package/lib/typescript/components/ReorderableListCore/ReorderableListCore.d.ts +1 -0
  31. package/lib/typescript/components/ReorderableListCore/ReorderableListCore.d.ts.map +1 -1
  32. package/lib/typescript/components/ReorderableListCore/useReorderableListCore.d.ts +4 -5
  33. package/lib/typescript/components/ReorderableListCore/useReorderableListCore.d.ts.map +1 -1
  34. package/lib/typescript/components/ScrollViewContainer.d.ts.map +1 -1
  35. package/lib/typescript/contexts/ScrollViewContainerContext.d.ts +1 -0
  36. package/lib/typescript/contexts/ScrollViewContainerContext.d.ts.map +1 -1
  37. package/lib/typescript/types/props.d.ts +0 -5
  38. package/lib/typescript/types/props.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/NestedReorderableList.tsx +2 -0
  41. package/src/components/ReorderableList.tsx +3 -2
  42. package/src/components/ReorderableListCell.tsx +3 -3
  43. package/src/components/ReorderableListCore/ReorderableListCore.tsx +4 -3
  44. package/src/components/ReorderableListCore/useReorderableListCore.ts +277 -184
  45. package/src/components/ScrollViewContainer.tsx +23 -5
  46. package/src/contexts/ScrollViewContainerContext.ts +1 -0
  47. package/src/types/props.ts +0 -5
@@ -16,12 +16,14 @@ import Animated, {
16
16
  AnimatedRef,
17
17
  Easing,
18
18
  SharedValue,
19
+ measure,
19
20
  runOnJS,
20
21
  runOnUI,
21
22
  scrollTo,
22
23
  useAnimatedReaction,
23
24
  useAnimatedRef,
24
25
  useAnimatedScrollHandler,
26
+ useDerivedValue,
25
27
  useSharedValue,
26
28
  withDelay,
27
29
  withTiming,
@@ -37,9 +39,9 @@ import {
37
39
  ReorderableListDragEndEvent,
38
40
  ReorderableListDragStartEvent,
39
41
  ReorderableListIndexChangeEvent,
42
+ ReorderableListReorderEvent,
40
43
  ReorderableListState,
41
44
  } from '../../types';
42
- import type {ReorderableListReorderEvent} from '../../types';
43
45
 
44
46
  const version = React.version.split('.');
45
47
  const hasAutomaticBatching = version.length
@@ -54,19 +56,19 @@ interface UseReorderableListCoreArgs<T> {
54
56
  autoscrollDelay: number;
55
57
  autoscrollActivationDelta: number;
56
58
  animationDuration: number;
57
- dragReorderThreshold: number;
58
59
  onReorder: (event: ReorderableListReorderEvent) => void;
59
60
  onDragStart?: (event: ReorderableListDragStartEvent) => void;
60
61
  onDragEnd?: (event: ReorderableListDragEndEvent) => void;
61
62
  onIndexChange?: (event: ReorderableListIndexChangeEvent) => void;
62
63
  onLayout?: (event: LayoutChangeEvent) => void;
63
64
  scrollViewContainerRef: React.RefObject<ScrollView> | undefined;
65
+ scrollViewPageY: SharedValue<number> | undefined;
64
66
  scrollViewHeightY: SharedValue<number> | undefined;
65
67
  scrollViewScrollOffsetY: SharedValue<number> | undefined;
66
68
  scrollViewScrollEnabled: SharedValue<boolean> | undefined;
69
+ scrollable: boolean | undefined;
67
70
  initialScrollEnabled: boolean | undefined;
68
71
  initialScrollViewScrollEnabled: boolean | undefined;
69
- nestedScrollable: boolean | undefined;
70
72
  cellAnimations: ReorderableListCellAnimations | undefined;
71
73
  shouldUpdateActiveItem: boolean | undefined;
72
74
  panEnabled: boolean;
@@ -81,19 +83,19 @@ export const useReorderableListCore = <T>({
81
83
  autoscrollDelay,
82
84
  autoscrollActivationDelta,
83
85
  animationDuration,
84
- dragReorderThreshold,
85
86
  onReorder,
86
87
  onDragStart,
87
88
  onDragEnd,
88
89
  onLayout,
89
90
  onIndexChange,
90
91
  scrollViewContainerRef,
92
+ scrollViewPageY,
91
93
  scrollViewHeightY,
92
94
  scrollViewScrollOffsetY,
93
95
  scrollViewScrollEnabled,
96
+ scrollable,
94
97
  initialScrollEnabled,
95
98
  initialScrollViewScrollEnabled,
96
- nestedScrollable,
97
99
  cellAnimations,
98
100
  shouldUpdateActiveItem,
99
101
  panActivateAfterLongPress,
@@ -105,22 +107,25 @@ export const useReorderableListCore = <T>({
105
107
  const gestureState = useSharedValue<State>(State.UNDETERMINED);
106
108
  const currentY = useSharedValue(0);
107
109
  const currentTranslationY = useSharedValue(0);
110
+ const currentItemDragCenterY = useSharedValue<number | null>(null);
111
+ const startItemDragCenterY = useSharedValue<number>(0);
108
112
  const flatListScrollOffsetY = useSharedValue(0);
109
113
  const flatListHeightY = useSharedValue(0);
110
- const nestedFlatListPositionY = useSharedValue(0);
114
+ const flatListPageY = useSharedValue(0);
115
+ // The scroll y translation of the list since drag start
111
116
  const dragScrollTranslationY = useSharedValue(0);
117
+ // The initial scroll offset y of the list on drag start
112
118
  const dragInitialScrollOffsetY = useSharedValue(0);
119
+ // The scroll y translation of the ScrollViewContainer since drag start
113
120
  const scrollViewDragScrollTranslationY = useSharedValue(0);
121
+ // The initial scroll offset y of the ScrollViewContainer on drag start
114
122
  const scrollViewDragInitialScrollOffsetY = useSharedValue(0);
115
123
  const draggedHeight = useSharedValue(0);
116
124
  const itemOffset = useSharedValue<number[]>([]);
117
125
  const itemHeight = useSharedValue<number[]>([]);
118
126
  const autoscrollTrigger = useSharedValue(-1);
119
127
  const lastAutoscrollTrigger = useSharedValue(-1);
120
- const previousY = useSharedValue(0);
121
128
  const dragY = useSharedValue(0);
122
- const previousDirection = useSharedValue(0);
123
- const previousIndex = useSharedValue(-1);
124
129
  const currentIndex = useSharedValue(-1);
125
130
  const draggedIndex = useSharedValue(-1);
126
131
  const state = useSharedValue<ReorderableListState>(ReorderableListState.IDLE);
@@ -135,6 +140,11 @@ export const useReorderableListCore = <T>({
135
140
  const lastDragDirectionPivot = useSharedValue<number | null>(null);
136
141
  const autoscrollDelta = useSharedValue(autoscrollActivationDelta);
137
142
 
143
+ // Position of the list relative to the scroll container
144
+ const nestedFlatListPositionY = useDerivedValue(
145
+ () => flatListPageY.value - (scrollViewPageY?.value || 0),
146
+ );
147
+
138
148
  useEffect(() => {
139
149
  duration.value = animationDuration;
140
150
  autoscrollDelta.value = autoscrollActivationDelta;
@@ -175,6 +185,8 @@ export const useReorderableListCore = <T>({
175
185
  * Decides the intended drag direction of the user.
176
186
  * This is used to to determine if the user intends to autoscroll
177
187
  * when within the threshold area.
188
+ *
189
+ * @param e - The payload of the pan gesture update event.
178
190
  */
179
191
  const setDragDirection = useCallback(
180
192
  (e: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
@@ -196,6 +208,39 @@ export const useReorderableListCore = <T>({
196
208
  [dragDirection, lastDragDirectionPivot, autoscrollDelta],
197
209
  );
198
210
 
211
+ const setCurrentItemDragCenterY = useCallback(
212
+ (e: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
213
+ 'worklet';
214
+
215
+ if (currentItemDragCenterY.value === null) {
216
+ if (currentIndex.value >= 0) {
217
+ const itemCenter = itemHeight.value[currentIndex.value] * 0.5;
218
+ // the y coordinate of the item relative to the list
219
+ const itemY =
220
+ itemOffset.value[currentIndex.value] -
221
+ (flatListScrollOffsetY.value +
222
+ scrollViewDragScrollTranslationY.value);
223
+
224
+ const value = itemY + itemCenter + e.translationY;
225
+ startItemDragCenterY.value = value;
226
+ currentItemDragCenterY.value = value;
227
+ }
228
+ } else {
229
+ currentItemDragCenterY.value =
230
+ startItemDragCenterY.value + e.translationY;
231
+ }
232
+ },
233
+ [
234
+ currentItemDragCenterY,
235
+ currentIndex,
236
+ startItemDragCenterY,
237
+ itemOffset,
238
+ itemHeight,
239
+ flatListScrollOffsetY,
240
+ scrollViewDragScrollTranslationY,
241
+ ],
242
+ );
243
+
199
244
  const panGestureHandler = useMemo(
200
245
  () =>
201
246
  Gesture.Pan()
@@ -215,6 +260,8 @@ export const useReorderableListCore = <T>({
215
260
  }
216
261
 
217
262
  if (state.value !== ReorderableListState.RELEASED) {
263
+ setCurrentItemDragCenterY(e);
264
+
218
265
  currentY.value = startY.value + e.translationY;
219
266
  currentTranslationY.value = e.translationY;
220
267
  dragY.value =
@@ -233,9 +280,10 @@ export const useReorderableListCore = <T>({
233
280
  currentTranslationY,
234
281
  dragY,
235
282
  gestureState,
236
- setDragDirection,
237
283
  dragScrollTranslationY,
238
284
  scrollViewDragScrollTranslationY,
285
+ setDragDirection,
286
+ setCurrentItemDragCenterY,
239
287
  ],
240
288
  );
241
289
 
@@ -295,6 +343,7 @@ export const useReorderableListCore = <T>({
295
343
  scrollViewDragScrollTranslationY.value = 0;
296
344
  dragDirection.value = 0;
297
345
  lastDragDirectionPivot.value = null;
346
+ currentItemDragCenterY.value = null;
298
347
  }, [
299
348
  state,
300
349
  draggedIndex,
@@ -303,6 +352,7 @@ export const useReorderableListCore = <T>({
303
352
  scrollViewDragScrollTranslationY,
304
353
  dragDirection,
305
354
  lastDragDirectionPivot,
355
+ currentItemDragCenterY,
306
356
  ]);
307
357
 
308
358
  const resetSharedValuesAfterAnimations = useCallback(() => {
@@ -325,102 +375,106 @@ export const useReorderableListCore = <T>({
325
375
  }
326
376
  };
327
377
 
328
- const getIndexFromY = useCallback(
329
- (y: number) => {
378
+ const recomputeLayout = useCallback(
379
+ (from: number, to: number) => {
330
380
  'worklet';
331
381
 
332
- const relativeY = flatListScrollOffsetY.value + y;
333
- const count = itemOffset.value.length;
382
+ const itemDirection = to > from;
383
+ const index1 = itemDirection ? from : to;
384
+ const index2 = itemDirection ? to : from;
385
+
386
+ const newOffset1 = itemOffset.value[index1];
387
+ const newHeight1 = itemHeight.value[index2];
388
+ const newOffset2 =
389
+ itemOffset.value[index2] +
390
+ itemHeight.value[index2] -
391
+ itemHeight.value[index1];
392
+ const newHeight2 = itemHeight.value[index1];
393
+
394
+ itemOffset.value[index1] = newOffset1;
395
+ itemHeight.value[index1] = newHeight1;
396
+ itemOffset.value[index2] = newOffset2;
397
+ itemHeight.value[index2] = newHeight2;
398
+ },
399
+ [itemOffset, itemHeight],
400
+ );
334
401
 
335
- for (let i = 0; i < count; i++) {
336
- if (currentIndex.value === i) {
337
- continue;
338
- }
402
+ /**
403
+ * Computes a potential new drop container for the current dragged item and evaluates
404
+ * whether the dragged item center is nearer to the center of the current container or the new one.
405
+ *
406
+ * @returns The new index if the center of the dragged item is closer to the center of
407
+ * the new drop container or the current index if closer to the current drop container.
408
+ */
409
+ const computeCurrentIndex = useCallback(() => {
410
+ 'worklet';
339
411
 
340
- const direction = i > currentIndex.value ? 1 : -1;
341
- const threshold = Math.max(0, Math.min(1, dragReorderThreshold));
342
- const height = itemHeight.value[i];
343
- const offset = itemOffset.value[i] + height * threshold * direction;
412
+ if (currentItemDragCenterY.value === null) {
413
+ return currentIndex.value;
414
+ }
344
415
 
345
- if (
346
- (i === 0 && relativeY <= offset) ||
347
- (i === count - 1 && relativeY >= offset + height) ||
348
- (relativeY >= offset && relativeY <= offset + height)
349
- ) {
350
- return {index: i, direction};
351
- }
416
+ // apply scroll offset and scroll container translation
417
+ const relativeDragCenterY =
418
+ flatListScrollOffsetY.value +
419
+ scrollViewDragScrollTranslationY.value +
420
+ currentItemDragCenterY.value;
421
+
422
+ const currentOffset = itemOffset.value[currentIndex.value];
423
+ const currentHeight = itemHeight.value[currentIndex.value];
424
+ const currentCenter = currentOffset + currentHeight * 0.5;
425
+
426
+ const max = itemOffset.value.length;
427
+ const possibleIndex =
428
+ relativeDragCenterY < currentCenter
429
+ ? Math.max(0, currentIndex.value - 1)
430
+ : Math.min(max - 1, currentIndex.value + 1);
431
+
432
+ if (currentIndex.value !== possibleIndex) {
433
+ let possibleOffset = itemOffset.value[possibleIndex];
434
+ if (possibleIndex > currentIndex.value) {
435
+ possibleOffset += itemHeight.value[possibleIndex] - currentHeight;
352
436
  }
353
437
 
354
- return {
355
- index: currentIndex.value,
356
- direction: previousDirection.value,
357
- };
358
- },
359
- [
360
- dragReorderThreshold,
361
- currentIndex,
362
- flatListScrollOffsetY,
363
- previousDirection,
364
- itemOffset,
365
- itemHeight,
366
- ],
367
- );
438
+ const possibleCenter = possibleOffset + currentHeight * 0.5;
439
+ const distanceFromCurrent = Math.abs(relativeDragCenterY - currentCenter);
440
+ const distanceFromPossible = Math.abs(
441
+ relativeDragCenterY - possibleCenter,
442
+ );
368
443
 
369
- const setCurrentIndex = useCallback(
370
- (y: number) => {
371
- 'worklet';
444
+ return distanceFromCurrent <= distanceFromPossible
445
+ ? currentIndex.value
446
+ : possibleIndex;
447
+ }
372
448
 
373
- const {index: newIndex, direction: newDirection} = getIndexFromY(y);
374
- const delta = Math.abs(previousY.value - y);
449
+ return currentIndex.value;
450
+ }, [
451
+ currentIndex,
452
+ currentItemDragCenterY,
453
+ itemOffset,
454
+ itemHeight,
455
+ flatListScrollOffsetY,
456
+ scrollViewDragScrollTranslationY,
457
+ ]);
375
458
 
376
- if (
377
- currentIndex.value !== newIndex &&
378
- // if the same two items re-swap index check delta and direction to avoid swap flickering
379
- (previousIndex.value !== newIndex ||
380
- (previousDirection.value !== newDirection && delta >= 5))
381
- ) {
382
- const itemDirection = newIndex > currentIndex.value;
383
- const index1 = itemDirection ? currentIndex.value : newIndex;
384
- const index2 = itemDirection ? newIndex : currentIndex.value;
385
-
386
- const newOffset1 = itemOffset.value[index1];
387
- const newHeight1 = itemHeight.value[index2];
388
- const newOffset2 =
389
- itemOffset.value[index2] +
390
- (itemHeight.value[index2] - itemHeight.value[index1]);
391
- const newHeight2 = itemHeight.value[index1];
392
-
393
- itemOffset.value[index1] = newOffset1;
394
- itemHeight.value[index1] = newHeight1;
395
- itemOffset.value[index2] = newOffset2;
396
- itemHeight.value[index2] = newHeight2;
397
-
398
- previousY.value = y;
399
- previousDirection.value = newDirection;
400
- previousIndex.value = currentIndex.value;
401
- currentIndex.value = newIndex;
402
-
403
- onIndexChange?.({index: newIndex});
404
- }
405
- },
406
- [
407
- currentIndex,
408
- previousIndex,
409
- previousDirection,
410
- previousY,
411
- itemOffset,
412
- itemHeight,
413
- getIndexFromY,
414
- onIndexChange,
415
- ],
416
- );
459
+ const setCurrentIndex = useCallback(() => {
460
+ 'worklet';
461
+
462
+ const newIndex = computeCurrentIndex();
463
+
464
+ if (currentIndex.value !== newIndex) {
465
+ recomputeLayout(currentIndex.value, newIndex);
466
+ currentIndex.value = newIndex;
467
+
468
+ onIndexChange?.({index: newIndex});
469
+ }
470
+ }, [currentIndex, computeCurrentIndex, recomputeLayout, onIndexChange]);
417
471
 
418
472
  const runDefaultDragAnimations = useCallback(
419
473
  (type: 'start' | 'end') => {
420
474
  'worklet';
421
475
 
422
476
  // if no custom scale run default
423
- if (!(cellAnimations && 'transformtra' in cellAnimations)) {
477
+ if (!(cellAnimations && 'transform' in cellAnimations)) {
424
478
  const scaleConfig = SCALE_ANIMATION_CONFIG_DEFAULT[type];
425
479
  scaleDefault.value = withTiming(scaleConfig.toValue, scaleConfig);
426
480
  }
@@ -498,7 +552,7 @@ export const useReorderableListCore = <T>({
498
552
  },
499
553
  );
500
554
 
501
- const calculateHiddenArea = useCallback(() => {
555
+ const computeHiddenArea = useCallback(() => {
502
556
  'worklet';
503
557
  if (!scrollViewScrollOffsetY || !scrollViewHeightY) {
504
558
  return {top: 0, bottom: 0};
@@ -524,114 +578,132 @@ export const useReorderableListCore = <T>({
524
578
  flatListHeightY,
525
579
  ]);
526
580
 
527
- const calculateThresholdArea = useCallback(
528
- (hiddenArea: {top: number; bottom: number}) => {
529
- 'worklet';
530
- const offsetTop = Math.max(0, autoscrollThresholdOffset?.top || 0);
531
- const offsetBottom = Math.max(0, autoscrollThresholdOffset?.bottom || 0);
532
- const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
533
- const visibleHeight =
534
- flatListHeightY.value -
535
- (hiddenArea.top + hiddenArea.bottom) -
536
- (offsetTop + offsetBottom);
581
+ const computeThresholdArea = useCallback(() => {
582
+ 'worklet';
537
583
 
538
- const area = visibleHeight * threshold;
539
- const top = area + offsetTop;
540
- const bottom = flatListHeightY.value - area - offsetBottom;
584
+ const hiddenArea = computeHiddenArea();
541
585
 
542
- return {top, bottom};
543
- },
544
- [autoscrollThreshold, autoscrollThresholdOffset, flatListHeightY],
545
- );
586
+ const offsetTop = Math.max(0, autoscrollThresholdOffset?.top || 0);
587
+ const offsetBottom = Math.max(0, autoscrollThresholdOffset?.bottom || 0);
588
+ const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
589
+ const visibleHeight =
590
+ flatListHeightY.value -
591
+ (hiddenArea.top + hiddenArea.bottom) -
592
+ (offsetTop + offsetBottom);
546
593
 
547
- const calculateThresholdAreaParent = useCallback(
548
- (hiddenArea: {top: number; bottom: number}) => {
549
- 'worklet';
550
- const offsetTop = Math.max(0, autoscrollThresholdOffset?.top || 0);
551
- const offsetBottom = Math.max(0, autoscrollThresholdOffset?.bottom || 0);
552
- const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
553
-
554
- const area = flatListHeightY.value * threshold;
555
- const top = area + offsetTop;
556
- const bottom = flatListHeightY.value - area - offsetBottom;
557
-
558
- // if the hidden area is 0 then we don't have a threshold area
559
- // we might have floating errors like 0.0001 which we should ignore
560
- return {
561
- top: hiddenArea.top > 0.1 ? top + hiddenArea.top : 0,
562
- bottom: hiddenArea.bottom > 0.1 ? bottom - hiddenArea.bottom : 0,
563
- };
564
- },
565
- [autoscrollThreshold, autoscrollThresholdOffset, flatListHeightY],
566
- );
594
+ const area = visibleHeight * threshold;
595
+ const top = area + offsetTop;
596
+ const bottom = flatListHeightY.value - area - offsetBottom;
567
597
 
568
- const shouldScrollParent = useCallback(
598
+ return {top, bottom};
599
+ }, [
600
+ computeHiddenArea,
601
+ autoscrollThreshold,
602
+ autoscrollThresholdOffset,
603
+ flatListHeightY,
604
+ ]);
605
+
606
+ const computeContainerThresholdArea = useCallback(() => {
607
+ 'worklet';
608
+ if (!scrollViewHeightY) {
609
+ return {top: -Infinity, bottom: Infinity};
610
+ }
611
+
612
+ const offsetTop = Math.max(0, autoscrollThresholdOffset?.top || 0);
613
+ const offsetBottom = Math.max(0, autoscrollThresholdOffset?.bottom || 0);
614
+ const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
615
+ const visibleHeight = scrollViewHeightY.value - (offsetTop + offsetBottom);
616
+
617
+ const area = visibleHeight * threshold;
618
+ const top = area + offsetTop;
619
+ const bottom = visibleHeight - area - offsetBottom;
620
+
621
+ return {top, bottom};
622
+ }, [autoscrollThreshold, autoscrollThresholdOffset, scrollViewHeightY]);
623
+
624
+ const shouldScrollContainer = useCallback(
569
625
  (y: number) => {
570
626
  'worklet';
571
- const hiddenArea = calculateHiddenArea();
572
- const thresholdAreaParent = calculateThresholdAreaParent(hiddenArea);
627
+ const containerThresholdArea = computeContainerThresholdArea();
628
+ const nestedListHiddenArea = computeHiddenArea();
573
629
 
574
- // we might have floating errors like 0.0001 which we should ignore
630
+ // We should scroll the container if there's a hidden part of the nested list.
631
+ // We might have floating errors like 0.0001 which we should ignore.
575
632
  return (
576
- (hiddenArea.top > 0.1 && y <= thresholdAreaParent.top) ||
577
- (hiddenArea.bottom > 0.1 && y >= thresholdAreaParent.bottom)
633
+ (nestedListHiddenArea.top > 0.01 && y <= containerThresholdArea.top) ||
634
+ (nestedListHiddenArea.bottom > 0.01 &&
635
+ y >= containerThresholdArea.bottom)
578
636
  );
579
637
  },
580
- [calculateHiddenArea, calculateThresholdAreaParent],
638
+ [computeHiddenArea, computeContainerThresholdArea],
581
639
  );
582
640
 
583
- const scrollDirection = useCallback(
584
- (y: number) => {
585
- 'worklet';
586
- const hiddenArea = calculateHiddenArea();
641
+ const getRelativeContainerY = useCallback(() => {
642
+ 'worklet';
587
643
 
588
- if (shouldScrollParent(y)) {
589
- const thresholdAreaParent = calculateThresholdAreaParent(hiddenArea);
590
- if (y <= thresholdAreaParent.top) {
591
- return -1;
592
- }
644
+ return (
645
+ currentY.value +
646
+ nestedFlatListPositionY.value -
647
+ scrollViewDragInitialScrollOffsetY.value
648
+ );
649
+ }, [currentY, nestedFlatListPositionY, scrollViewDragInitialScrollOffsetY]);
593
650
 
594
- if (y >= thresholdAreaParent.bottom) {
595
- return 1;
596
- }
651
+ const getRelativeListY = useCallback(() => {
652
+ 'worklet';
597
653
 
598
- return 0;
599
- } else if (nestedScrollable) {
600
- const thresholdArea = calculateThresholdArea(hiddenArea);
601
- if (y <= thresholdArea.top) {
602
- return -1;
603
- }
654
+ return currentY.value + scrollViewDragScrollTranslationY.value;
655
+ }, [currentY, scrollViewDragScrollTranslationY]);
604
656
 
605
- if (y >= thresholdArea.bottom) {
606
- return 1;
607
- }
657
+ const scrollDirection = useCallback(() => {
658
+ 'worklet';
659
+
660
+ const relativeContainerY = getRelativeContainerY();
661
+ if (shouldScrollContainer(relativeContainerY)) {
662
+ const containerThresholdArea = computeContainerThresholdArea();
663
+ if (relativeContainerY <= containerThresholdArea.top) {
664
+ return -1;
608
665
  }
609
666
 
610
- return 0;
611
- },
612
- [
613
- nestedScrollable,
614
- shouldScrollParent,
615
- calculateHiddenArea,
616
- calculateThresholdArea,
617
- calculateThresholdAreaParent,
618
- ],
619
- );
667
+ if (relativeContainerY >= containerThresholdArea.bottom) {
668
+ return 1;
669
+ }
670
+ } else if (scrollable) {
671
+ const relativeListY = getRelativeListY();
672
+ const thresholdArea = computeThresholdArea();
673
+
674
+ if (relativeListY <= thresholdArea.top) {
675
+ return -1;
676
+ }
677
+
678
+ if (relativeListY >= thresholdArea.bottom) {
679
+ return 1;
680
+ }
681
+ }
682
+
683
+ return 0;
684
+ }, [
685
+ shouldScrollContainer,
686
+ computeThresholdArea,
687
+ computeContainerThresholdArea,
688
+ getRelativeContainerY,
689
+ getRelativeListY,
690
+ scrollable,
691
+ ]);
620
692
 
621
693
  useAnimatedReaction(
622
- () => currentY.value + scrollViewDragScrollTranslationY.value,
623
- y => {
694
+ () => currentY.value,
695
+ () => {
624
696
  if (
625
697
  state.value === ReorderableListState.DRAGGED ||
626
698
  state.value === ReorderableListState.AUTOSCROLL
627
699
  ) {
628
- setCurrentIndex(y);
700
+ setCurrentIndex();
629
701
 
630
702
  // Trigger autoscroll when:
631
703
  // 1. Within the threshold area (top or bottom of list)
632
704
  // 2. Have dragged in the same direction as the scroll
633
705
  // 3. Not already in autoscroll mode
634
- if (dragDirection.value === scrollDirection(y)) {
706
+ if (dragDirection.value === scrollDirection()) {
635
707
  // When the first two conditions are met and it's already in autoscroll mode, we let it continue (no-op)
636
708
  if (state.value !== ReorderableListState.AUTOSCROLL) {
637
709
  state.value = ReorderableListState.AUTOSCROLL;
@@ -652,9 +724,8 @@ export const useReorderableListCore = <T>({
652
724
  autoscrollTrigger.value !== lastAutoscrollTrigger.value &&
653
725
  state.value === ReorderableListState.AUTOSCROLL
654
726
  ) {
655
- let y = currentY.value + scrollViewDragScrollTranslationY.value;
656
727
  const autoscrollIncrement =
657
- scrollDirection(y) *
728
+ dragDirection.value *
658
729
  AUTOSCROLL_CONFIG.increment *
659
730
  autoscrollSpeedScale;
660
731
 
@@ -663,7 +734,13 @@ export const useReorderableListCore = <T>({
663
734
  let listRef =
664
735
  flatListRef as unknown as AnimatedRef<Animated.ScrollView>;
665
736
 
666
- if (shouldScrollParent(y) && scrollViewScrollOffsetY) {
737
+ // Checking on every autoscroll whether to scroll the container,
738
+ // this allows to smoothly pass the scroll from the container to the nested list
739
+ // without any gesture input.
740
+ if (
741
+ scrollViewScrollOffsetY &&
742
+ shouldScrollContainer(getRelativeContainerY())
743
+ ) {
667
744
  scrollOffset = scrollViewScrollOffsetY.value;
668
745
  listRef =
669
746
  scrollViewContainerRef as unknown as AnimatedRef<Animated.ScrollView>;
@@ -674,7 +751,7 @@ export const useReorderableListCore = <T>({
674
751
 
675
752
  // when autoscrolling user may not be moving his finger so we need
676
753
  // to update the current position of the dragged item here
677
- setCurrentIndex(y);
754
+ setCurrentIndex();
678
755
  }
679
756
  },
680
757
  );
@@ -705,14 +782,13 @@ export const useReorderableListCore = <T>({
705
782
  }
706
783
  });
707
784
 
708
- // parent scroll handler
785
+ // container scroll handler
709
786
  useAnimatedReaction(
710
787
  () => scrollViewScrollOffsetY?.value,
711
788
  value => {
712
789
  if (value && scrollViewScrollEnabled) {
713
790
  // checking if the list is not scrollable instead of the scrolling state
714
- // fixes a bug on iOS where the item is shifted after autoscrolling and then
715
- // moving await from autoscroll area
791
+ // fixes a bug on iOS where the item is shifted after autoscrolling and moving away from the area
716
792
  if (!scrollViewScrollEnabled.value) {
717
793
  scrollViewDragScrollTranslationY.value =
718
794
  value - scrollViewDragInitialScrollOffsetY.value;
@@ -747,13 +823,11 @@ export const useReorderableListCore = <T>({
747
823
  }
748
824
 
749
825
  dragInitialScrollOffsetY.value = flatListScrollOffsetY.value;
750
- scrollViewDragInitialScrollOffsetY.value = scrollViewScrollOffsetY
751
- ? scrollViewScrollOffsetY.value
752
- : 0;
826
+ scrollViewDragInitialScrollOffsetY.value =
827
+ scrollViewScrollOffsetY?.value || 0;
753
828
 
754
829
  draggedHeight.value = itemHeight.value[index];
755
830
  draggedIndex.value = index;
756
- previousIndex.value = -1;
757
831
  currentIndex.value = index;
758
832
  state.value = ReorderableListState.DRAGGED;
759
833
 
@@ -772,7 +846,6 @@ export const useReorderableListCore = <T>({
772
846
  scrollViewDragInitialScrollOffsetY,
773
847
  setScrollEnabled,
774
848
  currentIndex,
775
- previousIndex,
776
849
  draggedHeight,
777
850
  draggedIndex,
778
851
  state,
@@ -785,12 +858,32 @@ export const useReorderableListCore = <T>({
785
858
 
786
859
  const handleFlatListLayout = useCallback(
787
860
  (e: LayoutChangeEvent) => {
788
- nestedFlatListPositionY.value = e.nativeEvent.layout.y;
789
861
  flatListHeightY.value = e.nativeEvent.layout.height;
790
862
 
863
+ // If nested in a scroll container.
864
+ if (scrollViewScrollOffsetY) {
865
+ // Timeout fixes a bug where measure returns height 0.
866
+ setTimeout(() => {
867
+ runOnUI(() => {
868
+ const measurement = measure(flatListRef);
869
+ if (!measurement) {
870
+ return;
871
+ }
872
+
873
+ flatListPageY.value = measurement.pageY;
874
+ })();
875
+ }, 100);
876
+ }
877
+
791
878
  onLayout?.(e);
792
879
  },
793
- [nestedFlatListPositionY, flatListHeightY, onLayout],
880
+ [
881
+ flatListRef,
882
+ flatListPageY,
883
+ flatListHeightY,
884
+ scrollViewScrollOffsetY,
885
+ onLayout,
886
+ ],
794
887
  );
795
888
 
796
889
  const handleRef = (value: FlatList<T>) => {