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,