authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-07 10:10:00-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-07 10:10:00-07:00
log8d7f78121660eb8599d4c5ff40cb4394159abfab
tree8ba5afb19748b089e5b8249b7264053e6a09566f
parent357b8d5217cd62826767b6ce6d7c47915f2777d2
signature Signed by PGP key B5690EEEBB952194

fix(mobile): wait for native thread scroll before reveal (#10486)


3 files changed, 357 insertions(+), 125 deletions(-)

patches/@legendapp__list@3.3.5.patch+138-120
......@@ -241,7 +241,7 @@ index ce1fe00001c9e5aee6c6ea8bb2d4757d4586d002..3ccf6f16067152dfcb0c143371e2ec6a
241241 * Number of columns to render items in.
242242 * @default 1
243243diff --git a/react-native.js b/react-native.js
244index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac0047a8ead3 100644
244index b3c5a306b293f797a8b338adfca3060c0f6db22b..5ac6bbe8fe40bf252fefa6b885c2196d0677d75f 100644
245245--- a/react-native.js
246246+++ b/react-native.js
247247@@ -717,6 +717,15 @@ function hasActiveInitialScroll(state) {
......@@ -269,16 +269,25 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
269269 set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
270270 set$(
271271 ctx,
272@@ -954,7 +963,7 @@ function setInitialRenderState(ctx, {
272@@ -954,7 +963,16 @@ function setInitialRenderState(ctx, {
273273 if (didInitialScroll) {
274274 state.didFinishInitialScroll = true;
275275 }
276276- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
277+ // Native contentOffset can seed an end target without dispatchInitialScroll.
278+ // Both overflow completion paths must wait for the native end landing.
279+ // Underflow needs no scroll: preserve UIKit's untouched resting position.
280+ if (state.didContainersLayout && state.didFinishInitialScroll && state.initialScroll && state.initialScroll.viewPosition === 1 && state.initialScroll.index === state.props.data.length - 1 && state.props.data.length > 0) {
281+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);
282+ if (insetStartAdjustment > 0 && getContentSize(ctx) > state.scrollLength - insetStartAdjustment + 1) {
283+ startInsetEndSettleWatchdog(ctx);
284+ }
285+ }
277286+ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold);
278287 if (isReadyToRender && !peek$(ctx, "readyToRender")) {
279288 set$(ctx, "readyToRender", true);
280289 setAdaptiveRender(ctx, "normal", "ready");
281@@ -1090,7 +1099,7 @@ function getRawContentLength(ctx) {
290@@ -1090,7 +1108,7 @@ function getRawContentLength(ctx) {
282291 function getAlignItemsAtEndPadding(ctx) {
283292 const { state } = ctx;
284293 const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
......@@ -287,7 +296,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
287296 }
288297 function updateContentMetricsState(ctx) {
289298 const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
290@@ -1115,6 +1124,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
299@@ -1115,6 +1133,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
291300 totalSize += add;
292301 }
293302 if (prevTotalSize !== totalSize) {
......@@ -298,7 +307,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
298307 if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
299308 state.pendingTotalSize = totalSize;
300309 } else {
301@@ -1304,18 +1317,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
310@@ -1304,18 +1326,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
302311 }
303312
304313 // src/core/clampScrollOffset.ts
......@@ -324,7 +333,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
324333 return clampedOffset;
325334 }
326335
327@@ -1451,10 +1469,10 @@ function checkFinishedScrollFrame(ctx) {
336@@ -1451,10 +1478,10 @@ function checkFinishedScrollFrame(ctx) {
328337 finishScrollTo(ctx);
329338 }
330339 }
......@@ -337,7 +346,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
337346 x: ctx.state.props.horizontal ? offset : 0,
338347 y: ctx.state.props.horizontal ? 0 : offset
339348 });
340@@ -1503,7 +1521,10 @@ function checkFinishedScrollFallback(ctx) {
349@@ -1503,7 +1530,10 @@ function checkFinishedScrollFallback(ctx) {
341350 );
342351 scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
343352 } else if (shouldRetryUnalignedEndScroll) {
......@@ -349,7 +358,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
349358 scheduleFallbackCheck(100);
350359 } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
351360 finishScrollTo(ctx);
352@@ -1560,15 +1581,28 @@ function doMaintainScrollAtEnd(ctx) {
361@@ -1560,15 +1590,28 @@ function doMaintainScrollAtEnd(ctx) {
353362 } = state;
354363 const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
355364 const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
......@@ -379,7 +388,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
379388 }
380389 if (!state.maintainingScrollAtEnd) {
381390 const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
382@@ -1591,9 +1625,18 @@ function doMaintainScrollAtEnd(ctx) {
391@@ -1591,9 +1634,18 @@ function doMaintainScrollAtEnd(ctx) {
383392 y: 0
384393 });
385394 } else {
......@@ -401,7 +410,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
401410 }
402411 setTimeout(
403412 () => {
404@@ -1624,6 +1667,10 @@ function doMaintainScrollAtEnd(ctx) {
413@@ -1624,6 +1676,10 @@ function doMaintainScrollAtEnd(ctx) {
405414 function requestAdjust(ctx, positionDiff, dataChanged) {
406415 const state = ctx.state;
407416 if (Math.abs(positionDiff) > 0.1) {
......@@ -412,7 +421,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
412421 const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
413422 const doit = () => {
414423 if (needsScrollWorkaround) {
415@@ -1728,7 +1775,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
424@@ -1728,7 +1784,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
416425 if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) {
417426 return 0;
418427 }
......@@ -423,7 +432,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
423432 const clampDelta = maxScroll - state.scroll;
424433 if (unresolvedAmount < 0) {
425434 return Math.max(unresolvedAmount, Math.min(0, clampDelta));
426@@ -1790,7 +1839,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
435@@ -1790,7 +1848,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
427436 settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta);
428437 return true;
429438 }
......@@ -432,7 +441,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
432441 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll);
433442 const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON;
434443 if (isAtExpectedNativeClamp) {
435@@ -1923,7 +1972,7 @@ function prepareMVCP(ctx, dataChanged) {
444@@ -1923,7 +1981,7 @@ function prepareMVCP(ctx, dataChanged) {
436445 if (diff > 0) {
437446 diff = Math.max(0, totalSize - state.scroll - state.scrollLength);
438447 } else {
......@@ -441,7 +450,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
441450 state.scroll = maxScroll;
442451 state.scrollPending = maxScroll;
443452 diff = 0;
444@@ -2320,8 +2369,121 @@ function scrollToIndex(ctx, {
453@@ -2320,8 +2378,121 @@ function scrollToIndex(ctx, {
445454 }
446455
447456 // src/core/initialScroll.ts
......@@ -451,11 +460,11 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
451460+var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40;
452461+function startInsetEndSettleWatchdog(ctx) {
453462+ const state = ctx.state;
454+ if (state.insetEndSettleWatchdogActive) {
463+ if (state.insetEndSettleWatchdogStarted || state.didLoad) {
455464+ return;
456465+ }
466+ state.insetEndSettleWatchdogStarted = true;
457467+ state.insetEndSettleWatchdogActive = true;
458+ state.didUserDrag = false;
459468+ // Hold the readyToRender opacity gate until the end landing is stable, so
460469+ // the estimated-to-measured settle chase happens before first VISIBLE
461470+ // paint instead of in front of the user. Capped so slow measurement can
......@@ -464,6 +473,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
464473+ let frames = 0;
465474+ let settledFrames = 0;
466475+ let revealStableFrames = 0;
476+ let previousEndOffset;
467477+ const releaseRevealHold = () => {
468478+ if (state.insetEndRevealHold) {
469479+ state.insetEndRevealHold = false;
......@@ -503,33 +513,32 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
503513+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);
504514+ const contentSize = getContentSize(ctx);
505515+ const scrollLength = state.scrollLength;
506+ if (insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize) && contentSize > scrollLength && !state.scrollingTo && !state.maintainingScrollAtEnd) {
507+ const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength);
508+ const distance = endOffset - state.scroll;
509+ // Estimated row sizes converging to measured ones can strand the initial
510+ // end landing when the library's own end-anchor bookkeeping gives up.
511+ // While still near the end (never fighting a user who scrolled away),
512+ // re-pin to the current true end until sizes stop changing.
513+ if (Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) {
514+ settledFrames = 0;
515+ revealStableFrames = 0;
516+ const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength);
517+ // state.scroll is optimistic: non-animated scrollTo writes it before UIKit
518+ // moves. Only a native scroll event proves that the requested offset landed.
519+ const nativeOffset = state.lastNativeScroll;
520+ const hasNativeOffset = typeof nativeOffset === "number" && Number.isFinite(nativeOffset);
521+ const distance = hasNativeOffset ? endOffset - nativeOffset : Infinity;
522+ const isIdle = !state.scrollingTo && !state.maintainingScrollAtEnd;
523+ const hasViewport = insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize);
524+ const isStable = hasViewport && isIdle && Math.abs(distance) <= 2 && previousEndOffset !== void 0 && Math.abs(endOffset - previousEndOffset) <= 1;
525+ previousEndOffset = endOffset;
526+ if (isStable) {
527+ settledFrames++;
528+ revealStableFrames++;
529+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
530+ onRevealStability();
531+ }
532+ } else {
533+ settledFrames = 0;
534+ revealStableFrames = 0;
535+ // Keep the existing near-end correction limit. In-flight scrolls and
536+ // unknown native offsets wait; neither counts as a settled frame.
537+ if (hasViewport && isIdle && Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) {
516538+ const scroller = state.refScroller.current;
517539+ if (scroller) {
518540+ scroller.scrollTo({ animated: false, x: 0, y: endOffset });
519541+ }
520+ } else {
521+ settledFrames++;
522+ revealStableFrames++;
523+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
524+ onRevealStability();
525+ }
526+ }
527+ } else {
528+ // Conditions that make re-pinning unnecessary (underflow, in-flight
529+ // programmatic scroll) count toward stability for the reveal.
530+ revealStableFrames++;
531+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
532+ onRevealStability();
533542+ }
534543+ }
535544+ requestAnimationFrame(tick);
......@@ -563,7 +572,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
563572 const requestedIndex = target.index;
564573 const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0;
565574 const itemSize = getItemSizeAtIndex(ctx, index);
566@@ -2747,7 +2909,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
575@@ -2747,7 +2918,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
567576 return;
568577 }
569578 if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {
......@@ -574,7 +583,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
574583 if (!shouldKeepEndTargetAlive) {
575584 if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) {
576585 clearPendingInitialScrollFooterLayout(ctx, {
577@@ -4672,7 +4836,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
586@@ -4672,7 +4845,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
578587 contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap);
579588 contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props);
580589 isReady = !hasUnknownTailSize;
......@@ -584,7 +593,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
584593 } else if (anchorIndex >= 0) {
585594 isReady = false;
586595 }
587@@ -4692,6 +4857,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
596@@ -4692,6 +4866,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
588597 updateScroll(ctx, state.scroll, true, { markHasScrolled: false });
589598 }
590599 (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });
......@@ -597,7 +606,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
597606 }
598607 return nextSize;
599608 }
600@@ -5715,6 +5886,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
609@@ -5715,6 +5895,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
601610 horizontal
602611 }) {
603612 const ctx = useStateContext();
......@@ -605,7 +614,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
605614 const columnWrapperStyle = ctx.columnWrapperStyle;
606615 const animSize = useValue$("totalSize");
607616 const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]);
608@@ -5725,6 +5897,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
617@@ -5725,6 +5906,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
609618 opacity: isVisible ? 1 : 0,
610619 width: animSize
611620 } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 };
......@@ -619,7 +628,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
619628 if (columnWrapperStyle) {
620629 const { columnGap, rowGap, gap } = columnWrapperStyle;
621630 const gapX = columnGap || gap || 0;
622@@ -5745,7 +5924,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
631@@ -5745,7 +5933,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
623632 }
624633 }
625634 }
......@@ -629,7 +638,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
629638 });
630639 var Containers = typedMemo(function Containers2({
631640 freshDataTransitionEpoch,
632@@ -5896,7 +6076,12 @@ var StyleSheet = ReactNative.StyleSheet;
641@@ -5896,7 +6085,12 @@ var StyleSheet = ReactNative.StyleSheet;
633642
634643 // src/components/ListComponent.tsx
635644 var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) {
......@@ -642,7 +651,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
642651 if (alignItemsAtEndPadding <= 0) {
643652 return null;
644653 }
645@@ -5929,8 +6114,12 @@ var ListComponent = typedMemo(function ListComponent2({
654@@ -5929,8 +6123,12 @@ var ListComponent = typedMemo(function ListComponent2({
646655 refScrollView,
647656 renderScrollComponent,
648657 onLayoutFooter,
......@@ -655,7 +664,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
655664 scrollAdjustHandler,
656665 snapToIndices,
657666 stickyHeaderConfig,
658@@ -6001,7 +6190,17 @@ var ListComponent = typedMemo(function ListComponent2({
667@@ -6001,7 +6199,17 @@ var ListComponent = typedMemo(function ListComponent2({
659668 SnapOrScroll,
660669 {
661670 ...rest,
......@@ -674,7 +683,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
674683 contentContainerStyle: [
675684 horizontal ? { height: "100%" } : {},
676685 contentContainerStyle,
677@@ -6010,7 +6209,10 @@ var ListComponent = typedMemo(function ListComponent2({
686@@ -6010,7 +6218,10 @@ var ListComponent = typedMemo(function ListComponent2({
678687 ],
679688 contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0,
680689 horizontal,
......@@ -686,7 +695,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
686695 onLayout,
687696 onScroll: onScroll2,
688697 ref: refScrollView,
689@@ -6751,7 +6953,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
698@@ -6751,7 +6962,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
690699 endBuffered: state.endBuffered,
691700 getAverageItemSizes: () => getAverageItemSizes(state),
692701 indexByKey: (key) => state.indexByKey.get(key),
......@@ -695,7 +704,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
695704 isAtStart: peek$(ctx, "isAtStart"),
696705 isEndReached: state.isEndReached,
697706 isNearEnd: peek$(ctx, "isNearEnd"),
698@@ -7075,6 +7277,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
707@@ -7075,6 +7286,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
699708 dataVersion,
700709 drawDistance = 250,
701710 contentInsetEndAdjustment,
......@@ -703,7 +712,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
703712 estimatedItemSize = 100,
704713 estimatedListSize,
705714 extraData,
706@@ -7132,10 +7335,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
715@@ -7132,10 +7344,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
707716 const animatedPropsInternal = props.animatedPropsInternal;
708717 const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list";
709718 const positionComponentInternal = props.positionComponentInternal;
......@@ -716,7 +725,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
716725 stickyPositionComponentInternal: _stickyPositionComponentInternal,
717726 ...restProps
718727 } = rest;
719@@ -7200,7 +7405,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
728@@ -7200,7 +7414,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
720729 const combinedRef = useCombinedRef(refScroller, refScrollView);
721730 const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString());
722731 const stickyHeaderIndices = stickyHeaderIndicesProp;
......@@ -725,7 +734,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
725734 const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved);
726735 const alwaysRenderIndices = React2.useMemo(() => {
727736 const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex);
728@@ -7341,6 +7546,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
737@@ -7341,6 +7555,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
729738 contentContainerAlignItems: contentContainerStyle.alignItems,
730739 contentInset,
731740 contentInsetEndAdjustment: contentInsetEndAdjustmentResolved,
......@@ -733,7 +742,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
733742 data: dataProp,
734743 dataKey,
735744 dataVersion,
736@@ -7372,6 +7578,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
745@@ -7372,6 +7587,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
737746 renderItem,
738747 rtl,
739748 snapToIndices,
......@@ -741,7 +750,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
741750 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [],
742751 stickyHeaderIndicesSet: React2.useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]),
743752 stickyPositionComponentInternal,
744@@ -7423,6 +7630,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
753@@ -7423,6 +7639,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
745754 return void 0;
746755 }
747756 const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll);
......@@ -755,7 +764,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
755764 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset;
756765 }, [usesBootstrapInitialScroll]);
757766 React2.useLayoutEffect(() => {
758@@ -7547,6 +7761,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
767@@ -7547,6 +7770,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
759768 [
760769 dataKey,
761770 dataVersion,
......@@ -763,7 +772,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
763772 memoizedLastItemKeys.join(","),
764773 numColumnsProp,
765774 nextScrollAxisGap,
766@@ -7643,6 +7858,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
775@@ -7643,6 +7867,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
767776 () => ({
768777 getRenderedItem: (key) => getRenderedItem(ctx, key),
769778 onMomentumScrollEnd: (event) => {
......@@ -771,7 +780,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
771780 checkFinishedScrollFallback(ctx);
772781 if (state.props.onMomentumScrollEnd) {
773782 state.props.onMomentumScrollEnd(event);
774@@ -7651,6 +7867,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
783@@ -7651,6 +7876,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
775784 onScroll: (event) => onScroll(ctx, event),
776785 onScrollBeginDrag: (event) => {
777786 var _a4, _b2;
......@@ -780,7 +789,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
780789 prepareReachedEdgeForNextUserScroll(ctx);
781790 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
782791 },
783@@ -7676,11 +7894,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
792@@ -7676,11 +7903,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
784793 ListFooterComponent,
785794 ListFooterComponentStyle,
786795 ListHeaderComponent,
......@@ -800,7 +809,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
800809 recycleItems,
801810 refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, {
802811diff --git a/react-native.mjs b/react-native.mjs
803index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97440bc623 100644
812index 40e87cda8c9bc79a889e5542f29af429a24b24d4..93aac741d2cf77ce35996362439108176f19da7a 100644
804813--- a/react-native.mjs
805814+++ b/react-native.mjs
806815@@ -696,6 +696,15 @@ function hasActiveInitialScroll(state) {
......@@ -828,16 +837,25 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
828837 set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
829838 set$(
830839 ctx,
831@@ -933,7 +942,7 @@ function setInitialRenderState(ctx, {
840@@ -933,7 +942,16 @@ function setInitialRenderState(ctx, {
832841 if (didInitialScroll) {
833842 state.didFinishInitialScroll = true;
834843 }
835844- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
845+ // Native contentOffset can seed an end target without dispatchInitialScroll.
846+ // Both overflow completion paths must wait for the native end landing.
847+ // Underflow needs no scroll: preserve UIKit's untouched resting position.
848+ if (state.didContainersLayout && state.didFinishInitialScroll && state.initialScroll && state.initialScroll.viewPosition === 1 && state.initialScroll.index === state.props.data.length - 1 && state.props.data.length > 0) {
849+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);
850+ if (insetStartAdjustment > 0 && getContentSize(ctx) > state.scrollLength - insetStartAdjustment + 1) {
851+ startInsetEndSettleWatchdog(ctx);
852+ }
853+ }
836854+ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold);
837855 if (isReadyToRender && !peek$(ctx, "readyToRender")) {
838856 set$(ctx, "readyToRender", true);
839857 setAdaptiveRender(ctx, "normal", "ready");
840@@ -1069,7 +1078,7 @@ function getRawContentLength(ctx) {
858@@ -1069,7 +1087,7 @@ function getRawContentLength(ctx) {
841859 function getAlignItemsAtEndPadding(ctx) {
842860 const { state } = ctx;
843861 const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
......@@ -846,7 +864,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
846864 }
847865 function updateContentMetricsState(ctx) {
848866 const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
849@@ -1094,6 +1103,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
867@@ -1094,6 +1112,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
850868 totalSize += add;
851869 }
852870 if (prevTotalSize !== totalSize) {
......@@ -857,7 +875,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
857875 if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
858876 state.pendingTotalSize = totalSize;
859877 } else {
860@@ -1283,18 +1296,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
878@@ -1283,18 +1305,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
861879 }
862880
863881 // src/core/clampScrollOffset.ts
......@@ -883,7 +901,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
883901 return clampedOffset;
884902 }
885903
886@@ -1430,10 +1448,10 @@ function checkFinishedScrollFrame(ctx) {
904@@ -1430,10 +1457,10 @@ function checkFinishedScrollFrame(ctx) {
887905 finishScrollTo(ctx);
888906 }
889907 }
......@@ -896,7 +914,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
896914 x: ctx.state.props.horizontal ? offset : 0,
897915 y: ctx.state.props.horizontal ? 0 : offset
898916 });
899@@ -1482,7 +1500,10 @@ function checkFinishedScrollFallback(ctx) {
917@@ -1482,7 +1509,10 @@ function checkFinishedScrollFallback(ctx) {
900918 );
901919 scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
902920 } else if (shouldRetryUnalignedEndScroll) {
......@@ -908,7 +926,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
908926 scheduleFallbackCheck(100);
909927 } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
910928 finishScrollTo(ctx);
911@@ -1539,15 +1560,28 @@ function doMaintainScrollAtEnd(ctx) {
929@@ -1539,15 +1569,28 @@ function doMaintainScrollAtEnd(ctx) {
912930 } = state;
913931 const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
914932 const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
......@@ -938,7 +956,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
938956 }
939957 if (!state.maintainingScrollAtEnd) {
940958 const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
941@@ -1570,9 +1604,18 @@ function doMaintainScrollAtEnd(ctx) {
959@@ -1570,9 +1613,18 @@ function doMaintainScrollAtEnd(ctx) {
942960 y: 0
943961 });
944962 } else {
......@@ -960,7 +978,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
960978 }
961979 setTimeout(
962980 () => {
963@@ -1603,6 +1646,10 @@ function doMaintainScrollAtEnd(ctx) {
981@@ -1603,6 +1655,10 @@ function doMaintainScrollAtEnd(ctx) {
964982 function requestAdjust(ctx, positionDiff, dataChanged) {
965983 const state = ctx.state;
966984 if (Math.abs(positionDiff) > 0.1) {
......@@ -971,7 +989,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
971989 const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
972990 const doit = () => {
973991 if (needsScrollWorkaround) {
974@@ -1707,7 +1754,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
992@@ -1707,7 +1763,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
975993 if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) {
976994 return 0;
977995 }
......@@ -982,7 +1000,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
9821000 const clampDelta = maxScroll - state.scroll;
9831001 if (unresolvedAmount < 0) {
9841002 return Math.max(unresolvedAmount, Math.min(0, clampDelta));
985@@ -1769,7 +1818,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
1003@@ -1769,7 +1827,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
9861004 settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta);
9871005 return true;
9881006 }
......@@ -991,7 +1009,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
9911009 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll);
9921010 const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON;
9931011 if (isAtExpectedNativeClamp) {
994@@ -1902,7 +1951,7 @@ function prepareMVCP(ctx, dataChanged) {
1012@@ -1902,7 +1960,7 @@ function prepareMVCP(ctx, dataChanged) {
9951013 if (diff > 0) {
9961014 diff = Math.max(0, totalSize - state.scroll - state.scrollLength);
9971015 } else {
......@@ -1000,7 +1018,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
10001018 state.scroll = maxScroll;
10011019 state.scrollPending = maxScroll;
10021020 diff = 0;
1003@@ -2299,8 +2348,121 @@ function scrollToIndex(ctx, {
1021@@ -2299,8 +2357,121 @@ function scrollToIndex(ctx, {
10041022 }
10051023
10061024 // src/core/initialScroll.ts
......@@ -1010,11 +1028,11 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
10101028+var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40;
10111029+function startInsetEndSettleWatchdog(ctx) {
10121030+ const state = ctx.state;
1013+ if (state.insetEndSettleWatchdogActive) {
1031+ if (state.insetEndSettleWatchdogStarted || state.didLoad) {
10141032+ return;
10151033+ }
1034+ state.insetEndSettleWatchdogStarted = true;
10161035+ state.insetEndSettleWatchdogActive = true;
1017+ state.didUserDrag = false;
10181036+ // Hold the readyToRender opacity gate until the end landing is stable, so
10191037+ // the estimated-to-measured settle chase happens before first VISIBLE
10201038+ // paint instead of in front of the user. Capped so slow measurement can
......@@ -1023,6 +1041,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
10231041+ let frames = 0;
10241042+ let settledFrames = 0;
10251043+ let revealStableFrames = 0;
1044+ let previousEndOffset;
10261045+ const releaseRevealHold = () => {
10271046+ if (state.insetEndRevealHold) {
10281047+ state.insetEndRevealHold = false;
......@@ -1062,33 +1081,32 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
10621081+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);
10631082+ const contentSize = getContentSize(ctx);
10641083+ const scrollLength = state.scrollLength;
1065+ if (insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize) && contentSize > scrollLength && !state.scrollingTo && !state.maintainingScrollAtEnd) {
1066+ const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength);
1067+ const distance = endOffset - state.scroll;
1068+ // Estimated row sizes converging to measured ones can strand the initial
1069+ // end landing when the library's own end-anchor bookkeeping gives up.
1070+ // While still near the end (never fighting a user who scrolled away),
1071+ // re-pin to the current true end until sizes stop changing.
1072+ if (Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) {
1073+ settledFrames = 0;
1074+ revealStableFrames = 0;
1084+ const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength);
1085+ // state.scroll is optimistic: non-animated scrollTo writes it before UIKit
1086+ // moves. Only a native scroll event proves that the requested offset landed.
1087+ const nativeOffset = state.lastNativeScroll;
1088+ const hasNativeOffset = typeof nativeOffset === "number" && Number.isFinite(nativeOffset);
1089+ const distance = hasNativeOffset ? endOffset - nativeOffset : Infinity;
1090+ const isIdle = !state.scrollingTo && !state.maintainingScrollAtEnd;
1091+ const hasViewport = insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize);
1092+ const isStable = hasViewport && isIdle && Math.abs(distance) <= 2 && previousEndOffset !== void 0 && Math.abs(endOffset - previousEndOffset) <= 1;
1093+ previousEndOffset = endOffset;
1094+ if (isStable) {
1095+ settledFrames++;
1096+ revealStableFrames++;
1097+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
1098+ onRevealStability();
1099+ }
1100+ } else {
1101+ settledFrames = 0;
1102+ revealStableFrames = 0;
1103+ // Keep the existing near-end correction limit. In-flight scrolls and
1104+ // unknown native offsets wait; neither counts as a settled frame.
1105+ if (hasViewport && isIdle && Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) {
10751106+ const scroller = state.refScroller.current;
10761107+ if (scroller) {
10771108+ scroller.scrollTo({ animated: false, x: 0, y: endOffset });
10781109+ }
1079+ } else {
1080+ settledFrames++;
1081+ revealStableFrames++;
1082+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
1083+ onRevealStability();
1084+ }
1085+ }
1086+ } else {
1087+ // Conditions that make re-pinning unnecessary (underflow, in-flight
1088+ // programmatic scroll) count toward stability for the reveal.
1089+ revealStableFrames++;
1090+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
1091+ onRevealStability();
10921110+ }
10931111+ }
10941112+ requestAnimationFrame(tick);
......@@ -1122,7 +1140,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
11221140 const requestedIndex = target.index;
11231141 const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0;
11241142 const itemSize = getItemSizeAtIndex(ctx, index);
1125@@ -2726,7 +2888,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
1143@@ -2726,7 +2897,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
11261144 return;
11271145 }
11281146 if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {
......@@ -1133,7 +1151,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
11331151 if (!shouldKeepEndTargetAlive) {
11341152 if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) {
11351153 clearPendingInitialScrollFooterLayout(ctx, {
1136@@ -4651,7 +4815,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
1154@@ -4651,7 +4824,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
11371155 contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap);
11381156 contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props);
11391157 isReady = !hasUnknownTailSize;
......@@ -1143,7 +1161,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
11431161 } else if (anchorIndex >= 0) {
11441162 isReady = false;
11451163 }
1146@@ -4671,6 +4836,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
1164@@ -4671,6 +4845,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
11471165 updateScroll(ctx, state.scroll, true, { markHasScrolled: false });
11481166 }
11491167 (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });
......@@ -1156,7 +1174,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
11561174 }
11571175 return nextSize;
11581176 }
1159@@ -5694,6 +5865,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
1177@@ -5694,6 +5874,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
11601178 horizontal
11611179 }) {
11621180 const ctx = useStateContext();
......@@ -1164,7 +1182,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
11641182 const columnWrapperStyle = ctx.columnWrapperStyle;
11651183 const animSize = useValue$("totalSize");
11661184 const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]);
1167@@ -5704,6 +5876,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
1185@@ -5704,6 +5885,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
11681186 opacity: isVisible ? 1 : 0,
11691187 width: animSize
11701188 } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 };
......@@ -1178,7 +1196,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
11781196 if (columnWrapperStyle) {
11791197 const { columnGap, rowGap, gap } = columnWrapperStyle;
11801198 const gapX = columnGap || gap || 0;
1181@@ -5724,7 +5903,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
1199@@ -5724,7 +5912,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
11821200 }
11831201 }
11841202 }
......@@ -1188,7 +1206,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
11881206 });
11891207 var Containers = typedMemo(function Containers2({
11901208 freshDataTransitionEpoch,
1191@@ -5875,7 +6055,12 @@ var StyleSheet = StyleSheet$1;
1209@@ -5875,7 +6064,12 @@ var StyleSheet = StyleSheet$1;
11921210
11931211 // src/components/ListComponent.tsx
11941212 var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) {
......@@ -1201,7 +1219,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
12011219 if (alignItemsAtEndPadding <= 0) {
12021220 return null;
12031221 }
1204@@ -5908,8 +6093,12 @@ var ListComponent = typedMemo(function ListComponent2({
1222@@ -5908,8 +6102,12 @@ var ListComponent = typedMemo(function ListComponent2({
12051223 refScrollView,
12061224 renderScrollComponent,
12071225 onLayoutFooter,
......@@ -1214,7 +1232,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
12141232 scrollAdjustHandler,
12151233 snapToIndices,
12161234 stickyHeaderConfig,
1217@@ -5980,7 +6169,17 @@ var ListComponent = typedMemo(function ListComponent2({
1235@@ -5980,7 +6178,17 @@ var ListComponent = typedMemo(function ListComponent2({
12181236 SnapOrScroll,
12191237 {
12201238 ...rest,
......@@ -1233,7 +1251,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
12331251 contentContainerStyle: [
12341252 horizontal ? { height: "100%" } : {},
12351253 contentContainerStyle,
1236@@ -5989,7 +6188,10 @@ var ListComponent = typedMemo(function ListComponent2({
1254@@ -5989,7 +6197,10 @@ var ListComponent = typedMemo(function ListComponent2({
12371255 ],
12381256 contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0,
12391257 horizontal,
......@@ -1245,7 +1263,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
12451263 onLayout,
12461264 onScroll: onScroll2,
12471265 ref: refScrollView,
1248@@ -6730,7 +6932,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
1266@@ -6730,7 +6941,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
12491267 endBuffered: state.endBuffered,
12501268 getAverageItemSizes: () => getAverageItemSizes(state),
12511269 indexByKey: (key) => state.indexByKey.get(key),
......@@ -1254,7 +1272,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
12541272 isAtStart: peek$(ctx, "isAtStart"),
12551273 isEndReached: state.isEndReached,
12561274 isNearEnd: peek$(ctx, "isNearEnd"),
1257@@ -7054,6 +7256,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1275@@ -7054,6 +7265,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12581276 dataVersion,
12591277 drawDistance = 250,
12601278 contentInsetEndAdjustment,
......@@ -1262,7 +1280,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
12621280 estimatedItemSize = 100,
12631281 estimatedListSize,
12641282 extraData,
1265@@ -7111,10 +7314,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1283@@ -7111,10 +7323,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12661284 const animatedPropsInternal = props.animatedPropsInternal;
12671285 const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list";
12681286 const positionComponentInternal = props.positionComponentInternal;
......@@ -1275,7 +1293,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
12751293 stickyPositionComponentInternal: _stickyPositionComponentInternal,
12761294 ...restProps
12771295 } = rest;
1278@@ -7179,7 +7384,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1296@@ -7179,7 +7393,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12791297 const combinedRef = useCombinedRef(refScroller, refScrollView);
12801298 const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString());
12811299 const stickyHeaderIndices = stickyHeaderIndicesProp;
......@@ -1284,7 +1302,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
12841302 const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved);
12851303 const alwaysRenderIndices = useMemo(() => {
12861304 const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex);
1287@@ -7320,6 +7525,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1305@@ -7320,6 +7534,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12881306 contentContainerAlignItems: contentContainerStyle.alignItems,
12891307 contentInset,
12901308 contentInsetEndAdjustment: contentInsetEndAdjustmentResolved,
......@@ -1292,7 +1310,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
12921310 data: dataProp,
12931311 dataKey,
12941312 dataVersion,
1295@@ -7351,6 +7557,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1313@@ -7351,6 +7566,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12961314 renderItem,
12971315 rtl,
12981316 snapToIndices,
......@@ -1300,7 +1318,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
13001318 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [],
13011319 stickyHeaderIndicesSet: useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]),
13021320 stickyPositionComponentInternal,
1303@@ -7402,6 +7609,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1321@@ -7402,6 +7618,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
13041322 return void 0;
13051323 }
13061324 const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll);
......@@ -1314,7 +1332,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
13141332 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset;
13151333 }, [usesBootstrapInitialScroll]);
13161334 useLayoutEffect(() => {
1317@@ -7526,6 +7740,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1335@@ -7526,6 +7749,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
13181336 [
13191337 dataKey,
13201338 dataVersion,
......@@ -1322,7 +1340,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
13221340 memoizedLastItemKeys.join(","),
13231341 numColumnsProp,
13241342 nextScrollAxisGap,
1325@@ -7622,6 +7837,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1343@@ -7622,6 +7846,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
13261344 () => ({
13271345 getRenderedItem: (key) => getRenderedItem(ctx, key),
13281346 onMomentumScrollEnd: (event) => {
......@@ -1330,7 +1348,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
13301348 checkFinishedScrollFallback(ctx);
13311349 if (state.props.onMomentumScrollEnd) {
13321350 state.props.onMomentumScrollEnd(event);
1333@@ -7630,6 +7846,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1351@@ -7630,6 +7855,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
13341352 onScroll: (event) => onScroll(ctx, event),
13351353 onScrollBeginDrag: (event) => {
13361354 var _a4, _b2;
......@@ -1339,7 +1357,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
13391357 prepareReachedEdgeForNextUserScroll(ctx);
13401358 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
13411359 },
1342@@ -7655,11 +7873,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1360@@ -7655,11 +7882,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
13431361 ListFooterComponent,
13441362 ListFooterComponentStyle,
13451363 ListHeaderComponent,
pnpm-lock.yaml+5-5
......@@ -91,7 +91,7 @@ patchedDependencies:
9191 '@effect/vitest@4.0.0-beta.103': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b
9292 '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2
9393 '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368
94 '@legendapp/list@3.3.5': 2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee
94 '@legendapp/list@3.3.5': a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b
9595 '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4
9696 '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d
9797 '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd
......@@ -236,7 +236,7 @@ importers:
236236 version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
237237 '@legendapp/list':
238238 specifier: 'catalog:'
239 version: 3.3.5(patch_hash=2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
239 version: 3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
240240 '@noble/curves':
241241 specifier: 'catalog:'
242242 version: 1.9.1
......@@ -580,7 +580,7 @@ importers:
580580 version: 0.9.0
581581 '@legendapp/list':
582582 specifier: 'catalog:'
583 version: 3.3.5(patch_hash=2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
583 version: 3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
584584 '@lexical/react':
585585 specifier: ^0.41.0
586586 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31)
......@@ -13269,7 +13269,7 @@ snapshots:
1326913269 dependencies:
1327013270 jsbi: 4.3.2
1327113271
13272 '@legendapp/list@3.3.5(patch_hash=2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
13272 '@legendapp/list@3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
1327313273 dependencies:
1327413274 react: 19.2.3
1327513275 use-sync-external-store: 1.6.0(react@19.2.3)
......@@ -13277,7 +13277,7 @@ snapshots:
1327713277 react-dom: 19.2.3(react@19.2.3)
1327813278 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
1327913279
13280 '@legendapp/list@3.3.5(patch_hash=2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
13280 '@legendapp/list@3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
1328113281 dependencies:
1328213282 react: 19.2.6
1328313283 use-sync-external-store: 1.6.0(react@19.2.6)
scripts/legend-list-initial-reveal.test.ts created+214
......@@ -0,0 +1,214 @@
1// @effect-diagnostics nodeBuiltinImport:off - reads the installed native JS bundle for synchronous VM tests.
2import * as NodeFS from "node:fs";
3import * as NodeVM from "node:vm";
4import { describe, expect, it, vi } from "vite-plus/test";
5
6// Exercise the shipped patch without loading React Native in the Node runner.
7// Native scroll delivery and animation frames advance independently here.
8function createList(bundle: string) {
9 const source = NodeFS.readFileSync(
10 new URL(`../apps/mobile/node_modules/@legendapp/list/${bundle}`, import.meta.url),
11 "utf8",
12 );
13 const renderState = source.slice(
14 source.indexOf("function setInitialRenderState("),
15 source.indexOf("// src/core/finishInitialScroll.ts"),
16 );
17 const watchdog = source.slice(
18 source.indexOf("var INSET_END_SETTLE_WATCHDOG_FRAMES"),
19 source.indexOf("function dispatchInitialScroll("),
20 );
21 const frames: Array<() => void> = [];
22 const values = new Map<string, unknown>();
23 const scrollTo = vi.fn();
24 const onLoad = vi.fn();
25 const state = {
26 props: { data: ["message"], onLoad, drawDistance: 500 },
27 initialScroll: { index: 0, viewPosition: 1 },
28 loadStartTime: 0,
29 didContainersLayout: true,
30 didFinishInitialScroll: true,
31 didLoad: false,
32 didUserDrag: false,
33 scrollLength: 800,
34 scroll: 400,
35 lastNativeScroll: 200 as number | undefined,
36 scrollingTo: undefined as { offset: number } | undefined,
37 maintainingScrollAtEnd: false,
38 refScroller: { current: { scrollTo } },
39 };
40 const ctx = { state };
41 let contentSize = 1200;
42 let inset = 100;
43 const api = NodeVM.runInNewContext(
44 `${renderState}\n${watchdog}\n({ start: startInsetEndSettleWatchdog, complete: setInitialRenderState })`,
45 {
46 requestAnimationFrame: (callback: () => void) => frames.push(callback),
47 getContentSize: () => contentSize,
48 getContentInsetStartAdjustment: () => inset,
49 peek$: (_ctx: unknown, key: string) => values.get(key),
50 set$: (_ctx: unknown, key: string, value: unknown) => values.set(key, value),
51 setAdaptiveRender: vi.fn(),
52 scheduleFullDrawDistancePrewarm: vi.fn(),
53 INITIAL_DRAW_DISTANCE: 250,
54 },
55 ) as {
56 start: (context: typeof ctx) => void;
57 complete: (context: typeof ctx, flags: Record<string, boolean>) => void;
58 };
59 return {
60 state,
61 scrollTo,
62 onLoad,
63 start: () => api.start(ctx),
64 complete: () => api.complete(ctx, {}),
65 ready: () => values.get("readyToRender") === true,
66 resize: (size: number) => {
67 contentSize = size;
68 },
69 setInset: (value: number) => {
70 inset = value;
71 },
72 advance(count: number) {
73 for (let index = 0; index < count; index++) {
74 const batch = frames.splice(0);
75 for (const frame of batch) frame();
76 }
77 },
78 };
79}
80
81for (const bundle of ["react-native.js", "react-native.mjs"]) {
82 describe(`initial inset end reveal (${bundle})`, () => {
83 it("waits for the native offset instead of the optimistic scroll target", () => {
84 const list = createList(bundle);
85 list.start();
86 list.advance(8);
87 expect(list.ready()).toBe(false);
88 expect(list.scrollTo).toHaveBeenCalledWith({ animated: false, x: 0, y: 400 });
89 list.state.lastNativeScroll = 400;
90 list.advance(7);
91 expect(list.ready()).toBe(true);
92 expect(list.onLoad).toHaveBeenCalledTimes(1);
93 });
94
95 it("gates the seeded contentOffset completion path too", () => {
96 const list = createList(bundle);
97 list.complete();
98 expect(list.ready()).toBe(false);
99 list.state.lastNativeScroll = 400;
100 list.advance(7);
101 expect(list.ready()).toBe(true);
102 });
103
104 it("preserves an initial index before the last item", () => {
105 const list = createList(bundle);
106 list.state.props.data = ["requested message", "later message"];
107 list.state.scroll = 200;
108 list.complete();
109 list.advance(8);
110 expect(list.ready()).toBe(true);
111 expect(list.scrollTo).not.toHaveBeenCalled();
112 });
113
114 it("does not count an in-flight scroll as stability", () => {
115 const list = createList(bundle);
116 list.state.lastNativeScroll = 400;
117 list.state.scrollingTo = { offset: 400 };
118 list.start();
119 list.advance(8);
120 expect(list.ready()).toBe(false);
121 list.state.scrollingTo = undefined;
122 list.state.maintainingScrollAtEnd = true;
123 list.advance(8);
124 expect(list.ready()).toBe(false);
125 list.state.maintainingScrollAtEnd = false;
126 list.advance(7);
127 expect(list.ready()).toBe(true);
128 });
129
130 it("waits for a stable end target even when native scrolling follows each measurement", () => {
131 const list = createList(bundle);
132 list.start();
133 for (let index = 0; index < 10; index++) {
134 list.resize(1200 + index * 10);
135 list.state.lastNativeScroll = 400 + index * 10;
136 list.advance(1);
137 }
138 expect(list.ready()).toBe(false);
139 list.advance(7);
140 expect(list.ready()).toBe(true);
141 });
142
143 it("accounts for header insets when content is shorter than the full viewport", () => {
144 const list = createList(bundle);
145 list.resize(780);
146 list.state.scroll = -20;
147 list.state.lastNativeScroll = -100;
148 list.start();
149 list.advance(8);
150 expect(list.ready()).toBe(false);
151 expect(list.scrollTo).toHaveBeenCalledWith({ animated: false, x: 0, y: -20 });
152 list.state.lastNativeScroll = -20;
153 list.advance(7);
154 expect(list.ready()).toBe(true);
155 });
156
157 it("keeps the reveal bounded when native events never arrive", () => {
158 const list = createList(bundle);
159 list.state.lastNativeScroll = undefined;
160 list.start();
161 list.advance(8);
162 expect(list.ready()).toBe(false);
163 expect(list.scrollTo).not.toHaveBeenCalled();
164 list.advance(32);
165 expect(list.ready()).toBe(true);
166 });
167
168 it("releases control on drag and never starts another initial hold", () => {
169 const list = createList(bundle);
170 list.start();
171 list.state.didUserDrag = true;
172 list.advance(1);
173 expect(list.ready()).toBe(true);
174 expect(list.scrollTo).not.toHaveBeenCalled();
175 list.complete();
176 list.start();
177 list.advance(20);
178 expect(list.state.didUserDrag).toBe(true);
179 expect(list.scrollTo).not.toHaveBeenCalled();
180 expect(list.onLoad).toHaveBeenCalledTimes(1);
181 });
182
183 it("preserves a drag that started before initial layout completed", () => {
184 const list = createList(bundle);
185 list.state.didUserDrag = true;
186 list.complete();
187 list.advance(1);
188 expect(list.ready()).toBe(true);
189 expect(list.state.didUserDrag).toBe(true);
190 expect(list.scrollTo).not.toHaveBeenCalled();
191 });
192
193 it("reveals short content at UIKit's resting offset without awaiting a scroll event", () => {
194 const list = createList(bundle);
195 list.resize(700);
196 list.state.lastNativeScroll = undefined;
197 list.complete();
198 expect(list.ready()).toBe(true);
199 list.advance(10);
200 expect(list.scrollTo).not.toHaveBeenCalled();
201 });
202
203 it("does not gate empty lists or lists without an automatic header inset", () => {
204 const list = createList(bundle);
205 list.setInset(0);
206 list.complete();
207 expect(list.ready()).toBe(true);
208 const empty = createList(bundle);
209 empty.state.props.data = [];
210 empty.complete();
211 expect(empty.ready()).toBe(true);
212 });
213 });
214}