@legendapp/list 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,14 @@
1
+ ## 3.1.2
2
+
3
+ - Fix: On Android, end-aligned lists could double apply scroll clamping during MVCP and be slightly underscrolled
4
+
5
+ ## 3.1.1
6
+
7
+ - Fix: `maintainScrollAtEnd` now stays pinned when a `ListFooterComponent` appears, disappears, or changes size, so chat typing indicators and other dynamic footers do not leave the list slightly above the end. If you use an explicit `maintainScrollAtEnd.on` config, add `footerLayout` to opt into footer size changes.
8
+
1
9
  ## 3.1.0
2
10
 
3
- - Feat: Add `experimental_adaptiveRender` with `useAdaptiveRender` and `useAdaptiveRenderChange`, so item components can render a lighter version while the list is moving quickly and return to normal after scrolling slows.
11
+ - Feat: Add `experimental_adaptiveRender` prop, with `useAdaptiveRender` and `useAdaptiveRenderChange` hooks, so item components can render a lighter version while the list is moving quickly and return to normal after scrolling slows.
4
12
  - Feat: Add `setItemSize` to the list ref for updating a known item's measured size directly when its content changes outside normal layout measurement.
5
13
  - Fix: Web maintainVisibleContentPosition keeps the intended anchor when headers change, browser scroll anchoring runs, or an animated `scrollToEnd` is already in progress. #468 #463
6
14
  - Fix: `initialScrollIndex` and `initialScrollAtEnd` use the latest initial scroll props when data starts empty and loads later, so lists do not scroll to an old startup target.
package/animated.d.ts CHANGED
@@ -460,6 +460,7 @@ interface AlwaysRenderConfig {
460
460
  }
