From fbd8c555c00d26b59d72843942d036a13f82c975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 27 Aug 2025 01:16:02 +0200 Subject: [PATCH 1/3] Fix obtaining initial position of the dragged item --- .../reanimated/utils/animatedTimeout.ts | 2 +- .../src/providers/shared/DragProvider.ts | 35 ++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/react-native-sortables/src/integrations/reanimated/utils/animatedTimeout.ts b/packages/react-native-sortables/src/integrations/reanimated/utils/animatedTimeout.ts index 5610a0c8..766ddbaa 100644 --- a/packages/react-native-sortables/src/integrations/reanimated/utils/animatedTimeout.ts +++ b/packages/react-native-sortables/src/integrations/reanimated/utils/animatedTimeout.ts @@ -18,7 +18,7 @@ function removeFromPendingTimeouts(id: AnimatedTimeoutID): void { export function setAnimatedTimeout( callback: F, - delay: number + delay = 0 ): AnimatedTimeoutID { let startTimestamp: number; diff --git a/packages/react-native-sortables/src/providers/shared/DragProvider.ts b/packages/react-native-sortables/src/providers/shared/DragProvider.ts index fbb36f3a..372a1104 100644 --- a/packages/react-native-sortables/src/providers/shared/DragProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/DragProvider.ts @@ -289,7 +289,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< activeAnimationProgress.value = 0; activeItemDropped.value = false; - prevActiveItemKey.value = activeItemKey.value; + prevActiveItemKey.value = null; activeItemKey.value = key; activeItemPosition.value = position; activeItemDimensions.value = dimensions; @@ -305,23 +305,14 @@ const { DragProvider, useDragContext } = createProvider('Drag')< updateLayer?.(LayerState.FOCUSED); - let touchedItemPosition = position; - // We need to update the custom handle measurements if the custom handle // is used (touch position is relative to the handle in this case) updateActiveHandleMeasurements?.(key); - if (activeHandleMeasurements?.value) { - const { pageX, pageY } = activeHandleMeasurements.value; - touchedItemPosition = { - x: pageX - containerMeasurements.pageX, - y: pageY - containerMeasurements.pageY - }; - } // Touch position relative to the top-left corner of the sortable // container - const touchX = touchedItemPosition.x + touch.x; - const touchY = touchedItemPosition.y + touch.y; + const touchX = touch.absoluteX - containerMeasurements.pageX; + const touchY = touch.absoluteY - containerMeasurements.pageY; touchPosition.value = { x: touchX, y: touchY }; dragStartTouchPosition.value = touchPosition.value; @@ -338,27 +329,33 @@ const { DragProvider, useDragContext } = createProvider('Drag')< inactiveAnimationProgress.value = hasInactiveAnimation ? animate() : 0; activeAnimationProgress.value = animate(); + activationAnimationProgress.value = 0.01; activationAnimationProgress.value = animate(); haptics.medium(); - stableOnDragStart({ - fromIndex: dragStartIndex.value, - indexToKey: indexToKey.value, - key, - keyToIndex: keyToIndex.value + + // Use timeout to ensure that the callback is called after all animated + // reactions are computed in the library + setAnimatedTimeout(() => { + stableOnDragStart({ + fromIndex: dragStartIndex.value, + indexToKey: indexToKey.value, + key, + keyToIndex: keyToIndex.value + }); }); }, [ activationAnimationDuration, activeAnimationProgress, activeContainerId, - activeHandleMeasurements, activeItemDimensions, activeItemDropped, activationState, activeItemKey, activeItemPosition, containerId, + containerRef, dragStartIndex, dragStartItemTouchOffset, dragStartTouchPosition, @@ -369,7 +366,6 @@ const { DragProvider, useDragContext } = createProvider('Drag')< indexToKey, keyToIndex, multiZoneActiveItemDimensions, - containerRef, prevActiveItemKey, stableOnDragStart, touchPosition, @@ -569,7 +565,6 @@ const { DragProvider, useDragContext } = createProvider('Drag')< }); setAnimatedTimeout(() => { - prevActiveItemKey.value = null; activeItemDropped.value = true; updateLayer?.(LayerState.IDLE); stableOnActiveItemDropped({ From 795c00c5566063ba7dcaa000c1be0aebf3508894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 27 Aug 2025 01:38:55 +0200 Subject: [PATCH 2/3] Fix missing collapsible item layout animation when teleported --- .../shared/DraggableView/ActiveItemPortal.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/react-native-sortables/src/components/shared/DraggableView/ActiveItemPortal.tsx b/packages/react-native-sortables/src/components/shared/DraggableView/ActiveItemPortal.tsx index 0c287c6f..2b339a3b 100644 --- a/packages/react-native-sortables/src/components/shared/DraggableView/ActiveItemPortal.tsx +++ b/packages/react-native-sortables/src/components/shared/DraggableView/ActiveItemPortal.tsx @@ -43,8 +43,8 @@ export default function ActiveItemPortal({ }: ActiveItemPortalProps) { const { isTeleported, measurePortalOutlet, teleport } = usePortalContext() ?? {}; - const updateTimeoutRef = useRef>(null); const teleportEnabled = useMutableValue(false); + const isFirstUpdateRef = useRef(true); const renderTeleportedItemCell = useCallback( () => ( @@ -80,14 +80,12 @@ export default function ActiveItemPortal({ const teleportedItemId = `${commonValuesContext.containerId}-${itemKey}`; const enableTeleport = useStableCallback(() => { + isFirstUpdateRef.current = true; teleport?.(teleportedItemId, renderTeleportedItemCell()); onTeleport(true); }); const disableTeleport = useCallback(() => { - if (updateTimeoutRef.current !== null) { - clearTimeout(updateTimeoutRef.current); - } teleport?.(teleportedItemId, null); onTeleport(false); }, [teleport, teleportedItemId, onTeleport]); @@ -95,23 +93,29 @@ export default function ActiveItemPortal({ useEffect(() => disableTeleport, [disableTeleport]); useEffect(() => { - if (isTeleported?.(teleportedItemId)) { - // We have to delay the update in order not to schedule render via this - // useEffect at the same time as the enableTeleport render is scheduled. - // This may happen if the user changes the item style/content via the - // onDragStart callback (e.g. in collapsible items) when we want to - // render the view unchanged at first and change it a while later to - // properly trigger all layout transitions that the item has. - updateTimeoutRef.current = setTimeout(() => { - teleport?.(teleportedItemId, renderTeleportedItemCell()); - }); + const checkTeleported = () => isTeleported?.(teleportedItemId); + if (!checkTeleported()) return; + + const update = () => + teleport?.(teleportedItemId, renderTeleportedItemCell()); + + if (isFirstUpdateRef.current) { + isFirstUpdateRef.current = false; + // Needed for proper collapsible items behavior + setTimeout(update); + } else { + update(); } }, [isTeleported, renderTeleportedItemCell, teleport, teleportedItemId]); useAnimatedReaction( () => activationAnimationProgress.value, (progress, prevProgress) => { - if (prevProgress && progress > prevProgress && !teleportEnabled.value) { + if ( + prevProgress !== null && + progress > prevProgress && + !teleportEnabled.value + ) { // We have to ensure that the portal outlet ref is measured before the // teleported item is rendered within it because portal outlet position // must be known to calculate the teleported item position From 82fc0f785af77b7eedf0a2c6c3d750f3a11fdd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81opaci=C5=84ski?= Date: Wed, 27 Aug 2025 01:45:48 +0200 Subject: [PATCH 3/3] Update packages/react-native-sortables/src/providers/shared/DragProvider.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../react-native-sortables/src/providers/shared/DragProvider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native-sortables/src/providers/shared/DragProvider.ts b/packages/react-native-sortables/src/providers/shared/DragProvider.ts index 372a1104..9b8afefe 100644 --- a/packages/react-native-sortables/src/providers/shared/DragProvider.ts +++ b/packages/react-native-sortables/src/providers/shared/DragProvider.ts @@ -329,7 +329,6 @@ const { DragProvider, useDragContext } = createProvider('Drag')< inactiveAnimationProgress.value = hasInactiveAnimation ? animate() : 0; activeAnimationProgress.value = animate(); - activationAnimationProgress.value = 0.01; activationAnimationProgress.value = animate(); haptics.medium();