Skip to content
Open
Show file tree
Hide file tree
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
9 changes: 8 additions & 1 deletion src/components/LegendList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,14 @@ const LegendListInner = typedForwardRef(function LegendListInner<T>(
}

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]);
Expand Down
13 changes: 13 additions & 0 deletions src/core/bootstrapInitialScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
) {
Expand Down
13 changes: 10 additions & 3 deletions src/core/checkFinishedScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StateContext["state"]["scrollingTo"]>;
const INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20;
Expand Down Expand Up @@ -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,
});
}

Expand Down