From 438dfe91ed7f8221080a08b21a96b0554b565ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Sat, 27 Jun 2026 22:37:20 +0200 Subject: [PATCH] Add reorderOnDrag prop to delay sorting until the item is dropped When reorderOnDrag is false, items don't reorder during the drag. The prospective order is computed while dragging and committed in handleDragEnd before onDragEnd/onActiveItemDropped fire, so the drag-end callbacks report the final order instead of the pre-drop one. --- .../src/components/SortableFlex.tsx | 6 ++- .../src/components/SortableGrid.tsx | 6 ++- .../src/constants/props.ts | 1 + .../src/providers/shared/DragProvider.ts | 39 ++++++++++++++++--- .../providers/shared/hooks/useOrderUpdater.ts | 20 ++++++++-- .../src/types/props/shared.ts | 4 ++ .../src/types/providers/shared.ts | 1 + 7 files changed, 67 insertions(+), 10 deletions(-) diff --git a/packages/react-native-sortables/src/components/SortableFlex.tsx b/packages/react-native-sortables/src/components/SortableFlex.tsx index 407eb982..78e49fab 100644 --- a/packages/react-native-sortables/src/components/SortableFlex.tsx +++ b/packages/react-native-sortables/src/components/SortableFlex.tsx @@ -100,6 +100,7 @@ const SortableFlexInner = memo(function SortableFlexInner({ paddingRight, paddingTop, paddingVertical, + reorderOnDrag, rowGap, showDropIndicator, strategy, @@ -158,6 +159,7 @@ const SortableFlexInner = memo(function SortableFlexInner({ itemEntering={itemEntering} itemExiting={itemExiting} overflow={overflow} + reorderOnDrag={reorderOnDrag} showDropIndicator={showDropIndicator} strategy={strategy} styleProps={styleProps} @@ -175,6 +177,7 @@ type SortableFlexComponentProps = Pick< | 'itemEntering' | 'itemExiting' | 'overflow' + | 'reorderOnDrag' | 'showDropIndicator' | 'strategy' > & { @@ -182,6 +185,7 @@ type SortableFlexComponentProps = Pick< }; function SortableFlexComponent({ + reorderOnDrag, strategy, styleProps, ...rest @@ -199,7 +203,7 @@ function SortableFlexComponent({ width } = styleProps; - useOrderUpdater(strategy, FLEX_STRATEGIES); + useOrderUpdater(strategy, FLEX_STRATEGIES, reorderOnDrag); const baseContainerStyle: ViewStyle = { ...styleProps, diff --git a/packages/react-native-sortables/src/components/SortableGrid.tsx b/packages/react-native-sortables/src/components/SortableGrid.tsx index b8a494cb..6023df31 100644 --- a/packages/react-native-sortables/src/components/SortableGrid.tsx +++ b/packages/react-native-sortables/src/components/SortableGrid.tsx @@ -109,6 +109,7 @@ const SortableGridInner = typedMemo(function SortableGridInner({ itemEntering, itemExiting, overflow, + reorderOnDrag, rowGap: _rowGap, rowHeight, showDropIndicator, @@ -156,6 +157,7 @@ const SortableGridInner = typedMemo(function SortableGridInner({ itemEntering={itemEntering} itemExiting={itemExiting} overflow={overflow} + reorderOnDrag={reorderOnDrag} rowGap={rowGap} rowHeight={rowHeight} showDropIndicator={showDropIndicator} @@ -176,6 +178,7 @@ type SortableGridComponentProps = Pick< | 'itemEntering' | 'itemExiting' | 'overflow' + | 'reorderOnDrag' | 'rowHeight' | 'showDropIndicator' | 'strategy' @@ -188,6 +191,7 @@ function SortableGridComponent({ columnGap, groups, isVertical, + reorderOnDrag, rowGap, rowHeight, strategy, @@ -199,7 +203,7 @@ function SortableGridComponent({ const isFirstRenderRef = useRef(true); - useOrderUpdater(strategy, GRID_STRATEGIES); + useOrderUpdater(strategy, GRID_STRATEGIES, reorderOnDrag); useLayoutEffect(() => { if (isFirstRenderRef.current) { diff --git a/packages/react-native-sortables/src/constants/props.ts b/packages/react-native-sortables/src/constants/props.ts index f779d262..6b5993ee 100644 --- a/packages/react-native-sortables/src/constants/props.ts +++ b/packages/react-native-sortables/src/constants/props.ts @@ -57,6 +57,7 @@ export const DEFAULT_SHARED_PROPS = { onOrderChange: undefined, overDrag: 'both', overflow: 'visible', + reorderOnDrag: true, reorderTriggerOrigin: 'center', scrollableRef: undefined, showDropIndicator: false, diff --git a/packages/react-native-sortables/src/providers/shared/DragProvider.ts b/packages/react-native-sortables/src/providers/shared/DragProvider.ts index a8c1702e..044d5b70 100644 --- a/packages/react-native-sortables/src/providers/shared/DragProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/DragProvider.ts @@ -131,6 +131,9 @@ const { DragProvider, useDragContext } = createProvider('Drag')< const currentTouch = useMutableValue(null); // Used to trigger order change of items when the active item is dragged around const triggerOriginPosition = useMutableValue(null); + // Order computed during a `reorderOnDrag={false}` drag. It isn't applied while + // dragging - it's committed in handleDragEnd before the drag-end callbacks fire. + const pendingDropOrder = useMutableValue | null>(null); // ACTIVE ITEM POSITION UPDATER useAnimatedReaction( @@ -526,7 +529,30 @@ const { DragProvider, useDragContext } = createProvider('Drag')< } const fromIndex = ctx.dragStartIndex; - const toIndex = keyToIndex.value[key]!; + + // Commit a deferred (reorderOnDrag=false) reorder synchronously here so the + // drag-end callbacks below report the final order rather than the pre-drop + // one. With reorderOnDrag=true pendingDropOrder stays null and this is a no-op. + let dropIndexToKey = indexToKey.value; + let dropKeyToIndex = keyToIndex.value; + const newOrder = pendingDropOrder.value; + if (newOrder) { + pendingDropOrder.value = null; + indexToKey.value = newOrder; + dropIndexToKey = newOrder; + dropKeyToIndex = getKeyToIndex(newOrder); + const reorderedIndex = dropKeyToIndex[key]!; + if (reorderedIndex !== fromIndex) { + onOrderChange({ + fromIndex, + indexToKey: newOrder, + key, + keyToIndex: dropKeyToIndex, + toIndex: reorderedIndex + }); + } + } + const toIndex = dropKeyToIndex[key]!; prevActiveItemKey.value = activeItemKey.value; ctx.dragStartItemTouchOffset = null; @@ -554,9 +580,9 @@ const { DragProvider, useDragContext } = createProvider('Drag')< stableOnDragEnd({ fromIndex, - indexToKey: indexToKey.value, + indexToKey: dropIndexToKey, key, - keyToIndex: keyToIndex.value, + keyToIndex: dropKeyToIndex, toIndex }); @@ -565,9 +591,9 @@ const { DragProvider, useDragContext } = createProvider('Drag')< updateLayer?.(LayerState.IDLE); onActiveItemDropped({ fromIndex, - indexToKey: indexToKey.value, + indexToKey: dropIndexToKey, key, - keyToIndex: keyToIndex.value, + keyToIndex: dropKeyToIndex, toIndex }); }, dropDuration); @@ -590,6 +616,8 @@ const { DragProvider, useDragContext } = createProvider('Drag')< keyToIndex, multiZoneActiveItemDimensions, onActiveItemDropped, + onOrderChange, + pendingDropOrder, stableOnDragEnd, touchPosition, updateLayer, @@ -625,6 +653,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< handleOrderChange, handleTouchesMove, handleTouchStart, + pendingDropOrder, triggerOriginPosition } }; diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useOrderUpdater.ts b/packages/react-native-sortables/src/providers/shared/hooks/useOrderUpdater.ts index 57a11eb1..985445f0 100644 --- a/packages/react-native-sortables/src/providers/shared/hooks/useOrderUpdater.ts +++ b/packages/react-native-sortables/src/providers/shared/hooks/useOrderUpdater.ts @@ -8,7 +8,11 @@ import { useDragContext } from '../DragProvider'; export default function useOrderUpdater< P extends PredefinedStrategies = PredefinedStrategies ->(strategy: keyof P | SortStrategyFactory, predefinedStrategies: P) { +>( + strategy: keyof P | SortStrategyFactory, + predefinedStrategies: P, + reorderOnDrag: boolean +) { const useStrategy = typeof strategy === 'string' ? predefinedStrategies[strategy] : strategy; @@ -18,7 +22,8 @@ export default function useOrderUpdater< const { activeItemDimensions, activeItemKey, keyToIndex } = useCommonValuesContext(); - const { handleOrderChange, triggerOriginPosition } = useDragContext(); + const { handleOrderChange, pendingDropOrder, triggerOriginPosition } = + useDragContext(); const debugContext = useDebugContext(); const debugCross = debugContext?.useDebugCross(); @@ -53,13 +58,22 @@ export default function useOrderUpdater< position }); - if (newOrder) { + if (!newOrder) { + return; + } + + if (reorderOnDrag) { handleOrderChange( activeKey, activeIndex, newOrder.indexOf(activeKey), newOrder ); + } else { + // Keep computing the prospective order during the drag but don't apply + // it yet. It's committed in handleDragEnd right before onDragEnd fires, + // so the drag-end callbacks report the final order (see DragProvider). + pendingDropOrder.value = newOrder; } } ); diff --git a/packages/react-native-sortables/src/types/props/shared.ts b/packages/react-native-sortables/src/types/props/shared.ts index cfbd78f1..a3598eeb 100644 --- a/packages/react-native-sortables/src/types/props/shared.ts +++ b/packages/react-native-sortables/src/types/props/shared.ts @@ -329,6 +329,10 @@ export type SharedProps = Simplify< * @default 'visible' */ overflow: Overflow; + /** Whether to reorder items during drag gesture or after the active item is dropped + * @default true + */ + reorderOnDrag: boolean; /** Enables debug mode to show additional visual helpers and console logs. * @note This only works in development builds and has no effect in production. * @default false diff --git a/packages/react-native-sortables/src/types/providers/shared.ts b/packages/react-native-sortables/src/types/providers/shared.ts index 4121405d..04455003 100644 --- a/packages/react-native-sortables/src/types/providers/shared.ts +++ b/packages/react-native-sortables/src/types/providers/shared.ts @@ -123,6 +123,7 @@ export type AutoScrollContextType = { export type DragContextType = { triggerOriginPosition: SharedValue; + pendingDropOrder: SharedValue | null>; handleTouchStart: ( e: GestureTouchEvent, key: string,