react-native-reorderable-list 0.11.0 → 0.13.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 (22) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/components/ReorderableListCell.js +2 -2
  3. package/lib/commonjs/components/ReorderableListCell.js.map +1 -1
  4. package/lib/commonjs/components/ReorderableListCore/ReorderableListCore.js +2 -2
  5. package/lib/commonjs/components/ReorderableListCore/ReorderableListCore.js.map +1 -1
  6. package/lib/commonjs/components/ReorderableListCore/useReorderableListCore.js +122 -63
  7. package/lib/commonjs/components/ReorderableListCore/useReorderableListCore.js.map +1 -1
  8. package/lib/module/components/ReorderableListCell.js +2 -2
  9. package/lib/module/components/ReorderableListCell.js.map +1 -1
  10. package/lib/module/components/ReorderableListCore/ReorderableListCore.js +2 -2
  11. package/lib/module/components/ReorderableListCore/ReorderableListCore.js.map +1 -1
  12. package/lib/module/components/ReorderableListCore/useReorderableListCore.js +122 -63
  13. package/lib/module/components/ReorderableListCore/useReorderableListCore.js.map +1 -1
  14. package/lib/typescript/components/ReorderableListCore/useReorderableListCore.d.ts +2 -2
  15. package/lib/typescript/components/ReorderableListCore/useReorderableListCore.d.ts.map +1 -1
  16. package/lib/typescript/types/props.d.ts +4 -3
  17. package/lib/typescript/types/props.d.ts.map +1 -1
  18. package/package.json +1 -1
  19. package/src/components/ReorderableListCell.tsx +3 -3
  20. package/src/components/ReorderableListCore/ReorderableListCore.tsx +2 -2
  21. package/src/components/ReorderableListCore/useReorderableListCore.ts +196 -106
  22. package/src/types/props.ts +4 -3
@@ -6,7 +6,12 @@ import {
6
6
  unstable_batchedUpdates,
7
7
  } from 'react-native';
8
8
 
