Skip to content

Commit 3617f86

Browse files
authored
fix: Buggy grid swapping in some edge cases (#414)
## Description This PR fixes some buggy swapping behavior for both `insert` and `swap` sortable grid ordering strategies. ## Example recordings ### `insert` | Before | After | |-|-| | <video src="https://github.com/user-attachments/assets/1e4aa54a-2ea0-4171-bf5d-7e5adaa241a1" />| <video src="https://github.com/user-attachments/assets/38143114-00de-4f87-a3b4-71998829d251" /> | ### `swap` | Before | After | |-|-| | <video src="https://github.com/user-attachments/assets/bc60f215-1b8d-4ff7-8f05-79a70dfa62cd" /> | <video src="https://github.com/user-attachments/assets/e1396043-30ca-4f8b-b340-efa84f740fd6" /> |
1 parent e51ca61 commit 3617f86

5 files changed

Lines changed: 31 additions & 37 deletions

File tree

example/app/src/examples/SortableGrid/PlaygroundExample.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@ import Sortable from 'react-native-sortables';
66
import { ScrollScreen } from '@/components';
77
import { colors, radius, sizes, spacing, text } from '@/theme';
88

9-
const DATA = Array.from({ length: 12 }, (_, index) => `Item ${index + 1}`);
9+
const DATA = [
10+
{ height: 50, id: 1 },
11+
{ height: 50, id: 2 },
12+
{ height: 50, id: 3 },
13+
{ height: 300, id: 4 },
14+
{ height: 50, id: 5 }
15+
];
1016

1117
export default function PlaygroundExample() {
12-
const renderItem = useCallback<SortableGridRenderItem<string>>(
18+
const renderItem = useCallback<SortableGridRenderItem<(typeof DATA)[number]>>(
1319
({ item }) => (
14-
<View style={styles.card}>
15-
<Text style={styles.text}>{item}</Text>
20+
<View style={[styles.card, { height: item.height }]}>
21+
<Text style={styles.text}>{item.id}</Text>
1622
</View>
1723
),
1824
[]
@@ -26,6 +32,8 @@ export default function PlaygroundExample() {
2632
data={DATA}
2733
renderItem={renderItem}
2834
rowGap={10}
35+
strategy='swap'
36+
debug
2937
/>
3038
</ScrollScreen>
3139
);

packages/react-native-sortables/src/providers/layout/grid/updates/common.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const createGridStrategy =
7373
? crossAxisOffsets[index + 1]! -
7474
crossAxisOffsets[index] -
7575
crossGap.value
76-
: 0;
76+
: null;
7777

7878
// CROSS AXIS BOUNDS
7979
// Before bound
@@ -118,20 +118,14 @@ export const createGridStrategy =
118118
if (!nextCrossAxisOffset) {
119119
break;
120120
}
121-
crossAfterOffset = nextCrossAxisOffset - crossGap.value;
121+
crossAfterOffset =
122+
(crossAxisOffsets[crossIndex] ?? 0) + crossCurrentSize;
122123
const crossAfterSize = getItemCrossSize(crossIndex + 1);
124+
const swapOffset = (crossAfterOffset + nextCrossAxisOffset) / 2;
125+
const additionalAfterOffset = getAdditionalSwapOffset(crossAfterSize);
126+
crossAfterBound = swapOffset + additionalAfterOffset;
123127
if (crossAfterSize) {
124-
const swapOffset =
125-
((crossAxisOffsets[crossIndex] ?? 0) +
126-
nextCrossAxisOffset +
127-
crossCurrentSize) /
128-
2;
129-
const additionalAfterOffset = getAdditionalSwapOffset(crossAfterSize);
130-
crossAfterBound = swapOffset + additionalAfterOffset;
131128
crossCurrentSize = crossAfterSize;
132-
} else {
133-
crossAfterBound =
134-
(crossAxisOffsets[crossIndex] ?? 0) + crossCurrentSize;
135129
}
136130
} while (
137131
crossAfterBound < crossContainerSize.value &&
@@ -231,10 +225,8 @@ export const createGridStrategy =
231225
0,
232226
Math.min(crossIndex, Math.floor((itemsCount - 1) / numGroups))
233227
);
234-
const newIndex = Math.max(
235-
0,
236-
Math.min(limitedCrossIndex * numGroups + mainIndex, itemsCount - 1)
237-
);
228+
const newIndex = Math.max(0, limitedCrossIndex * numGroups + mainIndex);
229+
238230
if (
239231
newIndex === activeIndex ||
240232
fixedItemKeys?.value[idxToKey[newIndex]!]

packages/react-native-sortables/src/providers/layout/grid/updates/swap.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { useMutableValue } from '../../../../integrations/reanimated';
55
import { areArraysDifferent, reorderSwap } from '../../../../utils';
66
import { useCommonValuesContext } from '../../../shared';
77
import { useGridLayoutContext } from '../GridLayoutProvider';
8-
import { getMainIndex } from '../utils';
98
import { createGridStrategy } from './common';
109

1110
/**
@@ -19,7 +18,7 @@ import { createGridStrategy } from './common';
1918
* |12 |13 |14 |15 | |12 |14 |15 | <-- in the last row we can have anything
2019
*
2120
* It removes the active item and shifts items in the same column
22-
* to the top. Items in the last row are shifted to the left to fill
21+
* to the top. Remaining items are shifted to the left to fill
2322
* the blank space.
2423
*
2524
* The same applies to the horizontal grid but with direction changes.
@@ -43,22 +42,12 @@ function useInactiveIndexToKey() {
4342
}
4443

4544
const othersArray = [...idxToKey];
45+
let i = excludedIndex;
4646

47-
for (
48-
let i = excludedIndex;
49-
i + numGroups < othersArray.length;
50-
i += numGroups
51-
) {
47+
for (; i + numGroups < othersArray.length; i += numGroups) {
5248
othersArray[i] = othersArray[i + numGroups]!;
5349
}
54-
55-
const activeColumnIndex = getMainIndex(excludedIndex, numGroups);
56-
const lastRowIndex = Math.floor((othersArray.length - 1) / numGroups);
57-
for (
58-
let i = lastRowIndex * numGroups + activeColumnIndex;
59-
i < othersArray.length;
60-
i++
61-
) {
50+
for (; i < othersArray.length; i++) {
6251
othersArray[i] = othersArray[i + 1]!;
6352
}
6453
othersArray.pop();
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EXTRA_SWAP_OFFSET } from '../../constants';
2+
import type { Maybe } from '../../helperTypes';
23

3-
export const getAdditionalSwapOffset = (size: number) => {
4+
export const getAdditionalSwapOffset = (size: Maybe<number>) => {
45
'worklet';
5-
return Math.min(EXTRA_SWAP_OFFSET, size / 2);
6+
return size ? Math.min(EXTRA_SWAP_OFFSET, size / 2) : EXTRA_SWAP_OFFSET;
67
};

packages/react-native-sortables/src/utils/layout.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const reorderInsert = (
2727
fixedItemKeys: Record<string, boolean> | undefined
2828
) => {
2929
'worklet';
30+
toIndex = Math.min(toIndex, indexToKey.length - 1);
3031
const direction = toIndex > fromIndex ? -1 : 1;
3132
const fromKey = indexToKey[fromIndex]!;
3233
const op = direction < 0 ? lt : gt;
@@ -58,6 +59,9 @@ export const reorderSwap = (
5859
toIndex: number
5960
) => {
6061
'worklet';
62+
if (toIndex > indexToKey.length - 1) {
63+
return indexToKey;
64+
}
6165
const result = [...indexToKey];
6266
[result[fromIndex], result[toIndex]] = [result[toIndex]!, result[fromIndex]!];
6367
return result;

0 commit comments

Comments
 (0)