@legendapp/list 3.4.0 → 3.5.0

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,3 +1,9 @@
1
+ ## 3.5.0
2
+
3
+ - Feat: Add `scrollElement` so web lists can share an ancestor element's scrollbar
4
+ - Fix: Stopping scrolling keeps rows prepared in the last scroll direction instead of shifting the render buffer backward.
5
+ - Perf: Single-column lists do less layout work during initial rendering and scrolling when item sizes and data are unchanged.
6
+
1
7
  ## 3.4.0
2
8
 
3
9
  - Feat: Add `onReady`, called after each initial placement and scroll cycle completes, including cycles restarted by a `dataKey` change.
package/README.md CHANGED
@@ -112,6 +112,39 @@ export default LegendListExample
112
112
 
113
113
  ---
114
114
 
115
+ ### Sharing a scrollbar on the web
116
+
117
+ React DOM lists can use an ancestor element's scrollbar with `scrollElement`:
118
+
119
+ ```tsx
120
+ import { LegendList } from "@legendapp/list/react";
121
+ import { useState } from "react";
122
+
123
+ function History({ swaps, renderSwap }) {
124
+ const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(null);
125
+
126
+ return (
127
+ <div ref={setScrollElement} style={{ height: 600, overflow: "auto" }}>
128
+ <ProfileHeader />
129
+ <LegendList
130
+ data={swaps}
131
+ renderItem={renderSwap}
132
+ keyExtractor={(swap) => swap.id}
133
+ scrollElement={scrollElement}
134
+ />
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ Use a state-backed callback ref so the list receives the element after mounting and whenever it changes. `null` waits for an external element; `undefined` keeps the default list-owned scrollbar. The element must be an ancestor of the list. Keep the list's own height unconstrained so its content contributes to the parent's scroll range.
141
+
142
+ Multiple lists can share the same element, including lists in separate columns. Each uses the owner's viewport on the scroll axis and its own width/height on the cross axis. Content above the list is accounted for automatically, including preceding content that resizes. `scrollToIndex`, `scrollToOffset`, and `scrollToEnd` remain list-relative and move the shared owner.
143
+
144
+ `maintainVisibleContentPosition` anchors changes to the list's own rows; it does not freeze the layout of surrounding content. An offscreen list does not pull the owner back into view to apply a measurement correction.
145
+
146
+ `useWindowScroll` continues to use the browser window. Both modes share the same web implementation; `scrollElement` takes precedence when provided. As with window scrolling, external mode does not configure the owner's scrollbar styling or CSS snapping. It is not supported with `renderScrollComponent`.
147
+
115
148
  ## How to Build
116
149
 
117
150
  1. `bun i`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/list",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
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.js CHANGED
@@ -935,8 +935,14 @@ function scheduleFullDrawDistancePrewarm(ctx) {
935
935
  return;
936
936
  }
937
937
  state.scheduledWork.frame(() => {
938
- var _a3;
939
- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
938
+ var _a3, _b, _c;
939
+ if (state.props.hasExternalScroll && !hasActiveInitialScroll(state) && !state.scrollingTo && !state.pendingNativeMVCPAdjust) {
940
+ const offset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getRawScrollOffset) == null ? void 0 : _b.call(_a3);
941
+ if (offset !== void 0) {
942
+ state.scroll = state.scrollPending = state.props.horizontal ? toLogicalHorizontalOffset(state, offset, state.totalSize) : offset;
943
+ }
944
+ }
945
+ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state);
940
946
  }, "fullDrawDistancePrewarm");
941
947
  }
942
948
 
@@ -949,6 +955,9 @@ function resetInitialRenderState(ctx, {
949
955
  if (resetLayout) {
950
956
  state.didContainersLayout = false;
951
957
  state.queuedInitialLayout = false;
958
+ state.scrollBufferDirection = void 0;
959
+ state.scrollHistory.length = 0;
960
+ state.scheduledWork.cancel("renderRangeProjection");
952
961
  state.edgeReachedGate = void 0;
953
962
  state.isStartReached = null;
954
963
  state.isEndReached = null;
@@ -1177,6 +1186,7 @@ function setSize(ctx, itemKey, size, notifyTotalSize = true) {
1177
1186
  const previousSize = sizes.get(itemKey);
1178
1187
  const diff = previousSize !== void 0 ? size - previousSize : size;
1179
1188
  if (diff !== 0) {
1189
+ state.positionsAreCurrent = false;
1180
1190
  addTotalSize(ctx, itemKey, diff, notifyTotalSize);
1181
1191
  }
1182
1192
  sizes.set(itemKey, size);
@@ -1237,6 +1247,7 @@ function getKnownOrFixedSize(ctx, key, index, data, resolved) {
1237
1247
  const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
1238
1248
  if (fixedSize !== void 0) {
1239
1249
  size = fixedSize + ctx.scrollAxisGap;
1250
+ state.positionsAreCurrent = false;
1240
1251
  state.sizesKnown.set(key, size);
1241
1252
  }
1242
1253
  }
@@ -1850,6 +1861,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) {
1850
1861
  state.scrollTime = currentTime;
1851
1862
  const scrollDelta = Math.abs(newScroll - prevScroll);
1852
1863
  const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust;
1864
+ if (isUserScrollEvent) {
1865
+ state.scrollBufferDirection = newScroll > prevScroll ? 1 : -1;
1866
+ }
1853
1867
  const didCancelMaintainScrollAtEnd = Platform.OS === "web" && isUserScrollEvent && newScroll < prevScroll && !!(state.maintainingScrollAtEnd || state.pendingMaintainScrollAtEnd);
1854
1868
  if (didCancelMaintainScrollAtEnd) {
1855
1869
  finishMaintainScrollAtEnd(ctx);
@@ -1929,10 +1943,10 @@ function finishMaintainScrollAtEnd(ctx) {
1929
1943
  }
1930
1944
  }
1931
1945
  function doMaintainScrollAtEnd(ctx, options) {
1932
- var _a3, _b, _c, _d, _e, _f;
1946
+ var _a3, _b, _c, _d, _e, _f, _g, _h;
1933
1947
  const state = ctx.state;
1934
1948
  let didMaintain = false;
1935
- if (state.isDragging) {
1949
+ if (state.isDragging || ((_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.isScrollInRange) == null ? void 0 : _b.call(_a3)) === false) {
1936
1950
  finishMaintainScrollAtEnd(ctx);
1937
1951
  } else {
1938
1952
  const {
@@ -1944,11 +1958,11 @@ function doMaintainScrollAtEnd(ctx, options) {
1944
1958
  const immediate = (options == null ? void 0 : options.immediate) && Platform.OS === "web" && !(maintainScrollAtEnd == null ? void 0 : maintainScrollAtEnd.animated);
1945
1959
  const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
1946
1960
  const isReplayingPendingMaintain = !!state.pendingMaintainScrollAtEnd;
1947
- const shouldMaintainScrollAtEnd = !!(!((_a3 = ctx.scrollRequestTracker) == null ? void 0 : _a3.isWaitingToRun()) && (!state.scrollingTo || state.scrollingTo.isScrollToEnd) && (isWithinMaintainScrollAtEndThreshold || isReplayingPendingMaintain || ((_b = state.scrollingTo) == null ? void 0 : _b.isScrollToEnd)) && maintainScrollAtEnd && didFinishInitialScroll);
1961
+ const shouldMaintainScrollAtEnd = !!(!((_c = ctx.scrollRequestTracker) == null ? void 0 : _c.isWaitingToRun()) && (!state.scrollingTo || state.scrollingTo.isScrollToEnd) && (isWithinMaintainScrollAtEndThreshold || isReplayingPendingMaintain || ((_d = state.scrollingTo) == null ? void 0 : _d.isScrollToEnd)) && maintainScrollAtEnd && didFinishInitialScroll);
1948
1962
  if (shouldMaintainScrollAtEnd && !state.scrollingTo) {
1949
1963
  const scroller = state.refScroller.current;
1950
- const nativeMax = (_c = scroller == null ? void 0 : scroller.getMaxScrollOffset) == null ? void 0 : _c.call(scroller);
1951
- const observed = (_d = scroller == null ? void 0 : scroller.getCurrentScrollOffset) == null ? void 0 : _d.call(scroller);
1964
+ const nativeMax = (_e = scroller == null ? void 0 : scroller.getMaxScrollOffset) == null ? void 0 : _e.call(scroller);
1965
+ const observed = (_f = scroller == null ? void 0 : scroller.getCurrentScrollOffset) == null ? void 0 : _f.call(scroller);
1952
1966
  if (nativeMax !== void 0 && observed !== void 0 && nativeMax < state.scroll - 1 && Math.abs(observed - nativeMax) <= 1) {
1953
1967
  state.pendingMaintainScrollAtEnd = true;
1954
1968
  state.scrollPending = observed;
@@ -1960,13 +1974,13 @@ function doMaintainScrollAtEnd(ctx, options) {
1960
1974
  } else if (pendingNativeMVCPAdjust) {
1961
1975
  state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd;
1962
1976
  } else if (shouldMaintainScrollAtEnd && (state.scrollingTo || state.pendingScrollResolve)) {
1963
- if (immediate && ((_e = state.scrollingTo) == null ? void 0 : _e.isScrollToEnd) && !state.scrollingTo.animated) {
1977
+ if (immediate && ((_g = state.scrollingTo) == null ? void 0 : _g.isScrollToEnd) && !state.scrollingTo.animated) {
1964
1978
  state.pendingMaintainScrollAtEnd = false;
1965
1979
  ctx.scrollToEnd({ animated: false });
1966
1980
  didMaintain = true;
1967
1981
  } else {
1968
1982
  state.pendingMaintainScrollAtEnd = true;
1969
- (_f = state.maintainingScrollAtEnd) != null ? _f : state.maintainingScrollAtEnd = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
1983
+ (_h = state.maintainingScrollAtEnd) != null ? _h : state.maintainingScrollAtEnd = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
1970
1984
  }
1971
1985
  } else if (shouldMaintainScrollAtEnd && didContainersLayout) {
1972
1986
  state.pendingMaintainScrollAtEnd = false;
@@ -1980,8 +1994,9 @@ function doMaintainScrollAtEnd(ctx, options) {
1980
1994
  const scrollAtRequest = state.scroll;
1981
1995
  state.maintainingScrollAtEnd = pendingState;
1982
1996
  const run = () => {
1997
+ var _a4, _b2;
1983
1998
  if (state.maintainingScrollAtEnd === pendingState) {
1984
- if (state.isDragging) {
1999
+ if (state.isDragging || ((_b2 = (_a4 = state.refScroller.current) == null ? void 0 : _a4.isScrollInRange) == null ? void 0 : _b2.call(_a4)) === false) {
1985
2000
  finishMaintainScrollAtEnd(ctx);
1986
2001
  } else {
1987
2002
  const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
@@ -3319,6 +3334,7 @@ function handleInitialScrollDataChange(ctx, options) {
3319
3334
 
3320
3335
  // src/core/resetLayoutCachesForDataChange.ts
3321
3336
  function resetLayoutCachesForDataChange(state) {
3337
+ state.positionsAreCurrent = false;
3322
3338
  state.indexByKey.clear();
3323
3339
  state.idCache.length = 0;
3324
3340
  state.positions.length = 0;
@@ -3599,7 +3615,7 @@ function updateItemPositions(ctx, dataChanged, {
3599
3615
  scrollBottomBuffered: -1,
3600
3616
  startIndex: 0
3601
3617
  }) {
3602
- var _a3, _b, _c, _d, _e;
3618
+ var _a3, _b, _c, _d, _e, _f;
3603
3619
  const state = ctx.state;
3604
3620
  const hasPositionListeners = ctx.positionListeners.size > 0;
3605
3621
  const {
@@ -3615,6 +3631,11 @@ function updateItemPositions(ctx, dataChanged, {
3615
3631
  const dataLength = data.length;
3616
3632
  const numColumns = (_a3 = peek$(ctx, "numColumns")) != null ? _a3 : 1;
3617
3633
  const hasColumns = numColumns > 1;
3634
+ const canReuseEstimates = !doMVCP || !state.didContainersLayout && Object.keys(state.averageSizes).length === 0;
3635
+ if (state.positionsAreCurrent && !dataChanged && !forceFullUpdate && state.positionRecalculationStartIndex === void 0 && !scrollingTo && !hasColumns && !snapToIndices && positions.length === dataLength && canReuseEstimates) {
3636
+ return;
3637
+ }
3638
+ state.positionsAreCurrent = false;
3618
3639
  const indexByKeyForChecking = IS_DEV ? /* @__PURE__ */ new Map() : void 0;
3619
3640
  const extraData = peek$(ctx, "extraData");
3620
3641
  const layoutConfig = overrideItemLayout ? { span: 1 } : void 0;
@@ -3729,6 +3750,10 @@ function updateItemPositions(ctx, dataChanged, {
3729
3750
  }
3730
3751
  if (!didBreakEarly) {
3731
3752
  updateTotalSize(ctx);
3753
+ state.positionsAreCurrent = !hasColumns;
3754
+ if (startIndex <= ((_f = state.positionRecalculationStartIndex) != null ? _f : -1)) {
3755
+ state.positionRecalculationStartIndex = void 0;
3756
+ }
3732
3757
  }
3733
3758
  if (snapToIndices) {
3734
3759
  updateSnapToOffsets(ctx);
@@ -4291,7 +4316,7 @@ function handleStickyRecycling(ctx, stickyArray, scroll, drawDistance, currentSt
4291
4316
  }
4292
4317
  function trackVisibleRange(range, i, top, size, scroll, scrollBottom) {
4293
4318
  let didPassVisibleEnd = false;
4294
- if (range.startNoBuffer === null && top + size > scroll) {
4319
+ if (range.startNoBuffer === null && top + size > scroll && top <= scrollBottom) {
4295
4320
  range.startNoBuffer = i;
4296
4321
  }
4297
4322
  if (range.firstFullyOnScreenIndex === void 0 && top >= scroll - 10 && top <= scrollBottom) {
@@ -4357,7 +4382,7 @@ function findFirstVisibleIndexInCachedRange(ctx, scroll) {
4357
4382
  return null;
4358
4383
  }
4359
4384
  function updateViewabilityForCachedRange(ctx, viewabilityConfigCallbackPairs, scrollLength, scroll, scrollBottom) {
4360
- var _a3, _b;
4385
+ var _a3, _b, _c, _d;
4361
4386
  const state = ctx.state;
4362
4387
  const {
4363
4388
  endBuffered,
@@ -4391,14 +4416,14 @@ function updateViewabilityForCachedRange(ctx, viewabilityConfigCallbackPairs, sc
4391
4416
  startNoBuffer: visibleRange.startNoBuffer
4392
4417
  });
4393
4418
  maybeEmitFirstVisibleItemChanged(state, visibleRange.startNoBuffer);
4394
- if (visibleRange.startNoBuffer !== null && visibleRange.endNoBuffer !== null) {
4419
+ if (state.props.hasExternalScroll || visibleRange.startNoBuffer !== null && visibleRange.endNoBuffer !== null) {
4395
4420
  updateViewableItems(
4396
4421
  state,
4397
4422
  ctx,
4398
4423
  viewabilityConfigCallbackPairs,
4399
4424
  scrollLength,
4400
- visibleRange.startNoBuffer,
4401
- visibleRange.endNoBuffer,
4425
+ (_c = visibleRange.startNoBuffer) != null ? _c : 0,
4426
+ (_d = visibleRange.endNoBuffer) != null ? _d : -1,
4402
4427
  startBuffered,
4403
4428
  endBuffered
4404
4429
  );
@@ -4407,7 +4432,7 @@ function updateViewabilityForCachedRange(ctx, viewabilityConfigCallbackPairs, sc
4407
4432
  function calculateItemsInView(ctx, params = {}) {
4408
4433
  const state = ctx.state;
4409
4434
  batchedUpdates(() => {
4410
- var _a3, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u;
4435
+ var _a3, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
4411
4436
  const {
4412
4437
  columns,
4413
4438
  containerItemKeys,
@@ -4459,7 +4484,7 @@ function calculateItemsInView(ctx, params = {}) {
4459
4484
  scrollAdjustPending = (_a4 = peek$(ctx, "scrollAdjustPending")) != null ? _a4 : 0;
4460
4485
  scrollAdjustPad = scrollAdjustPending - topPad;
4461
4486
  scroll = Math.round(nextScrollState + scrollExtra + scrollAdjustPad);
4462
- if (scroll + scrollLength > totalSize) {
4487
+ if (!state.props.hasExternalScroll && scroll + scrollLength > totalSize) {
4463
4488
  scroll = Math.max(0, totalSize - scrollLength);
4464
4489
  }
4465
4490
  };
@@ -4486,7 +4511,11 @@ function calculateItemsInView(ctx, params = {}) {
4486
4511
  let stickyState = dataChanged ? void 0 : resolveStickyState();
4487
4512
  let scrollBufferTop = drawDistance;
4488
4513
  let scrollBufferBottom = drawDistance;
4489
- if (speed > 0 || speed === 0 && scroll < Math.max(50, drawDistance)) {
4514
+ if (speed !== 0) {
4515
+ state.scrollBufferDirection = speed > 0 ? 1 : -1;
4516
+ }
4517
+ const bufferForward = state.scrollBufferDirection === 1 || state.scrollBufferDirection === void 0 && scroll < Math.max(50, drawDistance);
4518
+ if (bufferForward) {
4490
4519
  scrollBufferTop = drawDistance * 0.5;
4491
4520
  scrollBufferBottom = drawDistance * 1.5;
4492
4521
  } else {
@@ -4496,8 +4525,8 @@ function calculateItemsInView(ctx, params = {}) {
4496
4525
  const shouldProjectRenderRange = !dataChanged && !forceFullItemPositions && !suppressInitialScrollSideEffects && !hasActiveInitialScroll(state) && !state.scrollingTo && !state.pendingNativeMVCPAdjust && !!peek$(ctx, "readyToRender");
4497
4526
  const projectedBufferAdjustment = shouldProjectRenderRange ? getProjectedBufferAdjustment(speed, Math.min(scrollBufferTop, scrollBufferBottom)) : 0;
4498
4527
  const updateScrollRange = () => {
4499
- const scrollStart = Math.max(0, scroll);
4500
- const overscrollBeforeContent = Math.max(0, -nativeScrollState);
4528
+ const scrollStart = state.props.hasExternalScroll ? scroll : Math.max(0, scroll);
4529
+ const overscrollBeforeContent = state.props.hasExternalScroll ? 0 : Math.max(0, -nativeScrollState);
4501
4530
  scrollBottom = Math.max(scrollStart, scroll + scrollLength + overscrollBeforeContent);
4502
4531
  scrollTopBuffered = scrollStart - scrollBufferTop + projectedBufferAdjustment;
4503
4532
  scrollBottomBuffered = scrollBottom + scrollBufferBottom + projectedBufferAdjustment;
@@ -4626,7 +4655,7 @@ function calculateItemsInView(ctx, params = {}) {
4626
4655
  nextTop = top;
4627
4656
  }
4628
4657
  }
4629
- if (visibleRange.startNoBuffer !== null) {
4658
+ if (startBuffered !== null) {
4630
4659
  if (top <= scrollBottomBuffered) {
4631
4660
  endBuffered = i;
4632
4661
  if (scrollBottomBuffered > totalSize) {
@@ -4637,6 +4666,8 @@ function calculateItemsInView(ctx, params = {}) {
4637
4666
  } else {
4638
4667
  foundEnd = true;
4639
4668
  }
4669
+ } else if (top > scrollBottomBuffered) {
4670
+ foundEnd = true;
4640
4671
  }
4641
4672
  }
4642
4673
  }
@@ -4654,6 +4685,12 @@ function calculateItemsInView(ctx, params = {}) {
4654
4685
  bottom: nextBottom,
4655
4686
  top: nextTop
4656
4687
  };
4688
+ } else if (enableScrollForNextCalculateItemsInView && state.props.hasExternalScroll) {
4689
+ if (scrollBottomBuffered < 0) {
4690
+ state.scrollForNextCalculateItemsInView = { bottom: 0, top: null };
4691
+ } else if (scrollTopBuffered > totalSize) {
4692
+ state.scrollForNextCalculateItemsInView = { bottom: null, top: totalSize };
4693
+ }
4657
4694
  }
4658
4695
  let numContainers = prevNumContainers;
4659
4696
  const pendingRemoval = [];
@@ -4677,7 +4714,9 @@ function calculateItemsInView(ctx, params = {}) {
4677
4714
  }
4678
4715
  const hasScrollTargetPinnedRange = scrollTargetPinnedStart <= scrollTargetPinnedEnd;
4679
4716
  const isPinnedRenderIndex = (index) => alwaysRenderIndicesSet.has(index) || hasScrollTargetPinnedRange && index >= scrollTargetPinnedStart && index <= scrollTargetPinnedEnd;
4680
- if (startBuffered !== null && endBuffered !== null) {
4717
+ if (startBuffered !== null && endBuffered !== null || alwaysRenderIndicesArr.length || hasScrollTargetPinnedRange) {
4718
+ const allocationStart = startBuffered != null ? startBuffered : 0;
4719
+ const allocationEnd = endBuffered != null ? endBuffered : -1;
4681
4720
  const needNewContainers = [];
4682
4721
  const needNewContainersSet = /* @__PURE__ */ new Set();
4683
4722
  const addPinnedIndex = (index) => {
@@ -4693,7 +4732,7 @@ function calculateItemsInView(ctx, params = {}) {
4693
4732
  }
4694
4733
  }
4695
4734
  };
4696
- for (let i = startBuffered; i <= endBuffered; i++) {
4735
+ for (let i = allocationStart; i <= allocationEnd; i++) {
4697
4736
  const id = (_m = idCache[i]) != null ? _m : getId(state, i);
4698
4737
  if (!containerItemKeys.has(id)) {
4699
4738
  needNewContainersSet.add(i);
@@ -4715,8 +4754,8 @@ function calculateItemsInView(ctx, params = {}) {
4715
4754
  (_n = stickyState == null ? void 0 : stickyState.currentStickyIdx) != null ? _n : -1,
4716
4755
  needNewContainers,
4717
4756
  needNewContainersSet,
4718
- startBuffered,
4719
- endBuffered
4757
+ allocationStart,
4758
+ allocationEnd
4720
4759
  );
4721
4760
  } else if (previousStickyIndex !== -1) {
4722
4761
  set$(ctx, "activeStickyIndex", -1);
@@ -4725,8 +4764,8 @@ function calculateItemsInView(ctx, params = {}) {
4725
4764
  const availableContainerAllocations = findAvailableContainers(
4726
4765
  ctx,
4727
4766
  needNewContainers,
4728
- startBuffered,
4729
- endBuffered,
4767
+ allocationStart,
4768
+ allocationEnd,
4730
4769
  pendingRemoval,
4731
4770
  protectedContainerKeys,
4732
4771
  // Initial end alignment has no scroll velocity yet. Keep its
@@ -4853,19 +4892,19 @@ function calculateItemsInView(ctx, params = {}) {
4853
4892
  handleInitialScrollLayoutReady(ctx);
4854
4893
  }
4855
4894
  }
4856
- if (viewabilityConfigCallbackPairs && visibleRange.startNoBuffer !== null && visibleRange.endNoBuffer !== null) {
4895
+ if (viewabilityConfigCallbackPairs && (state.props.hasExternalScroll || visibleRange.startNoBuffer !== null && visibleRange.endNoBuffer !== null)) {
4857
4896
  updateViewableItems(
4858
4897
  ctx.state,
4859
4898
  ctx,
4860
4899
  viewabilityConfigCallbackPairs,
4861
4900
  scrollLength,
4862
- visibleRange.startNoBuffer,
4863
- visibleRange.endNoBuffer,
4864
- startBuffered != null ? startBuffered : visibleRange.startNoBuffer,
4865
- endBuffered != null ? endBuffered : visibleRange.endNoBuffer
4901
+ (_u = visibleRange.startNoBuffer) != null ? _u : 0,
4902
+ (_v = visibleRange.endNoBuffer) != null ? _v : -1,
4903
+ (_w = startBuffered != null ? startBuffered : visibleRange.startNoBuffer) != null ? _w : 0,
4904
+ (_x = endBuffered != null ? endBuffered : visibleRange.endNoBuffer) != null ? _x : -1
4866
4905
  );
4867
4906
  }
4868
- (_u = stickyState == null ? void 0 : stickyState.finishCalculateItemsInView) == null ? void 0 : _u.call(stickyState);
4907
+ (_y = stickyState == null ? void 0 : stickyState.finishCalculateItemsInView) == null ? void 0 : _y.call(stickyState);
4869
4908
  });
4870
4909
  }
4871
4910
 
@@ -5160,6 +5199,7 @@ function updateOneItemSize(ctx, itemKey, sizeObj, resolvedMeasurementItem, notif
5160
5199
  return 0;
5161
5200
  }
5162
5201
  const size = Platform.OS === "web" ? Math.round(rawSize) : roundSize(rawSize);
5202
+ if (prevSizeKnown !== size) state.positionsAreCurrent = false;
5163
5203
  sizesKnown.set(itemKey, size);
5164
5204
  if (fixedItemSize === void 0 && size > 0) {
5165
5205
  itemType != null ? itemType : itemType = getItemType ? (_b = getItemType(itemData, index)) != null ? _b : "" : "";
@@ -6260,6 +6300,7 @@ var ListComponent = typedMemo(function ListComponent2({
6260
6300
  stickyHeaderConfig,
6261
6301
  stickyHeaderIndices,
6262
6302
  useWindowScroll = false,
6303
+ scrollElement,
6263
6304
  ...rest
6264
6305
  }) {
6265
6306
  const ctx = useStateContext();
@@ -6325,7 +6366,7 @@ var ListComponent = typedMemo(function ListComponent2({
6325
6366
  SnapOrScroll,
6326
6367
  {
6327
6368
  ...rest,
6328
- ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} : { onScrollBeginDrag: onInternalScrollBeginDrag, onScrollEndDrag: onInternalScrollEndDrag },
6369
+ ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, scrollElement, useWindowScroll } : {} : { onScrollBeginDrag: onInternalScrollBeginDrag, onScrollEndDrag: onInternalScrollEndDrag },
6329
6370
  contentContainerStyle: [
6330
6371
  horizontal ? { height: "100%" } : {},
6331
6372
  contentContainerStyle,
@@ -6365,7 +6406,15 @@ var WEB_UNBOUNDED_HEIGHT_CONTAINER_RATIO = 0.9;
6365
6406
  var WEB_UNBOUNDED_HEIGHT_VIEWPORT_RATIO = 0.9;
6366
6407
  function useDevChecksImpl(props) {
6367
6408
  const ctx = useStateContext();
6368
- const { anchoredEndSpace, childrenMode, keyExtractor, numColumns, renderScrollComponent, useWindowScroll } = props;
6409
+ const {
6410
+ anchoredEndSpace,
6411
+ childrenMode,
6412
+ keyExtractor,
6413
+ numColumns,
6414
+ renderScrollComponent,
6415
+ scrollElement,
6416
+ useWindowScroll
6417
+ } = props;
6369
6418
  const hasAnchoredEndSpace = !!anchoredEndSpace;
6370
6419
  React2.useEffect(() => {
6371
6420
  if (hasAnchoredEndSpace && (numColumns != null ? numColumns : 1) > 1) {
@@ -6376,13 +6425,13 @@ function useDevChecksImpl(props) {
6376
6425
  }
6377
6426
  }, [hasAnchoredEndSpace, numColumns]);
6378
6427
  React2.useEffect(() => {
6379
- if (useWindowScroll && renderScrollComponent) {
6428
+ if ((useWindowScroll || scrollElement !== void 0) && renderScrollComponent) {
6380
6429
  warnDevOnce(
6381
6430
  "useWindowScrollRenderScrollComponent",
6382
- "useWindowScroll is not supported when renderScrollComponent is provided."
6431
+ "useWindowScroll and scrollElement are not supported when renderScrollComponent is provided."
6383
6432
  );
6384
6433
  }
6385
- }, [renderScrollComponent, useWindowScroll]);
6434
+ }, [renderScrollComponent, scrollElement, useWindowScroll]);
6386
6435
  React2.useEffect(() => {
6387
6436
  if (!keyExtractor && !ctx.state.isFirst && ctx.state.didDataChange && !childrenMode) {
6388
6437
  warnDevOnce(
@@ -6394,8 +6443,8 @@ function useDevChecksImpl(props) {
6394
6443
  React2.useEffect(() => {
6395
6444
  const state = ctx.state;
6396
6445
  const dataLength = state.props.data.length;
6397
- const useWindowScrollResolved = state.props.useWindowScroll;
6398
- if (Platform.OS !== "web" || useWindowScrollResolved || dataLength < WEB_UNBOUNDED_HEIGHT_MIN_DATA_LENGTH) {
6446
+ const usesExternalScroll = state.props.hasExternalScroll;
6447
+ if (Platform.OS !== "web" || usesExternalScroll || dataLength < WEB_UNBOUNDED_HEIGHT_MIN_DATA_LENGTH) {
6399
6448
  return;
6400
6449
  }
6401
6450
  const warnIfUnboundedOuterSize = () => {
@@ -6559,7 +6608,8 @@ function doInitialAllocateContainers(ctx) {
6559
6608
  calculateItemsInView(ctx, { dataChanged: true, doMVCP: true });
6560
6609
  });
6561
6610
  } else {
6562
- calculateItemsInView(ctx, { dataChanged: true, doMVCP: true });
6611
+ const reuseInitialPositions = !state.didLoad && state.positionsAreCurrent && !state.didDataChange && !state.didColumnsChange;
6612
+ calculateItemsInView(ctx, { dataChanged: !reuseInitialPositions, doMVCP: true });
6563
6613
  }
6564
6614
  }
6565
6615
  return true;
@@ -6598,10 +6648,8 @@ function handleLayout(ctx, layoutParam, setCanRender) {
6598
6648
  updateContentMetricsState(ctx);
6599
6649
  state.lastBatchingAction = Date.now();
6600
6650
  state.scrollForNextCalculateItemsInView = void 0;
6601
- if (scrollLength > 0) {
6602
- doInitialAllocateContainers(ctx);
6603
- }
6604
- if (needsCalculate) {
6651
+ const didAllocate = scrollLength > 0 && doInitialAllocateContainers(ctx);
6652
+ if (needsCalculate && (!didAllocate || state.initialScroll)) {
6605
6653
  calculateItemsInView(ctx, { doMVCP: true });
6606
6654
  }
6607
6655
  if (didChange || otherAxisSize !== prevOtherAxisSize) {
@@ -6683,7 +6731,7 @@ function onScroll(ctx, event) {
6683
6731
  }
6684
6732
  state.lastNativeScroll = newScroll;
6685
6733
  state.lastNativeScrollTime = Date.now();
6686
- if (state.scrollingTo && state.scrollingTo.offset >= newScroll) {
6734
+ if (!state.props.hasExternalScroll && state.scrollingTo && state.scrollingTo.offset >= newScroll) {
6687
6735
  const maxOffset = clampScrollOffset(ctx, newScroll, state.scrollingTo);
6688
6736
  if (newScroll !== maxOffset && Math.abs(newScroll - maxOffset) > 1) {
6689
6737
  newScroll = maxOffset;
@@ -7466,6 +7514,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
7466
7514
  stickyHeaderIndices: stickyHeaderIndicesProp,
7467
7515
  style: styleProp,
7468
7516
  useWindowScroll = false,
7517
+ scrollElement,
7469
7518
  viewabilityConfig,
7470
7519
  viewabilityConfigCallbackPairs,
7471
7520
  ...rest
@@ -7560,7 +7609,9 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
7560
7609
  dataVersion,
7561
7610
  keyExtractor
7562
7611
  ]);
7563
- const useWindowScrollResolved = Platform.OS === "web" && !!useWindowScroll && !renderScrollComponent;
7612
+ const scrollElementResolved = Platform.OS === "web" && !renderScrollComponent ? scrollElement : void 0;
7613
+ const useWindowScrollResolved = Platform.OS === "web" && !!useWindowScroll && scrollElementResolved === void 0 && !renderScrollComponent;
7614
+ const hasExternalScroll = useWindowScrollResolved || scrollElementResolved !== void 0;
7564
7615
  const refState = React2.useRef(void 0);
7565
7616
  const hasOverrideItemLayout = !!overrideItemLayout;
7566
7617
  const prevHasOverrideItemLayout = React2.useRef(hasOverrideItemLayout);
@@ -7698,6 +7749,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
7698
7749
  estimatedItemSize,
7699
7750
  getFixedItemSize: useWrapIfItem(getFixedItemSize),
7700
7751
  getItemType: useWrapIfItem(getItemType),
7752
+ hasExternalScroll,
7701
7753
  hideItemsUntilMeasured: experimental_hideItemsUntilMeasured,
7702
7754
  horizontal: !!horizontal,
7703
7755
  itemsAreEqual,
@@ -7894,9 +7946,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
7894
7946
  const { onLayout } = useOnLayoutSync({
7895
7947
  onLayoutChange,
7896
7948
  onLayoutProp,
7897
- ref: refScroller
7898
- // the type of ScrollView doesn't include measure?
7899
- });
7949
+ ref: refScroller});
7900
7950
  React2.useLayoutEffect(() => {
7901
7951
  if (snapToIndices) {
7902
7952
  updateSnapToOffsets(ctx);
@@ -8064,6 +8114,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
8064
8114
  refScrollView: combinedRef,
8065
8115
  renderScrollComponent,
8066
8116
  scrollAdjustHandler: (_s = refState.current) == null ? void 0 : _s.scrollAdjustHandler,
8117
+ scrollElement: scrollElementResolved,
8067
8118
  scrollEventThrottle: 0,
8068
8119
  snapToIndices,
8069
8120
  stickyHeaderIndices,