diff --git a/src/components/LegendList.tsx b/src/components/LegendList.tsx index 676f8b8f..4d4ca9c3 100644 --- a/src/components/LegendList.tsx +++ b/src/components/LegendList.tsx @@ -518,7 +518,14 @@ const LegendListInner = typedForwardRef(function LegendListInner( } const resolvedOffset = initialScroll.contentOffset ?? resolveInitialScrollOffset(ctx, initialScroll); - return usesBootstrapInitialScroll && state.initialScrollSession?.kind === "bootstrap" && Platform.OS === "web" + // For a horizontal RTL *bootstrap* scroll (initialScrollIndex / initialScrollAtEnd), + // this seed is a logical (LTR) offset; feeding it to the native `contentOffset` + // unconverted lands on the RTL mirror. Skip the seed and let the bootstrap + // dispatch (which converts) position the list. Scoped to bootstrap sessions so + // offset-only initial scrolls still apply their offset. + return usesBootstrapInitialScroll && + ((state.initialScrollSession?.kind === "bootstrap" && Platform.OS === "web") || + isHorizontalRTLProps({ horizontal, rtl })) ? undefined : resolvedOffset; }, [usesBootstrapInitialScroll]); diff --git a/src/core/bootstrapInitialScroll.ts b/src/core/bootstrapInitialScroll.ts index 1df77917..a4c8713f 100644 --- a/src/core/bootstrapInitialScroll.ts +++ b/src/core/bootstrapInitialScroll.ts @@ -10,6 +10,7 @@ import { IS_DEV } from "@/utils/devEnvironment"; import { getId } from "@/utils/getId"; import { getItemSize } from "@/utils/getItemSize"; import { requestAdjust } from "@/utils/requestAdjust"; +import { isHorizontalRTL } from "@/utils/rtl"; const DEFAULT_BOOTSTRAP_REVEAL_EPSILON = 1; const DEFAULT_BOOTSTRAP_REVEAL_MAX_FRAMES = 8; @@ -525,6 +526,12 @@ export function startBootstrapInitialScrollOnMount( const shouldFinishAtOrigin = offset === 0 && !initialScrollAtEnd && + // For a horizontal RTL list a logical offset of 0 is NOT the native origin: + // the native ScrollView rests at the opposite (max-offset) edge, so finishing + // "at origin" without scrolling leaves the list on the mirror end and the + // target index (e.g. initialScrollIndex 0) renders blank. Let the bootstrap + // dispatch run so the offset is converted to native RTL coordinates. + !isHorizontalRTL(state) && (isOffsetInitialScrollSession(state) ? Math.abs(target.contentOffset ?? 0) <= 1 : target.index === 0 && (target.viewPosition ?? 0) === 0 && Math.abs(target.viewOffset ?? 0) <= 1); @@ -943,6 +950,12 @@ export function evaluateBootstrapInitialScroll(ctx: StateContext) { if ( Platform.OS !== "web" && Platform.OS !== "android" && + // For a horizontal RTL list the mount seed and observed offset are logical 0, + // but the native origin is the opposite (max-offset) edge — so "already at the + // resolved offset" is a false positive and finishing without a scroll leaves + // index 0 blank. Always dispatch for horizontal RTL (which converts logical 0 + // to the native RTL coordinate). Mirrors the shouldFinishAtOrigin guard above. + !isHorizontalRTL(state) && Math.abs(bootstrapInitialScroll.seedContentOffset - resolvedOffset) <= 1 && Math.abs(getObservedBootstrapInitialScrollOffset(state) - resolvedOffset) <= 1 ) { diff --git a/src/core/checkFinishedScroll.ts b/src/core/checkFinishedScroll.ts index b3f74535..ec3b04f6 100644 --- a/src/core/checkFinishedScroll.ts +++ b/src/core/checkFinishedScroll.ts @@ -6,6 +6,7 @@ import { initialScrollCompletion, initialScrollWatchdog } from "@/core/initialSc import { Platform } from "@/platform/Platform"; import { getContentSize } from "@/state/getContentSize"; import type { StateContext } from "@/state/state"; +import { toNativeHorizontalOffset } from "@/utils/rtl"; type ActiveScrollTarget = NonNullable; const INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20; @@ -151,10 +152,16 @@ function checkFinishedScrollFrame(ctx: StateContext) { } function scrollToFallbackOffset(ctx: StateContext, offset: number) { - ctx.state.refScroller.current?.scrollTo({ + const state = ctx.state; + const isHorizontal = !!state.props.horizontal; + // `offset` is a logical (LTR) offset. The normal dispatch path converts it to + // the native RTL coordinate; do the same here so the watchdog/retry doesn't + // dispatch an unconverted offset and land on the RTL mirror index. + const x = isHorizontal ? toNativeHorizontalOffset(state, offset, getContentSize(ctx)) : 0; + state.refScroller.current?.scrollTo({ animated: false, - x: ctx.state.props.horizontal ? offset : 0, - y: ctx.state.props.horizontal ? 0 : offset, + x, + y: isHorizontal ? 0 : offset, }); }