9
- import {Gesture, State} from 'react-native-gesture-handler';
9
+ import {
10
+ Gesture,
11
+ GestureUpdateEvent,
12
+ PanGestureHandlerEventPayload,
13
+ State,
14
+ } from 'react-native-gesture-handler';
10
15
  import Animated, {
11
16
  AnimatedRef,
12
17
  Easing,
@@ -47,8 +52,8 @@ interface UseReorderableListCoreArgs<T> {
47
52
  autoscrollThresholdOffset: {top?: number; bottom?: number} | undefined;
48
53
  autoscrollSpeedScale: number;
49
54
  autoscrollDelay: number;
55
+ autoscrollActivationDelta: number;
50
56
  animationDuration: number;
51
- dragReorderThreshold: number;
52
57
  onReorder: (event: ReorderableListReorderEvent) => void;
53
58
  onDragStart?: (event: ReorderableListDragStartEvent) => void;
54
59
  onDragEnd?: (event: ReorderableListDragEndEvent) => void;
@@ -73,8 +78,8 @@ export const useReorderableListCore = <T>({
73
78
  autoscrollThresholdOffset,
74
79
  autoscrollSpeedScale,
75
80
  autoscrollDelay,
81
+ autoscrollActivationDelta,
76
82
  animationDuration,
77
- dragReorderThreshold,
78
83
  onReorder,
79
84
  onDragStart,
80
85
  onDragEnd,
@@ -98,22 +103,25 @@ export const useReorderableListCore = <T>({
98
103
  const gestureState = useSharedValue<State>(State.UNDETERMINED);
99
104
  const currentY = useSharedValue(0);
100
105
  const currentTranslationY = useSharedValue(0);
106
+ const currentItemDragCenterY = useSharedValue<number | null>(null);
107
+ const startItemDragCenterY = useSharedValue<number>(0);
101
108
  const flatListScrollOffsetY = useSharedValue(0);
102
109
  const flatListHeightY = useSharedValue(0);
103
110
  const nestedFlatListPositionY = useSharedValue(0);
111
+ // The scroll y translation of the list since drag start
104
112
  const dragScrollTranslationY = useSharedValue(0);
113
+ // The initial scroll offset y of the list on drag start
105
114
  const dragInitialScrollOffsetY = useSharedValue(0);
115
+ // The scroll y translation of the ScrollViewContainer since drag start
106
116
  const scrollViewDragScrollTranslationY = useSharedValue(0);
117
+ // The initial scroll offset y of the ScrollViewContainer on drag start
107
118
  const scrollViewDragInitialScrollOffsetY = useSharedValue(0);
108
119
  const draggedHeight = useSharedValue(0);
109
120
  const itemOffset = useSharedValue<number[]>([]);
110
121
  const itemHeight = useSharedValue<number[]>([]);
111
122
  const autoscrollTrigger = useSharedValue(-1);
112
123
  const lastAutoscrollTrigger = useSharedValue(-1);
113
- const previousY = useSharedValue(0);
114
124
  const dragY = useSharedValue(0);
115
- const previousDirection = useSharedValue(0);
116
- const previousIndex = useSharedValue(-1);
117
125
  const currentIndex = useSharedValue(-1);
118
126
  const draggedIndex = useSharedValue(-1);
119
127
  const state = useSharedValue<ReorderableListState>(ReorderableListState.IDLE);
@@ -124,10 +132,14 @@ export const useReorderableListCore = <T>({
124
132
  const duration = useSharedValue(animationDuration);
125
133
  const scaleDefault = useSharedValue(1);
126
134
  const opacityDefault = useSharedValue(1);
135
+ const dragDirection = useSharedValue(0);
136
+ const lastDragDirectionPivot = useSharedValue<number | null>(null);
137
+ const autoscrollDelta = useSharedValue(autoscrollActivationDelta);
127
138
 
128
139
  useEffect(() => {
129
140
  duration.value = animationDuration;
130
- }, [duration, animationDuration]);
141
+ autoscrollDelta.value = autoscrollActivationDelta;
142
+ }, [duration, animationDuration, autoscrollDelta, autoscrollActivationDelta]);
131
143
 
132
144
  const listContextValue = useMemo(
133
145
  () => ({
@@ -160,6 +172,66 @@ export const useReorderableListCore = <T>({
160
172
  ],
161
173
  );
162
174
 
175
+ /**
176
+ * Decides the intended drag direction of the user.
177
+ * This is used to to determine if the user intends to autoscroll
178
+ * when within the threshold area.
179
+ *
180
+ * @param e - The payload of the pan gesture update event.
181
+ */
182
+ const setDragDirection = useCallback(
183
+ (e: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
184
+ 'worklet';
185
+
186
+ const direction = e.velocityY > 0 ? 1 : -1;
187
+ if (direction !== dragDirection.value) {
188
+ if (lastDragDirectionPivot.value === null) {
189
+ lastDragDirectionPivot.value = e.absoluteY;
190
+ } else if (
191
+ Math.abs(e.absoluteY - lastDragDirectionPivot.value) >=
192
+ autoscrollDelta.value
193
+ ) {
194
+ dragDirection.value = direction;
195
+ lastDragDirectionPivot.value = e.absoluteY;
196
+ }
197
+ }
198
+ },
199
+ [dragDirection, lastDragDirectionPivot, autoscrollDelta],
200
+ );
201
+
202
+ const setCurrentItemDragCenterY = useCallback(
203
+ (e: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
204
+ 'worklet';
205
+
206
+ if (currentItemDragCenterY.value === null) {
207
+ if (currentIndex.value >= 0) {
208
+ const itemCenter = itemHeight.value[currentIndex.value] * 0.5;
209
+ // the y coordinate of the item relative to the list
210
+ const itemY =
211
+ itemOffset.value[currentIndex.value] -
212
+ (flatListScrollOffsetY.value +
213
+ scrollViewDragScrollTranslationY.value);
214
+
215
+ const value = itemY + itemCenter + e.translationY;
216
+ startItemDragCenterY.value = value;
217
+ currentItemDragCenterY.value = value;
218
+ }
219
+ } else {
220
+ currentItemDragCenterY.value =
221
+ startItemDragCenterY.value + e.translationY;
222
+ }
223
+ },
224
+ [
225
+ currentItemDragCenterY,
226
+ currentIndex,
227
+ startItemDragCenterY,
228
+ itemOffset,
229
+ itemHeight,
230
+ flatListScrollOffsetY,
231
+ scrollViewDragScrollTranslationY,
232
+ ],
233
+ );
234
+
163
235
  const panGestureHandler = useMemo(
164
236
  () =>
165
237
  Gesture.Pan()
@@ -174,7 +246,13 @@ export const useReorderableListCore = <T>({
174
246
  }
175
247
  })
176
248
  .onUpdate(e => {
249
+ if (state.value === ReorderableListState.DRAGGED) {
250
+ setDragDirection(e);
251
+ }
252
+
177
253
  if (state.value !== ReorderableListState.RELEASED) {
254
+ setCurrentItemDragCenterY(e);
255
+
178
256
  currentY.value = startY.value + e.translationY;
179
257
  currentTranslationY.value = e.translationY;
180
258
  dragY.value =
@@ -187,14 +265,16 @@ export const useReorderableListCore = <T>({
187
265
  .onEnd(e => (gestureState.value = e.state))
188
266
  .onFinalize(e => (gestureState.value = e.state)),
189
267
  [
190
- currentTranslationY,
268
+ state,
269
+ startY,
191
270
  currentY,
271
+ currentTranslationY,
272
+ dragY,
273
+ gestureState,
192
274
  dragScrollTranslationY,
193
275
  scrollViewDragScrollTranslationY,
194
- gestureState,
195
- dragY,
196
- startY,
197
- state,
276
+ setDragDirection,
277
+ setCurrentItemDragCenterY,
198
278
  ],
199
279
  );
200
280
 
@@ -252,12 +332,18 @@ export const useReorderableListCore = <T>({
252
332
  dragY.value = 0;
253
333
  dragScrollTranslationY.value = 0;
254
334
  scrollViewDragScrollTranslationY.value = 0;
335
+ dragDirection.value = 0;
336
+ lastDragDirectionPivot.value = null;
337
+ currentItemDragCenterY.value = null;
255
338
  }, [
339
+ state,
256
340
  draggedIndex,
257
341
  dragY,
258
342
  dragScrollTranslationY,
259
343
  scrollViewDragScrollTranslationY,
260
- state,
344
+ dragDirection,
345
+ lastDragDirectionPivot,
346
+ currentItemDragCenterY,
261
347
  ]);
262
348
 
263
349
  const resetSharedValuesAfterAnimations = useCallback(() => {
@@ -280,102 +366,106 @@ export const useReorderableListCore = <T>({
280
366
  }
281
367
  };
282
368
 
283
- const getIndexFromY = useCallback(
284
- (y: number) => {
369
+ const recomputeLayout = useCallback(
370
+ (from: number, to: number) => {
285
371
  'worklet';
286
372
 
287
- const relativeY = flatListScrollOffsetY.value + y;
288
- const count = itemOffset.value.length;
373
+ const itemDirection = to > from;
374
+ const index1 = itemDirection ? from : to;
375
+ const index2 = itemDirection ? to : from;
376
+
377
+ const newOffset1 = itemOffset.value[index1];
378
+ const newHeight1 = itemHeight.value[index2];
379
+ const newOffset2 =
380
+ itemOffset.value[index2] +
381
+ itemHeight.value[index2] -
382
+ itemHeight.value[index1];
383
+ const newHeight2 = itemHeight.value[index1];
384
+
385
+ itemOffset.value[index1] = newOffset1;
386
+ itemHeight.value[index1] = newHeight1;
387
+ itemOffset.value[index2] = newOffset2;
388
+ itemHeight.value[index2] = newHeight2;
389
+ },
390
+ [itemOffset, itemHeight],
391
+ );
289
392
 
290
- for (let i = 0; i < count; i++) {
291
- if (currentIndex.value === i) {
292
- continue;
293
- }
393
+ /**
394
+ * Computes a potential new drop container for the current dragged item and evaluates
395
+ * whether the dragged item center is nearer to the center of the current container or the new one.
396
+ *
397
+ * @returns The new index if the center of the dragged item is closer to the center of
398
+ * the new drop container or the current index if closer to the current drop container.
399
+ */
400
+ const computeCurrentIndex = useCallback(() => {
401
+ 'worklet';
294
402
 
295
- const direction = i > currentIndex.value ? 1 : -1;
296
- const threshold = Math.max(0, Math.min(1, dragReorderThreshold));
297
- const height = itemHeight.value[i];
298
- const offset = itemOffset.value[i] + height * threshold * direction;
403
+ if (currentItemDragCenterY.value === null) {
404
+ return currentIndex.value;
405
+ }
299
406
 
300
- if (
301
- (i === 0 && relativeY <= offset) ||
302
- (i === count - 1 && relativeY >= offset + height) ||
303
- (relativeY >= offset && relativeY <= offset + height)
304
- ) {
305
- return {index: i, direction};
306
- }
407
+ // apply scroll offset and scroll container translation
408
+ const relativeDragCenterY =
409
+ flatListScrollOffsetY.value +
410
+ scrollViewDragScrollTranslationY.value +
411
+ currentItemDragCenterY.value;
412
+
413
+ const currentOffset = itemOffset.value[currentIndex.value];
414
+ const currentHeight = itemHeight.value[currentIndex.value];
415
+ const currentCenter = currentOffset + currentHeight * 0.5;
416
+
417
+ const max = itemOffset.value.length;
418
+ const possibleIndex =
419
+ relativeDragCenterY < currentCenter
420
+ ? Math.max(0, currentIndex.value - 1)
421
+ : Math.min(max - 1, currentIndex.value + 1);
422
+
423
+ if (currentIndex.value !== possibleIndex) {
424
+ let possibleOffset = itemOffset.value[possibleIndex];
425
+ if (possibleIndex > currentIndex.value) {
426
+ possibleOffset += itemHeight.value[possibleIndex] - currentHeight;
307
427
  }
308
428
 
309
- return {
310
- index: currentIndex.value,
311
- direction: previousDirection.value,
312
- };
313
- },
314
- [
315
- dragReorderThreshold,
316
- currentIndex,
317
- flatListScrollOffsetY,
318
- previousDirection,
319
- itemOffset,
320
- itemHeight,
321
- ],
322
- );
429
+ const possibleCenter = possibleOffset + currentHeight * 0.5;
430
+ const distanceFromCurrent = Math.abs(relativeDragCenterY - currentCenter);
431
+ const distanceFromPossible = Math.abs(
432
+ relativeDragCenterY - possibleCenter,
433
+ );
323
434
 
324
- const setCurrentIndex = useCallback(
325
- (y: number) => {
326
- 'worklet';
435
+ return distanceFromCurrent <= distanceFromPossible
436
+ ? currentIndex.value
437
+ : possibleIndex;
438
+ }
439
+
440
+ return currentIndex.value;
441
+ }, [
442
+ currentIndex,
443
+ currentItemDragCenterY,
444
+ itemOffset,
445
+ itemHeight,
446
+ flatListScrollOffsetY,
447
+ scrollViewDragScrollTranslationY,
448
+ ]);
327
449
 
328
- const {index: newIndex, direction: newDirection} = getIndexFromY(y);
329
- const delta = Math.abs(previousY.value - y);
450
+ const setCurrentIndex = useCallback(() => {
451
+ 'worklet';
330
452
 
331
- if (
332
- currentIndex.value !== newIndex &&
333
- // if the same two items re-swap index check delta and direction to avoid swap flickering
334
- (previousIndex.value !== newIndex ||
335
- (previousDirection.value !== newDirection && delta >= 5))
336
- ) {
337
- const itemDirection = newIndex > currentIndex.value;
338
- const index1 = itemDirection ? currentIndex.value : newIndex;
339
- const index2 = itemDirection ? newIndex : currentIndex.value;
340
-
341
- const newOffset1 = itemOffset.value[index1];
342
- const newHeight1 = itemHeight.value[index2];
343
- const newOffset2 =
344
- itemOffset.value[index2] +
345
- (itemHeight.value[index2] - itemHeight.value[index1]);
346
- const newHeight2 = itemHeight.value[index1];
347
-
348
- itemOffset.value[index1] = newOffset1;
349
- itemHeight.value[index1] = newHeight1;
350
- itemOffset.value[index2] = newOffset2;
351
- itemHeight.value[index2] = newHeight2;
352
-
353
- previousY.value = y;
354
- previousDirection.value = newDirection;
355
- previousIndex.value = currentIndex.value;
356
- currentIndex.value = newIndex;
357
-
358
- onIndexChange?.({index: newIndex});
359
- }
360
- },
361
- [
362
- currentIndex,
363
- previousIndex,
364
- previousDirection,
365
- previousY,
366
- itemOffset,
367
- itemHeight,
368
- getIndexFromY,
369
- onIndexChange,
370
- ],
371
- );
453
+ const newIndex = computeCurrentIndex();
454
+
455
+ if (currentIndex.value !== newIndex) {
456
+ recomputeLayout(currentIndex.value, newIndex);
457
+ currentIndex.value = newIndex;
458
+
459
+ onIndexChange?.({index: newIndex});
460
+ }
461
+ }, [currentIndex, computeCurrentIndex, recomputeLayout, onIndexChange]);
372
462
 
373
463
  const runDefaultDragAnimations = useCallback(
374
464
  (type: 'start' | 'end') => {
375
465
  'worklet';
376
466
 
377
467
  // if no custom scale run default
378
- if (!(cellAnimations && 'transformtra' in cellAnimations)) {
468
+ if (!(cellAnimations && 'transform' in cellAnimations)) {
379
469
  const scaleConfig = SCALE_ANIMATION_CONFIG_DEFAULT[type];
380
470
  scaleDefault.value = withTiming(scaleConfig.toValue, scaleConfig);
381
471
  }
@@ -580,14 +670,18 @@ export const useReorderableListCore = <T>({
580
670
  state.value === ReorderableListState.DRAGGED ||
581
671
  state.value === ReorderableListState.AUTOSCROLL
582
672
  ) {
583
- setCurrentIndex(y);
584
-
585
- if (scrollDirection(y) !== 0) {
673
+ setCurrentIndex();
674
+
675
+ // Trigger autoscroll when:
676
+ // 1. Within the threshold area (top or bottom of list)
677
+ // 2. Have dragged in the same direction as the scroll
678
+ // 3. Not already in autoscroll mode
679
+ if (dragDirection.value === scrollDirection(y)) {
680
+ // When the first two conditions are met and it's already in autoscroll mode, we let it continue (no-op)
586
681
  if (state.value !== ReorderableListState.AUTOSCROLL) {
587
- // trigger autoscroll
682
+ state.value = ReorderableListState.AUTOSCROLL;
588
683
  lastAutoscrollTrigger.value = autoscrollTrigger.value;
589
684
  autoscrollTrigger.value *= -1;
590
- state.value = ReorderableListState.AUTOSCROLL;
591
685
  }
592
686
  } else if (state.value === ReorderableListState.AUTOSCROLL) {
593
687
  state.value = ReorderableListState.DRAGGED;
@@ -625,7 +719,7 @@ export const useReorderableListCore = <T>({
625
719
 
626
720
  // when autoscrolling user may not be moving his finger so we need
627
721
  // to update the current position of the dragged item here
628
- setCurrentIndex(y);
722
+ setCurrentIndex();
629
723
  }
630
724
  },
631
725
  );
@@ -662,8 +756,7 @@ export const useReorderableListCore = <T>({
662
756
  value => {
663
757
  if (value && scrollViewScrollEnabled) {
664
758
  // checking if the list is not scrollable instead of the scrolling state
665
- // fixes a bug on iOS where the item is shifted after autoscrolling and then
666
- // moving await from autoscroll area
759
+ // fixes a bug on iOS where the item is shifted after autoscrolling and moving away from the area
667
760
  if (!scrollViewScrollEnabled.value) {
668
761
  scrollViewDragScrollTranslationY.value =
669
762
  value - scrollViewDragInitialScrollOffsetY.value;
@@ -698,13 +791,11 @@ export const useReorderableListCore = <T>({
698
791
  }
699
792
 
700
793
  dragInitialScrollOffsetY.value = flatListScrollOffsetY.value;
701
- scrollViewDragInitialScrollOffsetY.value = scrollViewScrollOffsetY
702
- ? scrollViewScrollOffsetY.value
703
- : 0;
794
+ scrollViewDragInitialScrollOffsetY.value =
795
+ scrollViewScrollOffsetY?.value || 0;
704
796
 
705
797
  draggedHeight.value = itemHeight.value[index];
706
798
  draggedIndex.value = index;
707
- previousIndex.value = -1;
708
799
  currentIndex.value = index;
709
800
  state.value = ReorderableListState.DRAGGED;
710
801
 
@@ -723,7 +814,6 @@ export const useReorderableListCore = <T>({
723
814
  scrollViewDragInitialScrollOffsetY,
724
815
  setScrollEnabled,
725
816
  currentIndex,
726
- previousIndex,
727
817
  draggedHeight,
728
818
  draggedIndex,
729
819
  state,
@@ -90,10 +90,11 @@ export interface ReorderableListProps<T>
90
90
  */
91
91
  autoscrollDelay?: number;
92
92
  /**
93
- * Specifies the fraction of an item's size at which it will shift when a dragged item crosses over it.
94
- * For example, a value of 0.2 means the item shifts when the dragged item passes 20% of its height (vertical list). Default is `0.2`.
93
+ * Allows configuring the delta for autoscroll activation when dragging an item in the same direction as the autoscroll.
94
+ * This is particularly useful when an item is dragged within the autoscroll area to account for minor unintentional movements.
95
+ * Default: `5`.
95
96
  */
96
- dragReorderThreshold?: number;
97
+ autoscrollActivationDelta?: number;
97
98
  /**
98
99
  * Duration of the animations in milliseconds.
99
100
  * Be aware that users won't be able to drag a new item until the dragged item is released and