From 8b0e8bcfde61b3ea2050ad3c6df31ba2e7f116b6 Mon Sep 17 00:00:00 2001 From: Albert Hansrisuk Date: Thu, 3 Sep 2026 13:04:39 -0400 Subject: [PATCH] feat: add minScrollOffset to let autoscroll respect non-reorderable header content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The built-in top-edge autoscroll only disables itself once the list's scroll offset reaches absolute 0 (see isScrolledUp/isAtTopEdge). That's correct for a list that's entirely reorderable, but not for one that mixes non-reorderable content (e.g. a ListHeaderComponent) with a reorderable section below it: as soon as the dragged cell's on-screen position sits near the container's top edge — which happens immediately when dragging the reorderable section's own first item, since it can sit right at the top of the viewport — the library scrolls back up into that header content, which was never part of the reorder. `minScrollOffset` (an optional SharedValue, defaulting to 0 — existing behavior for anyone not passing it) lets a consumer tell autoscroll where the real top of the reorderable region is, so it stops there instead of continuing toward absolute 0. --- src/hooks/useAutoScroll.tsx | 12 ++++++++++-- src/types.ts | 5 +++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/hooks/useAutoScroll.tsx b/src/hooks/useAutoScroll.tsx index d3add83e..99484f2a 100644 --- a/src/hooks/useAutoScroll.tsx +++ b/src/hooks/useAutoScroll.tsx @@ -15,8 +15,14 @@ export function useAutoScroll() { const { autoscrollThreshold = DEFAULT_PROPS.autoscrollThreshold, autoscrollSpeed = DEFAULT_PROPS.autoscrollSpeed, + minScrollOffset, } = useProps(); + // Real list top (0) unless the consumer supplied a floor — see + // DraggableFlatListProps.minScrollOffset. + const DEFAULT_MIN_SCROLL_OFFSET = useSharedValue(0); + const minScrollOffsetSV = minScrollOffset || DEFAULT_MIN_SCROLL_OFFSET; + const { scrollOffset, scrollViewSize, @@ -31,7 +37,9 @@ export function useAutoScroll() { }, []); const isScrolledUp = useDerivedValue(() => { - return scrollOffset.value - SCROLL_POSITION_TOLERANCE <= 0; + return ( + scrollOffset.value - SCROLL_POSITION_TOLERANCE <= minScrollOffsetSV.value + ); }, []); const isScrolledDown = useDerivedValue(() => { @@ -103,7 +111,7 @@ export function useAutoScroll() { const speedPct = 1 - distFromEdge / autoscrollThreshold!; const offset = speedPct * autoscrollSpeed; const targetOffset = isAtTopEdge.value - ? Math.max(0, scrollOffset.value - offset) + ? Math.max(minScrollOffsetSV.value, scrollOffset.value - offset) : Math.min( scrollOffset.value + offset, scrollViewSize.value - containerSize.value diff --git a/src/types.ts b/src/types.ts index d6755c8f..a068c8b8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,6 +43,11 @@ export type DraggableFlatListProps = Modify< renderPlaceholder?: RenderPlaceholder; simultaneousHandlers?: React.Ref | React.Ref[]; outerScrollOffset?: Animated.SharedValue; + // Floor for the built-in top-edge autoscroll (see useAutoScroll's isScrolledUp/ + // scroll-target clamp) — lets a consumer whose list has non-reorderable content + // above the reorderable region (e.g. a ListHeaderComponent) tell autoscroll not + // to scroll back up into it while dragging. Defaults to 0 (the real list top). + minScrollOffset?: Animated.SharedValue; onAnimValInit?: (animVals: ReturnType) => void; itemEnteringAnimation?: AnimateProps["entering"]; itemExitingAnimation?: AnimateProps["exiting"];