Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions src/core/checkFinishedScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,41 @@ function isNativeInitialNonZeroTarget(state: StateContext["state"]) {
return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset);
}

function shouldFinishInitialScrollWithoutNativeProgress(state: StateContext["state"], scrollingTo: ActiveScrollTarget) {
function shouldFinishInitialScrollWithoutNativeProgress(ctx: StateContext, scrollingTo: ActiveScrollTarget) {
const { state } = ctx;
if (!scrollingTo.isInitialScroll || scrollingTo.animated || !state.didContainersLayout) {
return false;
}

if (state.initialScrollSession?.kind === "bootstrap") {
const targetOffset = scrollingTo.targetOffset ?? scrollingTo.offset;

// On iOS an unanimated initial scrollTo/contentOffset does NOT emit a
// scroll event, so gating completion on observing native scroll progress
// deadlocks plain initial targets: the fallback loop re-issues scrollTo to
// the same offset (which again emits nothing) until the watchdog gives up
// seconds later — all while the list is held at opacity 0.
//
// state.scroll is set optimistically at dispatch (doScrollTo.native.ts),
// so it is only trustworthy when the requested offset was actually
// applicable: a plain target (no viewOffset/viewPosition adjustment —
// those are recomputed on retries) that lies within the currently
// scrollable range (so native cannot have clamped it short). End-aligned /
// initialScrollAtEnd targets with footers can exceed the max scroll until
// late layout and must keep waiting for confirmed native progress, which
// their recomputed-offset retries do produce.
const maxScrollOffset = getContentSize(ctx) - state.scrollLength;
const canTrustDispatchedOffsetOnIOS =
Platform.OS === "ios" &&
!scrollingTo.viewOffset &&
!scrollingTo.viewPosition &&
targetOffset <= maxScrollOffset + 1;

if (!canTrustDispatchedOffsetOnIOS && state.initialScrollSession?.kind === "bootstrap") {
return false;
}

const targetOffset = scrollingTo.targetOffset ?? scrollingTo.offset;
if (
!canTrustDispatchedOffsetOnIOS &&
initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset) &&
initialScrollCompletion.didDispatchNativeScroll(state) &&
!state.hasScrolled
Expand Down Expand Up @@ -166,7 +190,7 @@ export function checkFinishedScrollFallback(ctx: StateContext) {
const shouldFinishInitialZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
const silentInitialDispatch = isSilentInitialDispatch(state, scrollingTo);
const canFinishInitialWithoutNativeProgress =
scrollingTo !== undefined ? shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) : false;
scrollingTo !== undefined ? shouldFinishInitialScrollWithoutNativeProgress(ctx, scrollingTo) : false;
const slowTimeout =
(scrollingTo?.isInitialScroll && !shouldFinishInitialZeroTarget && !canFinishInitialWithoutNativeProgress) ||
!state.didContainersLayout;
Expand Down Expand Up @@ -198,7 +222,7 @@ export function checkFinishedScrollFallback(ctx: StateContext) {
: 5;
const shouldFinishZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
const canFinishInitialScrollWithoutNativeProgress = shouldFinishInitialScrollWithoutNativeProgress(
state,
ctx,
isStillScrollingTo,
);
const completionState = getResolvedScrollCompletionState(ctx, isStillScrollingTo);
Expand Down