461
461
  interface MaintainScrollAtEndOnOptions {
462
462
  dataChange?: boolean;
463
+ footerLayout?: boolean;
463
464
  itemLayout?: boolean;
464
465
  layout?: boolean;
465
466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/list",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "Legend List is a drop-in replacement for FlatList with much better performance and supporting dynamically sized items.",
5
5
  "sideEffects": false,
6
6
  "private": false,
package/react-native.d.ts CHANGED
@@ -460,6 +460,7 @@ interface AlwaysRenderConfig {
460
460
  }
461
461
  interface MaintainScrollAtEndOnOptions {
462
462
  dataChange?: boolean;
463
+ footerLayout?: boolean;
463
464
  itemLayout?: boolean;
464
465
  layout?: boolean;
465
466
  }
package/react-native.js CHANGED
@@ -1217,6 +1217,67 @@ function WebAnchoredEndSpace({ horizontal }) {
1217
1217
  return /* @__PURE__ */ React2__namespace.createElement("div", { style }, null);
1218
1218
  }
1219
1219
 
1220
+ // src/core/doMaintainScrollAtEnd.ts
1221
+ function doMaintainScrollAtEnd(ctx) {
1222
+ const state = ctx.state;
1223
+ const {
1224
+ didContainersLayout,
1225
+ pendingNativeMVCPAdjust,
1226
+ refScroller,
1227
+ props: { maintainScrollAtEnd }
1228
+ } = state;
1229
+ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
1230
+ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
1231
+ if (pendingNativeMVCPAdjust) {
1232
+ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd;
1233
+ return false;
1234
+ }
1235
+ state.pendingMaintainScrollAtEnd = false;
1236
+ if (shouldMaintainScrollAtEnd) {
1237
+ const contentSize = getContentSize(ctx);
1238
+ if (contentSize < state.scrollLength) {
1239
+ state.scroll = 0;
1240
+ }
1241
+ if (!state.maintainingScrollAtEnd) {
1242
+ const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
1243
+ const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
1244
+ state.maintainingScrollAtEnd = pendingState;
1245
+ requestAnimationFrame(() => {
1246
+ if (peek$(ctx, "isWithinMaintainScrollAtEndThreshold")) {
1247
+ state.maintainingScrollAtEnd = activeState;
1248
+ const scroller = refScroller.current;
1249
+ if (state.props.horizontal && isHorizontalRTL(state)) {
1250
+ const currentContentSize = getContentSize(ctx);
1251
+ const logicalEndOffset = getLogicalHorizontalMaxOffset(state, currentContentSize);
1252
+ const nativeOffset = toNativeHorizontalOffset(state, logicalEndOffset, currentContentSize);
1253
+ scroller == null ? void 0 : scroller.scrollTo({
1254
+ animated: maintainScrollAtEnd.animated,
1255
+ x: nativeOffset,
1256
+ y: 0
1257
+ });
1258
+ } else {
1259
+ scroller == null ? void 0 : scroller.scrollToEnd({
1260
+ animated: maintainScrollAtEnd.animated
1261
+ });
1262
+ }
1263
+ setTimeout(
1264
+ () => {
1265
+ if (state.maintainingScrollAtEnd === activeState) {
1266
+ state.maintainingScrollAtEnd = void 0;
1267
+ }
1268
+ },
1269
+ maintainScrollAtEnd.animated ? 500 : 0
1270
+ );
1271
+ } else if (state.maintainingScrollAtEnd === pendingState) {
1272
+ state.maintainingScrollAtEnd = void 0;
1273
+ }
1274
+ });
1275
+ }
1276
+ return true;
1277
+ }
1278
+ return false;
1279
+ }
1280
+
1220
1281
  // src/core/calculateOffsetForIndex.ts
1221
1282
  function calculateOffsetForIndex(ctx, index) {
1222
1283
  const state = ctx.state;
@@ -1800,8 +1861,11 @@ function updateAdaptiveRender(ctx, scrollVelocity) {
1800
1861
  // src/utils/getEffectiveDrawDistance.ts
1801
1862
  var INITIAL_DRAW_DISTANCE = 100;
1802
1863
  function getEffectiveDrawDistance(ctx) {
1864
+ var _a3;
1803
1865
  const drawDistance = ctx.state.props.drawDistance;
1804
- return peek$(ctx, "readyToRender") ? drawDistance : Math.min(drawDistance, INITIAL_DRAW_DISTANCE);
1866
+ const initialScroll = ctx.state.initialScroll;
1867
+ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
1868
+ return peek$(ctx, "readyToRender") || needsFullInitialDrawDistance ? drawDistance : Math.min(drawDistance, INITIAL_DRAW_DISTANCE);
1805
1869
  }
1806
1870
 
1807
1871
  // src/utils/setInitialRenderState.ts
@@ -2184,10 +2248,12 @@ function requestAdjust(ctx, positionDiff, dataChanged) {
2184
2248
  // src/core/updateContentMetrics.ts
2185
2249
  var SCROLL_ADJUST_EPSILON = 0.1;
2186
2250
  function setContentLengthSignal(ctx, signalName, size) {
2187
- if (peek$(ctx, signalName) !== size) {
2251
+ const didChange = peek$(ctx, signalName) !== size;
2252
+ if (didChange) {
2188
2253
  set$(ctx, signalName, size);
2189
2254
  updateContentMetricsState(ctx);
2190
2255
  }
2256
+ return didChange;
2191
2257
  }
2192
2258
  function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize) {
2193
2259
  const { didContainersLayout, didFinishInitialScroll, props, scroll, scrollingTo } = ctx.state;
@@ -2211,7 +2277,7 @@ function setHeaderSize(ctx, size) {
2211
2277
  state.didMeasureHeader = true;
2212
2278
  }
2213
2279
  function setFooterSize(ctx, size) {
2214
- setContentLengthSignal(ctx, "footerSize", size);
2280
+ return setContentLengthSignal(ctx, "footerSize", size);
2215
2281
  }
2216
2282
  function areInsetsEqual(left, right) {
2217
2283
  var _a3, _b, _c, _d, _e, _f, _g, _h;
@@ -2312,14 +2378,25 @@ var ListComponent = typedMemo(function ListComponent2({
2312
2378
  );
2313
2379
  const ScrollComponent = renderScrollComponent ? CustomScrollComponent : ListComponentScrollView;
2314
2380
  const SnapOrScroll = snapToIndices ? SnapWrapper : ScrollComponent;
2381
+ const updateFooterSize = React2.useCallback(
2382
+ (size, afterSizeUpdate) => {
2383
+ var _a3;
2384
+ const didFooterSizeChange = setFooterSize(ctx, size);
2385
+ afterSizeUpdate == null ? void 0 : afterSizeUpdate();
2386
+ if (didFooterSizeChange && ((_a3 = ctx.state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onFooterLayout)) {
2387
+ doMaintainScrollAtEnd(ctx);
2388
+ }
2389
+ },
2390
+ [ctx]
2391
+ );
2315
2392
  React2.useLayoutEffect(() => {
2316
2393
  if (!ListHeaderComponent) {
2317
2394
  setHeaderSize(ctx, 0);
2318
2395
  }
2319
2396
  if (!ListFooterComponent) {
2320
- setFooterSize(ctx, 0);
2397
+ updateFooterSize(0);
2321
2398
  }
2322
- }, [ListHeaderComponent, ListFooterComponent, ctx]);
2399
+ }, [ListHeaderComponent, ListFooterComponent, ctx, updateFooterSize]);
2323
2400
  const onLayoutHeader = React2.useCallback(
2324
2401
  (rect) => {
2325
2402
  const size = rect[horizontal ? "width" : "height"];
@@ -2330,10 +2407,11 @@ var ListComponent = typedMemo(function ListComponent2({
2330
2407
  const onLayoutFooterInternal = React2.useCallback(
2331
2408
  (rect, fromLayoutEffect) => {
2332
2409
  const size = rect[horizontal ? "width" : "height"];
2333
- setFooterSize(ctx, size);
2334
- onLayoutFooter == null ? void 0 : onLayoutFooter(rect, fromLayoutEffect);
2410
+ updateFooterSize(size, () => {
2411
+ onLayoutFooter == null ? void 0 : onLayoutFooter(rect, fromLayoutEffect);
2412
+ });
2335
2413
  },
2336
- [ctx, horizontal, onLayoutFooter]
2414
+ [horizontal, onLayoutFooter, updateFooterSize]
2337
2415
  );
2338
2416
  return /* @__PURE__ */ React2__namespace.createElement(
2339
2417
  SnapOrScroll,
@@ -2438,67 +2516,6 @@ function useDevChecksNoop(_props) {
2438
2516
  }
2439
2517
  var useDevChecks = IS_DEV ? useDevChecksImpl : useDevChecksNoop;
2440
2518
 
2441
- // src/core/doMaintainScrollAtEnd.ts
2442
- function doMaintainScrollAtEnd(ctx) {
2443
- const state = ctx.state;
2444
- const {
2445
- didContainersLayout,
2446
- pendingNativeMVCPAdjust,
2447
- refScroller,
2448
- props: { maintainScrollAtEnd }
2449
- } = state;
2450
- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
2451
- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
2452
- if (pendingNativeMVCPAdjust) {
2453
- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd;
2454
- return false;
2455
- }
2456
- state.pendingMaintainScrollAtEnd = false;
2457
- if (shouldMaintainScrollAtEnd) {
2458
- const contentSize = getContentSize(ctx);
2459
- if (contentSize < state.scrollLength) {
2460
- state.scroll = 0;
2461
- }
2462
- if (!state.maintainingScrollAtEnd) {
2463
- const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
2464
- const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
2465
- state.maintainingScrollAtEnd = pendingState;
2466
- requestAnimationFrame(() => {
2467
- if (peek$(ctx, "isWithinMaintainScrollAtEndThreshold")) {
2468
- state.maintainingScrollAtEnd = activeState;
2469
- const scroller = refScroller.current;
2470
- if (state.props.horizontal && isHorizontalRTL(state)) {
2471
- const currentContentSize = getContentSize(ctx);
2472
- const logicalEndOffset = getLogicalHorizontalMaxOffset(state, currentContentSize);
2473
- const nativeOffset = toNativeHorizontalOffset(state, logicalEndOffset, currentContentSize);
2474
- scroller == null ? void 0 : scroller.scrollTo({
2475
- animated: maintainScrollAtEnd.animated,
2476
- x: nativeOffset,
2477
- y: 0
2478
- });
2479
- } else {
2480
- scroller == null ? void 0 : scroller.scrollToEnd({
2481
- animated: maintainScrollAtEnd.animated
2482
- });
2483
- }
2484
- setTimeout(
2485
- () => {
2486
- if (state.maintainingScrollAtEnd === activeState) {
2487
- state.maintainingScrollAtEnd = void 0;
2488
- }
2489
- },
2490
- maintainScrollAtEnd.animated ? 500 : 0
2491
- );
2492
- } else if (state.maintainingScrollAtEnd === pendingState) {
2493
- state.maintainingScrollAtEnd = void 0;
2494
- }
2495
- });
2496
- }
2497
- return true;
2498
- }
2499
- return false;
2500
- }
2501
-
2502
2519
  // src/core/mvcp.ts
2503
2520
  var MVCP_POSITION_EPSILON = 0.1;
2504
2521
  var MVCP_ANCHOR_LOCK_TTL_MS = 300;
@@ -2749,6 +2766,9 @@ function prepareMVCP(ctx, dataChanged) {
2749
2766
  if (diff > 0) {
2750
2767
  diff = Math.max(0, totalSize - state.scroll - state.scrollLength);
2751
2768
  } else {
2769
+ const maxScroll = Math.max(0, totalSize - state.scrollLength);
2770
+ state.scroll = maxScroll;
2771
+ state.scrollPending = maxScroll;
2752
2772
  diff = 0;
2753
2773
  }
2754
2774
  }
@@ -4956,7 +4976,7 @@ function calculateItemsInView(ctx, params = {}) {
4956
4976
  const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0;
4957
4977
  checkMVCP == null ? void 0 : checkMVCP();
4958
4978
  const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
4959
- if (didMVCPAdjustScroll && initialScroll) {
4979
+ if (didMVCPAdjustScroll && (initialScroll || state.scrollingTo)) {
4960
4980
  updateScroll2(state.scroll);
4961
4981
  updateScrollRange();
4962
4982
  }
@@ -6210,12 +6230,13 @@ function getRenderedItem(ctx, key) {
6210
6230
 
6211
6231
  // src/utils/normalizeMaintainScrollAtEnd.ts
6212
6232
  function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) {
6213
- var _a3, _b, _c;
6233
+ var _a3, _b, _c, _d;
6214
6234
  return {
6215
6235
  animated: false,
6216
6236
  onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true,
6217
- onItemLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.itemLayout) != null ? _b : false : true,
6218
- onLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.layout) != null ? _c : false : true
6237
+ onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true,
6238
+ onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true,
6239
+ onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true
6219
6240
  };
6220
6241
  }
6221
6242
  function normalizeMaintainScrollAtEnd(value) {
package/react-native.mjs CHANGED
@@ -1196,6 +1196,67 @@ function WebAnchoredEndSpace({ horizontal }) {
1196
1196
  return /* @__PURE__ */ React2.createElement("div", { style }, null);
1197
1197
  }
1198
1198
 
1199
+ // src/core/doMaintainScrollAtEnd.ts
1200
+ function doMaintainScrollAtEnd(ctx) {
1201
+ const state = ctx.state;
1202
+ const {
1203
+ didContainersLayout,
1204
+ pendingNativeMVCPAdjust,
1205
+ refScroller,
1206
+ props: { maintainScrollAtEnd }
1207
+ } = state;
1208
+ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
1209
+ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
1210
+ if (pendingNativeMVCPAdjust) {
1211
+ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd;
1212
+ return false;
1213
+ }
1214
+ state.pendingMaintainScrollAtEnd = false;
1215
+ if (shouldMaintainScrollAtEnd) {
1216
+ const contentSize = getContentSize(ctx);
1217
+ if (contentSize < state.scrollLength) {
1218
+ state.scroll = 0;
1219
+ }
1220
+ if (!state.maintainingScrollAtEnd) {
1221
+ const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
1222
+ const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
1223
+ state.maintainingScrollAtEnd = pendingState;
1224
+ requestAnimationFrame(() => {
1225
+ if (peek$(ctx, "isWithinMaintainScrollAtEndThreshold")) {
1226
+ state.maintainingScrollAtEnd = activeState;
1227
+ const scroller = refScroller.current;
1228
+ if (state.props.horizontal && isHorizontalRTL(state)) {
1229
+ const currentContentSize = getContentSize(ctx);
1230
+ const logicalEndOffset = getLogicalHorizontalMaxOffset(state, currentContentSize);
1231
+ const nativeOffset = toNativeHorizontalOffset(state, logicalEndOffset, currentContentSize);
1232
+ scroller == null ? void 0 : scroller.scrollTo({
1233
+ animated: maintainScrollAtEnd.animated,
1234
+ x: nativeOffset,
1235
+ y: 0
1236
+ });
1237
+ } else {
1238
+ scroller == null ? void 0 : scroller.scrollToEnd({
1239
+ animated: maintainScrollAtEnd.animated
1240
+ });
1241
+ }
1242
+ setTimeout(
1243
+ () => {
1244
+ if (state.maintainingScrollAtEnd === activeState) {
1245
+ state.maintainingScrollAtEnd = void 0;
1246
+ }
1247
+ },
1248
+ maintainScrollAtEnd.animated ? 500 : 0
1249
+ );
1250
+ } else if (state.maintainingScrollAtEnd === pendingState) {
1251
+ state.maintainingScrollAtEnd = void 0;
1252
+ }
1253
+ });
1254
+ }
1255
+ return true;
1256
+ }
1257
+ return false;
1258
+ }
1259
+
1199
1260
  // src/core/calculateOffsetForIndex.ts
1200
1261
  function calculateOffsetForIndex(ctx, index) {
1201
1262
  const state = ctx.state;
@@ -1779,8 +1840,11 @@ function updateAdaptiveRender(ctx, scrollVelocity) {
1779
1840
  // src/utils/getEffectiveDrawDistance.ts
1780
1841
  var INITIAL_DRAW_DISTANCE = 100;
1781
1842
  function getEffectiveDrawDistance(ctx) {
1843
+ var _a3;
1782
1844
  const drawDistance = ctx.state.props.drawDistance;
1783
- return peek$(ctx, "readyToRender") ? drawDistance : Math.min(drawDistance, INITIAL_DRAW_DISTANCE);
1845
+ const initialScroll = ctx.state.initialScroll;
1846
+ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
1847
+ return peek$(ctx, "readyToRender") || needsFullInitialDrawDistance ? drawDistance : Math.min(drawDistance, INITIAL_DRAW_DISTANCE);
1784
1848
  }
1785
1849
 
1786
1850
  // src/utils/setInitialRenderState.ts
@@ -2163,10 +2227,12 @@ function requestAdjust(ctx, positionDiff, dataChanged) {
2163
2227
  // src/core/updateContentMetrics.ts
2164
2228
  var SCROLL_ADJUST_EPSILON = 0.1;
2165
2229
  function setContentLengthSignal(ctx, signalName, size) {
2166
- if (peek$(ctx, signalName) !== size) {
2230
+ const didChange = peek$(ctx, signalName) !== size;
2231
+ if (didChange) {
2167
2232
  set$(ctx, signalName, size);
2168
2233
  updateContentMetricsState(ctx);
2169
2234
  }
2235
+ return didChange;
2170
2236
  }
2171
2237
  function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize) {
2172
2238
  const { didContainersLayout, didFinishInitialScroll, props, scroll, scrollingTo } = ctx.state;
@@ -2190,7 +2256,7 @@ function setHeaderSize(ctx, size) {
2190
2256
  state.didMeasureHeader = true;
2191
2257
  }
2192
2258
  function setFooterSize(ctx, size) {
2193
- setContentLengthSignal(ctx, "footerSize", size);
2259
+ return setContentLengthSignal(ctx, "footerSize", size);
2194
2260
  }
2195
2261
  function areInsetsEqual(left, right) {
2196
2262
  var _a3, _b, _c, _d, _e, _f, _g, _h;
@@ -2291,14 +2357,25 @@ var ListComponent = typedMemo(function ListComponent2({
2291
2357
  );
2292
2358
  const ScrollComponent = renderScrollComponent ? CustomScrollComponent : ListComponentScrollView;
2293
2359
  const SnapOrScroll = snapToIndices ? SnapWrapper : ScrollComponent;
2360
+ const updateFooterSize = useCallback(
2361
+ (size, afterSizeUpdate) => {
2362
+ var _a3;
2363
+ const didFooterSizeChange = setFooterSize(ctx, size);
2364
+ afterSizeUpdate == null ? void 0 : afterSizeUpdate();
2365
+ if (didFooterSizeChange && ((_a3 = ctx.state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onFooterLayout)) {
2366
+ doMaintainScrollAtEnd(ctx);
2367
+ }
2368
+ },
2369
+ [ctx]
2370
+ );
2294
2371
  useLayoutEffect(() => {
2295
2372
  if (!ListHeaderComponent) {
2296
2373
  setHeaderSize(ctx, 0);
2297
2374
  }
2298
2375
  if (!ListFooterComponent) {
2299
- setFooterSize(ctx, 0);
2376
+ updateFooterSize(0);
2300
2377
  }
2301
- }, [ListHeaderComponent, ListFooterComponent, ctx]);
2378
+ }, [ListHeaderComponent, ListFooterComponent, ctx, updateFooterSize]);
2302
2379
  const onLayoutHeader = useCallback(
2303
2380
  (rect) => {
2304
2381
  const size = rect[horizontal ? "width" : "height"];
@@ -2309,10 +2386,11 @@ var ListComponent = typedMemo(function ListComponent2({
2309
2386
  const onLayoutFooterInternal = useCallback(
2310
2387
  (rect, fromLayoutEffect) => {
2311
2388
  const size = rect[horizontal ? "width" : "height"];
2312
- setFooterSize(ctx, size);
2313
- onLayoutFooter == null ? void 0 : onLayoutFooter(rect, fromLayoutEffect);
2389
+ updateFooterSize(size, () => {
2390
+ onLayoutFooter == null ? void 0 : onLayoutFooter(rect, fromLayoutEffect);
2391
+ });
2314
2392
  },
2315
- [ctx, horizontal, onLayoutFooter]
2393
+ [horizontal, onLayoutFooter, updateFooterSize]
2316
2394
  );
2317
2395
  return /* @__PURE__ */ React2.createElement(
2318
2396
  SnapOrScroll,
@@ -2417,67 +2495,6 @@ function useDevChecksNoop(_props) {
2417
2495
  }
2418
2496
  var useDevChecks = IS_DEV ? useDevChecksImpl : useDevChecksNoop;
2419
2497
 
2420
- // src/core/doMaintainScrollAtEnd.ts
2421
- function doMaintainScrollAtEnd(ctx) {
2422
- const state = ctx.state;
2423
- const {
2424
- didContainersLayout,
2425
- pendingNativeMVCPAdjust,
2426
- refScroller,
2427
- props: { maintainScrollAtEnd }
2428
- } = state;
2429
- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
2430
- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
2431
- if (pendingNativeMVCPAdjust) {
2432
- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd;
2433
- return false;
2434
- }
2435
- state.pendingMaintainScrollAtEnd = false;
2436
- if (shouldMaintainScrollAtEnd) {
2437
- const contentSize = getContentSize(ctx);
2438
- if (contentSize < state.scrollLength) {
2439
- state.scroll = 0;
2440
- }
2441
- if (!state.maintainingScrollAtEnd) {
2442
- const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
2443
- const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
2444
- state.maintainingScrollAtEnd = pendingState;
2445
- requestAnimationFrame(() => {
2446
- if (peek$(ctx, "isWithinMaintainScrollAtEndThreshold")) {
2447
- state.maintainingScrollAtEnd = activeState;
2448
- const scroller = refScroller.current;
2449
- if (state.props.horizontal && isHorizontalRTL(state)) {
2450
- const currentContentSize = getContentSize(ctx);
2451
- const logicalEndOffset = getLogicalHorizontalMaxOffset(state, currentContentSize);
2452
- const nativeOffset = toNativeHorizontalOffset(state, logicalEndOffset, currentContentSize);
2453
- scroller == null ? void 0 : scroller.scrollTo({
2454
- animated: maintainScrollAtEnd.animated,
2455
- x: nativeOffset,
2456
- y: 0
2457
- });
2458
- } else {
2459
- scroller == null ? void 0 : scroller.scrollToEnd({
2460
- animated: maintainScrollAtEnd.animated
2461
- });
2462
- }
2463
- setTimeout(
2464
- () => {
2465
- if (state.maintainingScrollAtEnd === activeState) {
2466
- state.maintainingScrollAtEnd = void 0;
2467
- }
2468
- },
2469
- maintainScrollAtEnd.animated ? 500 : 0
2470
- );
2471
- } else if (state.maintainingScrollAtEnd === pendingState) {
2472
- state.maintainingScrollAtEnd = void 0;
2473
- }
2474
- });
2475
- }
2476
- return true;
2477
- }
2478
- return false;
2479
- }
2480
-
2481
2498
  // src/core/mvcp.ts
2482
2499
  var MVCP_POSITION_EPSILON = 0.1;
2483
2500
  var MVCP_ANCHOR_LOCK_TTL_MS = 300;
@@ -2728,6 +2745,9 @@ function prepareMVCP(ctx, dataChanged) {
2728
2745
  if (diff > 0) {
2729
2746
  diff = Math.max(0, totalSize - state.scroll - state.scrollLength);
2730
2747
  } else {
2748
+ const maxScroll = Math.max(0, totalSize - state.scrollLength);
2749
+ state.scroll = maxScroll;
2750
+ state.scrollPending = maxScroll;
2731
2751
  diff = 0;
2732
2752
  }
2733
2753
  }
@@ -4935,7 +4955,7 @@ function calculateItemsInView(ctx, params = {}) {
4935
4955
  const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0;
4936
4956
  checkMVCP == null ? void 0 : checkMVCP();
4937
4957
  const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
4938
- if (didMVCPAdjustScroll && initialScroll) {
4958
+ if (didMVCPAdjustScroll && (initialScroll || state.scrollingTo)) {
4939
4959
  updateScroll2(state.scroll);
4940
4960
  updateScrollRange();
4941
4961
  }
@@ -6189,12 +6209,13 @@ function getRenderedItem(ctx, key) {
6189
6209
 
6190
6210
  // src/utils/normalizeMaintainScrollAtEnd.ts
6191
6211
  function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) {
6192
- var _a3, _b, _c;
6212
+ var _a3, _b, _c, _d;
6193
6213
  return {
6194
6214
  animated: false,
6195
6215
  onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true,
6196
- onItemLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.itemLayout) != null ? _b : false : true,
6197
- onLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.layout) != null ? _c : false : true
6216
+ onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true,
6217
+ onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true,
6218
+ onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true
6198
6219
  };
6199
6220
  }
6200
6221
  function normalizeMaintainScrollAtEnd(value) {
@@ -483,6 +483,7 @@ interface AlwaysRenderConfig {
483
483
  }
484
484
  interface MaintainScrollAtEndOnOptions {
485
485
  dataChange?: boolean;
486
+ footerLayout?: boolean;
486
487
  itemLayout?: boolean;
487
488
  layout?: boolean;
488
489
  }