@legendapp/list 3.0.2 → 3.0.4

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,15 @@
1
+ ## 3.0.4
2
+
3
+ - Fix: scrollToEnd now waits for newly committed data before targeting the final item, improving chat-style append-and-scroll flows.
4
+ - Fix: Anchored end space waits for measured or fixed tail sizes before reporting readiness, avoiding stale end-space values during append flows.
5
+ - Feat: Add anchoredEndSpace.onReady to notify when the anchored tail has authoritative sizing.
6
+
7
+ ## 3.0.3
8
+
9
+ - Fix: MVCP was getting batched to improve big jumps, but was making scroll worse
10
+ - Fix: On native, ignore one-physical-pixel layout measurement noise, preventing unnecessary item size updates from Fabric and native onLayout rounding differences.
11
+ - Fix: Average item sizes update correctly when getFixedItemSize returns undefined for only some item types.
12
+
1
13
  ## 3.0.2
2
14
 
3
15
  - Fix: Using viewability was causing scrolling to end to sometimes not update items in view if the JS thread was slammed
package/animated.d.ts CHANGED
@@ -388,12 +388,22 @@ interface MaintainVisibleContentPositionConfig<ItemT = any> {
388
388
  size?: boolean;
389
389
  shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean;
390
390
  }
391
+ interface AnchoredEndSpaceReadyInfo {
392
+ anchorIndex: number | undefined;
393
+ anchorKey: string | undefined;
394
+ size: number;
395
+ }
396
+ interface ScrollToEndOptions {
397
+ animated?: boolean;
398
+ viewOffset?: number;
399
+ }
391
400
  interface AnchoredEndSpaceConfig {
392
401
  anchorIndex: number;
393
402
  anchorOffset?: number;
394
403
  anchorMaxSize?: number;
395
404
  includeInEndInset?: boolean;
396
405
  onSizeChanged?: (size: number) => void;
406
+ onReady?: (info: AnchoredEndSpaceReadyInfo) => void;
397
407
  }
398
408
  interface StickyHeaderConfig {
399
409
  /**
@@ -525,10 +535,7 @@ type LegendListRef$1 = {
525
535
  * @param options.animated - If true, animates the scroll. Default: true.
526
536
  * @param options.viewOffset - Offset from the target position.
527
537
  */
528
- scrollToEnd(options?: {
529
- animated?: boolean | undefined;
530
- viewOffset?: number | undefined;
531
- }): Promise<void>;
538
+ scrollToEnd(options?: ScrollToEndOptions): Promise<void>;
532
539
  /**
533
540
  * Scrolls to a specific index in the list.
534
541
  * @param params - Parameters for scrolling.
@@ -63,6 +63,10 @@ interface Insets {
63
63
  bottom: number;
64
64
  right: number;
65
65
  }
66
+ interface ScrollToEndOptions {
67
+ animated?: boolean;
68
+ viewOffset?: number;
69
+ }
66
70
  interface LegendListAverageItemSize {
67
71
  average: number;
68
72
  count: number;
@@ -141,10 +145,7 @@ type LegendListRef$1 = {
141
145
  * @param options.animated - If true, animates the scroll. Default: true.
142
146
  * @param options.viewOffset - Offset from the target position.
143
147
  */
144
- scrollToEnd(options?: {
145
- animated?: boolean | undefined;
146
- viewOffset?: number | undefined;
147
- }): Promise<void>;
148
+ scrollToEnd(options?: ScrollToEndOptions): Promise<void>;
148
149
  /**
149
150
  * Scrolls to a specific index in the list.
150
151
  * @param params - Parameters for scrolling.
package/keyboard.d.ts CHANGED
@@ -64,12 +64,22 @@ interface Insets {
64
64
  bottom: number;
65
65
  right: number;
66
66
  }
67
+ interface AnchoredEndSpaceReadyInfo {
68
+ anchorIndex: number | undefined;
69
+ anchorKey: string | undefined;
70
+ size: number;
71
+ }
72
+ interface ScrollToEndOptions {
73
+ animated?: boolean;
74
+ viewOffset?: number;
75
+ }
67
76
  interface AnchoredEndSpaceConfig$1 {
68
77
  anchorIndex: number;
69
78
  anchorOffset?: number;
70
79
  anchorMaxSize?: number;
71
80
  includeInEndInset?: boolean;
72
81
  onSizeChanged?: (size: number) => void;
82
+ onReady?: (info: AnchoredEndSpaceReadyInfo) => void;
73
83
  }
74
84
  interface LegendListAverageItemSize {
75
85
  average: number;
@@ -149,10 +159,7 @@ type LegendListRef$1 = {
149
159
  * @param options.animated - If true, animates the scroll. Default: true.
150
160
  * @param options.viewOffset - Offset from the target position.
151
161
  */
152
- scrollToEnd(options?: {
153
- animated?: boolean | undefined;
154
- viewOffset?: number | undefined;
155
- }): Promise<void>;
162
+ scrollToEnd(options?: ScrollToEndOptions): Promise<void>;
156
163
  /**
157
164
  * Scrolls to a specific index in the list.
158
165
  * @param params - Parameters for scrolling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/list",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
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
@@ -394,12 +394,22 @@ interface MaintainVisibleContentPositionConfig<ItemT = any> {
394
394
  size?: boolean;
395
395
  shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean;
396
396
  }
397
+ interface AnchoredEndSpaceReadyInfo {
398
+ anchorIndex: number | undefined;
399
+ anchorKey: string | undefined;
400
+ size: number;
401
+ }
402
+ interface ScrollToEndOptions {
403
+ animated?: boolean;
404
+ viewOffset?: number;
405
+ }
397
406
  interface AnchoredEndSpaceConfig {
398
407
  anchorIndex: number;
399
408
  anchorOffset?: number;
400
409
  anchorMaxSize?: number;
401
410
  includeInEndInset?: boolean;
402
411
  onSizeChanged?: (size: number) => void;
412
+ onReady?: (info: AnchoredEndSpaceReadyInfo) => void;
403
413
  }
404
414
  interface StickyHeaderConfig {
405
415
  /**
@@ -531,10 +541,7 @@ type LegendListRef$1 = {
531
541
  * @param options.animated - If true, animates the scroll. Default: true.
532
542
  * @param options.viewOffset - Offset from the target position.
533
543
  */
534
- scrollToEnd(options?: {
535
- animated?: boolean | undefined;
536
- viewOffset?: number | undefined;
537
- }): Promise<void>;
544
+ scrollToEnd(options?: ScrollToEndOptions): Promise<void>;
538
545
  /**
539
546
  * Scrolls to a specific index in the list.
540
547
  * @param params - Parameters for scrolling.
package/react-native.js CHANGED
@@ -743,6 +743,22 @@ function Separator({ ItemSeparatorComponent, leadingItem }) {
743
743
  const isLastItem = useIsLastItem();
744
744
  return isLastItem ? null : /* @__PURE__ */ React2__namespace.createElement(ItemSeparatorComponent, { leadingItem });
745
745
  }
746
+ var PixelRatio = ReactNative.PixelRatio;
747
+
748
+ // src/utils/layoutMeasurement.ts
749
+ var FLOATING_POINT_SLACK = 0.01;
750
+ var NATIVE_LAYOUT_MEASUREMENT_EPSILON = 1 / PixelRatio.get() + FLOATING_POINT_SLACK;
751
+ function isWithinEpsilon(delta) {
752
+ return Math.abs(delta) <= NATIVE_LAYOUT_MEASUREMENT_EPSILON;
753
+ }
754
+ function isNativeLayoutNoise(delta) {
755
+ return isWithinEpsilon(delta);
756
+ }
757
+ function isNativeLayoutSizeNoise(heightDelta, widthDelta) {
758
+ return isWithinEpsilon(heightDelta) && isWithinEpsilon(widthDelta);
759
+ }
760
+
761
+ // src/hooks/useOnLayoutSync.native.tsx
746
762
  function useOnLayoutSync({
747
763
  ref,
748
764
  onLayoutProp,
@@ -751,11 +767,21 @@ function useOnLayoutSync({
751
767
  const lastLayoutRef = React2.useRef(null);
752
768
  const onLayout = React2.useCallback(
753
769
  (event) => {
754
- var _a3, _b;
755
770
  const { layout } = event.nativeEvent;
756
- if (layout.height !== ((_a3 = lastLayoutRef.current) == null ? void 0 : _a3.height) || layout.width !== ((_b = lastLayoutRef.current) == null ? void 0 : _b.width)) {
771
+ const lastLayout = lastLayoutRef.current;
772
+ const didLayoutSizeChange = lastLayout && (layout.height !== lastLayout.height || layout.width !== lastLayout.width);
773
+ const isMeasuredLayoutNoise = !!didLayoutSizeChange && !!lastLayout.measuredLayout && isNativeLayoutSizeNoise(
774
+ layout.height - lastLayout.measuredLayout.height,
775
+ layout.width - lastLayout.measuredLayout.width
776
+ );
777
+ if (!lastLayout || didLayoutSizeChange && !isMeasuredLayoutNoise) {
757
778
  onLayoutChange(layout, false);
758
- lastLayoutRef.current = layout;
779
+ }
780
+ if (!lastLayout || didLayoutSizeChange) {
781
+ lastLayoutRef.current = {
782
+ ...layout,
783
+ measuredLayout: isMeasuredLayoutNoise ? lastLayout == null ? void 0 : lastLayout.measuredLayout : void 0
784
+ };
759
785
  }
760
786
  onLayoutProp == null ? void 0 : onLayoutProp(event);
761
787
  },
@@ -766,7 +792,7 @@ function useOnLayoutSync({
766
792
  if (ref.current) {
767
793
  ref.current.measure((x, y, width, height) => {
768
794
  const layout = { height, width, x, y };
769
- lastLayoutRef.current = layout;
795
+ lastLayoutRef.current = { ...layout, measuredLayout: layout };
770
796
  onLayoutChange(layout, true);
771
797
  });
772
798
  }
@@ -1805,17 +1831,42 @@ function setSize(ctx, itemKey, size, notifyTotalSize = true) {
1805
1831
  }
1806
1832
 
1807
1833
  // src/utils/getItemSize.ts
1834
+ function getKnownOrFixedSize(ctx, key, index, data) {
1835
+ var _a3;
1836
+ const state = ctx.state;
1837
+ const { getFixedItemSize, getItemType } = state.props;
1838
+ let size = key ? state.sizesKnown.get(key) : void 0;
1839
+ if (size === void 0 && key && getFixedItemSize) {
1840
+ const itemType = getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
1841
+ size = getFixedItemSize(data, index, itemType);
1842
+ if (size !== void 0) {
1843
+ state.sizesKnown.set(key, size);
1844
+ }
1845
+ }
1846
+ return size;
1847
+ }
1848
+ function getKnownOrFixedItemSize(ctx, index) {
1849
+ const key = getId(ctx.state, index);
1850
+ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
1851
+ }
1852
+ function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
1853
+ for (let index = startIndex; index <= endIndex; index++) {
1854
+ if (getKnownOrFixedItemSize(ctx, index) === void 0) {
1855
+ return false;
1856
+ }
1857
+ }
1858
+ return true;
1859
+ }
1808
1860
  function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize) {
1809
1861
  var _a3, _b, _c;
1810
1862
  const state = ctx.state;
1811
1863
  const {
1812
- sizesKnown,
1813
1864
  sizes,
1814
1865
  averageSizes,
1815
- props: { estimatedItemSize, getFixedItemSize, getItemType },
1866
+ props: { estimatedItemSize, getItemType },
1816
1867
  scrollingTo
1817
1868
  } = state;
1818
- const sizeKnown = sizesKnown.get(key);
1869
+ const sizeKnown = state.sizesKnown.get(key);
1819
1870
  if (sizeKnown !== void 0) {
1820
1871
  return sizeKnown;
1821
1872
  }
@@ -1826,14 +1877,13 @@ function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, no
1826
1877
  return renderedSize;
1827
1878
  }
1828
1879
  }
1829
- const itemType = getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
1830
- if (getFixedItemSize) {
1831
- size = getFixedItemSize(data, index, itemType);
1832
- if (size !== void 0) {
1833
- sizesKnown.set(key, size);
1834
- }
1880
+ size = getKnownOrFixedSize(ctx, key, index, data);
1881
+ if (size !== void 0) {
1882
+ setSize(ctx, key, size, notifyTotalSize);
1883
+ return size;
1835
1884
  }
1836
- if (size === void 0 && useAverageSize && sizeKnown === void 0 && !scrollingTo) {
1885
+ const itemType = getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
1886
+ if (useAverageSize && !scrollingTo) {
1837
1887
  const averageSizeForType = (_b = averageSizes[itemType]) == null ? void 0 : _b.avg;
1838
1888
  if (averageSizeForType !== void 0) {
1839
1889
  size = roundSize(averageSizeForType);
@@ -1842,7 +1892,7 @@ function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, no
1842
1892
  if (size === void 0 && renderedSize !== void 0) {
1843
1893
  return renderedSize;
1844
1894
  }
1845
- if (size === void 0 && useAverageSize && sizeKnown === void 0 && scrollingTo) {
1895
+ if (size === void 0 && useAverageSize && scrollingTo) {
1846
1896
  const averageSizeForType = (_c = scrollingTo.averageSizeSnapshot) == null ? void 0 : _c[itemType];
1847
1897
  if (averageSizeForType !== void 0) {
1848
1898
  size = roundSize(averageSizeForType);
@@ -2595,7 +2645,7 @@ function updateScroll(ctx, newScroll, forceUpdate, options) {
2595
2645
  if (scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust) {
2596
2646
  state.mvcpAnchorLock = void 0;
2597
2647
  state.pendingNativeMVCPAdjust = void 0;
2598
- state.userScrollAnchorResetKeys = /* @__PURE__ */ new Set();
2648
+ state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() };
2599
2649
  if (state.queuedMVCPRecalculate !== void 0) {
2600
2650
  cancelAnimationFrame(state.queuedMVCPRecalculate);
2601
2651
  state.queuedMVCPRecalculate = void 0;
@@ -4413,7 +4463,7 @@ function handleStickyRecycling(ctx, stickyArray, scroll, drawDistance, currentSt
4413
4463
  function calculateItemsInView(ctx, params = {}) {
4414
4464
  const state = ctx.state;
4415
4465
  batchedUpdates(() => {
4416
- var _a3, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
4466
+ var _a3, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
4417
4467
  const {
4418
4468
  columns,
4419
4469
  containerItemKeys,
@@ -4727,7 +4777,7 @@ function calculateItemsInView(ctx, params = {}) {
4727
4777
  state.containerItemTypes.set(containerIndex, requiredItemTypes[idx]);
4728
4778
  }
4729
4779
  containerItemKeys.set(id, containerIndex);
4730
- (_o = state.userScrollAnchorResetKeys) == null ? void 0 : _o.add(id);
4780
+ (_o = state.userScrollAnchorReset) == null ? void 0 : _o.keys.add(id);
4731
4781
  const containerSticky = `containerSticky${containerIndex}`;
4732
4782
  const isSticky = stickyHeaderIndicesSet.has(i);
4733
4783
  const isAlwaysRender = alwaysRenderSet.has(i);
@@ -4755,13 +4805,17 @@ function calculateItemsInView(ctx, params = {}) {
4755
4805
  }
4756
4806
  }
4757
4807
  }
4758
- if (((_p = state.userScrollAnchorResetKeys) == null ? void 0 : _p.size) === 0) {
4759
- state.userScrollAnchorResetKeys = void 0;
4808
+ if (state.userScrollAnchorReset) {
4809
+ if (state.userScrollAnchorReset.keys.size === 0) {
4810
+ state.userScrollAnchorReset = void 0;
4811
+ } else {
4812
+ state.userScrollAnchorReset.batchSize = state.userScrollAnchorReset.keys.size;
4813
+ }
4760
4814
  }
4761
4815
  if (alwaysRenderArr.length > 0) {
4762
4816
  for (const index of alwaysRenderArr) {
4763
4817
  if (index < 0 || index >= dataLength) continue;
4764
- const id = (_q = idCache[index]) != null ? _q : getId(state, index);
4818
+ const id = (_p = idCache[index]) != null ? _p : getId(state, index);
4765
4819
  const containerIndex = containerItemKeys.get(id);
4766
4820
  if (containerIndex !== void 0) {
4767
4821
  state.stickyContainerPool.add(containerIndex);
@@ -5159,22 +5213,27 @@ var ScrollAdjustHandler = class {
5159
5213
 
5160
5214
  // src/core/updateAnchoredEndSpace.ts
5161
5215
  function maybeUpdateAnchoredEndSpace(ctx) {
5162
- var _a3;
5216
+ var _a3, _b;
5163
5217
  const state = ctx.state;
5164
5218
  const anchoredEndSpace = state.props.anchoredEndSpace;
5165
5219
  const previousSize = peek$(ctx, "anchoredEndSpaceSize");
5220
+ const previousReadyAnchorIndex = state.anchoredEndSpaceReadyAnchorIndex;
5221
+ const previousReadyAnchorKey = state.anchoredEndSpaceReadyAnchorKey;
5222
+ const nextAnchorIndex = anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex;
5223
+ let nextAnchorKey;
5224
+ let isReady = true;
5166
5225
  let nextSize = 0;
5167
5226
  if (anchoredEndSpace) {
5168
5227
  const { anchorIndex, anchorMaxSize, anchorOffset = 0 } = anchoredEndSpace;
5169
5228
  const { data } = state.props;
5170
5229
  if (anchorIndex >= 0 && anchorIndex < data.length && state.scrollLength > 0) {
5230
+ nextAnchorKey = getId(state, anchorIndex);
5171
5231
  let contentBelowAnchor = 0;
5172
5232
  const footerSize = ctx.values.get("footerSize") || 0;
5173
5233
  const stylePaddingBottom = state.props.stylePaddingBottom || 0;
5174
5234
  let hasUnknownTailSize = false;
5175
5235
  for (let index = anchorIndex; index < data.length; index++) {
5176
- const itemKey = getId(state, index);
5177
- const size = itemKey ? state.sizesKnown.get(itemKey) : void 0;
5236
+ const size = getKnownOrFixedItemSize(ctx, index);
5178
5237
  const effectiveSize = index === anchorIndex && anchorMaxSize !== void 0 ? Math.min(size || 0, Math.max(0, anchorMaxSize)) : size;
5179
5238
  if (size === void 0) {
5180
5239
  hasUnknownTailSize = true;
@@ -5184,15 +5243,25 @@ function maybeUpdateAnchoredEndSpace(ctx) {
5184
5243
  }
5185
5244
  }
5186
5245
  contentBelowAnchor += footerSize + stylePaddingBottom;
5246
+ isReady = !hasUnknownTailSize;
5187
5247
  nextSize = hasUnknownTailSize ? previousSize || 0 : Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset);
5248
+ } else if (anchorIndex >= 0) {
5249
+ isReady = false;
5188
5250
  }
5189
5251
  }
5190
- if (previousSize !== nextSize) {
5191
- set$(ctx, "anchoredEndSpaceSize", nextSize);
5192
- (_a3 = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onSizeChanged) == null ? void 0 : _a3.call(anchoredEndSpace, nextSize);
5193
- if (anchoredEndSpace == null ? void 0 : anchoredEndSpace.includeInEndInset) {
5252
+ const didSizeChange = previousSize !== nextSize;
5253
+ const didReadyAnchorChange = previousReadyAnchorIndex !== nextAnchorIndex || previousReadyAnchorKey !== nextAnchorKey;
5254
+ if (isReady && (didSizeChange || didReadyAnchorChange)) {
5255
+ state.anchoredEndSpaceReadyAnchorIndex = nextAnchorIndex;
5256
+ state.anchoredEndSpaceReadyAnchorKey = nextAnchorKey;
5257
+ if (didSizeChange) {
5258
+ set$(ctx, "anchoredEndSpaceSize", nextSize);
5259
+ (_a3 = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onSizeChanged) == null ? void 0 : _a3.call(anchoredEndSpace, nextSize);
5260
+ }
5261
+ if (didSizeChange && (anchoredEndSpace == null ? void 0 : anchoredEndSpace.includeInEndInset)) {
5194
5262
  updateScroll(ctx, state.scroll, true);
5195
5263
  }
5264
+ (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });
5196
5265
  }
5197
5266
  return nextSize;
5198
5267
  }
@@ -5215,37 +5284,42 @@ function updateContentInsetEndAdjustment(ctx, previousContentInsetEndAdjustment)
5215
5284
 
5216
5285
  // src/core/updateItemSize.ts
5217
5286
  function runOrScheduleMVCPRecalculate(ctx) {
5287
+ var _a3, _b;
5218
5288
  const state = ctx.state;
5219
- if (state.userScrollAnchorResetKeys !== void 0) {
5220
- if (state.queuedMVCPRecalculate !== void 0) {
5221
- return;
5222
- }
5223
- state.queuedMVCPRecalculate = requestAnimationFrame(() => {
5224
- var _a3;
5225
- state.queuedMVCPRecalculate = void 0;
5289
+ if (state.userScrollAnchorReset !== void 0) {
5290
+ const replacementBatchSize = (_a3 = state.userScrollAnchorReset.batchSize) != null ? _a3 : state.userScrollAnchorReset.keys.size;
5291
+ const replacementMeasurementBatchThreshold = 3;
5292
+ const shouldBatchReplacementMeasurements = replacementBatchSize > replacementMeasurementBatchThreshold;
5293
+ if (shouldBatchReplacementMeasurements) {
5294
+ if (state.queuedMVCPRecalculate === void 0) {
5295
+ state.queuedMVCPRecalculate = requestAnimationFrame(() => {
5296
+ var _a4;
5297
+ state.queuedMVCPRecalculate = void 0;
5298
+ calculateItemsInView(ctx);
5299
+ if (((_a4 = state.userScrollAnchorReset) == null ? void 0 : _a4.keys.size) === 0) {
5300
+ state.userScrollAnchorReset = void 0;
5301
+ }
5302
+ });
5303
+ }
5304
+ } else {
5226
5305
  calculateItemsInView(ctx);
5227
- if (((_a3 = state.userScrollAnchorResetKeys) == null ? void 0 : _a3.size) === 0) {
5228
- state.userScrollAnchorResetKeys = void 0;
5306
+ if (((_b = state.userScrollAnchorReset) == null ? void 0 : _b.keys.size) === 0) {
5307
+ state.userScrollAnchorReset = void 0;
5229
5308
  }
5230
- });
5231
- return;
5232
- }
5233
- if (Platform.OS === "web") {
5309
+ }
5310
+ } else if (Platform.OS === "web") {
5234
5311
  if (!state.mvcpAnchorLock) {
5235
5312
  if (state.queuedMVCPRecalculate !== void 0) {
5236
5313
  cancelAnimationFrame(state.queuedMVCPRecalculate);
5237
5314
  state.queuedMVCPRecalculate = void 0;
5238
5315
  }
5239
5316
  calculateItemsInView(ctx, { doMVCP: true });
5240
- return;
5241
- }
5242
- if (state.queuedMVCPRecalculate !== void 0) {
5243
- return;
5317
+ } else if (state.queuedMVCPRecalculate === void 0) {
5318
+ state.queuedMVCPRecalculate = requestAnimationFrame(() => {
5319
+ state.queuedMVCPRecalculate = void 0;
5320
+ calculateItemsInView(ctx, { doMVCP: true });
5321
+ });
5244
5322
  }
5245
- state.queuedMVCPRecalculate = requestAnimationFrame(() => {
5246
- state.queuedMVCPRecalculate = void 0;
5247
- calculateItemsInView(ctx, { doMVCP: true });
5248
- });
5249
5323
  } else {
5250
5324
  calculateItemsInView(ctx, { doMVCP: true });
5251
5325
  }
@@ -5263,8 +5337,8 @@ function updateOtherAxisSizeIfNeeded(ctx, sizeObj, horizontal) {
5263
5337
  function updateItemSize(ctx, itemKey, sizeObj) {
5264
5338
  var _a3;
5265
5339
  const state = ctx.state;
5266
- const userScrollAnchorResetKeys = state.userScrollAnchorResetKeys;
5267
- const didMeasureUserScrollAnchorResetItem = !!(userScrollAnchorResetKeys == null ? void 0 : userScrollAnchorResetKeys.delete(itemKey));
5340
+ const userScrollAnchorReset = state.userScrollAnchorReset;
5341
+ const didMeasureUserScrollAnchorResetItem = !!(userScrollAnchorReset == null ? void 0 : userScrollAnchorReset.keys.delete(itemKey));
5268
5342
  const {
5269
5343
  didContainersLayout,
5270
5344
  sizesKnown,
@@ -5320,8 +5394,8 @@ function updateItemSize(ctx, itemKey, sizeObj) {
5320
5394
  if (needsRecalculate) {
5321
5395
  state.scrollForNextCalculateItemsInView = void 0;
5322
5396
  runOrScheduleMVCPRecalculate(ctx);
5323
- } else if (didMeasureUserScrollAnchorResetItem && (userScrollAnchorResetKeys == null ? void 0 : userScrollAnchorResetKeys.size) === 0) {
5324
- state.userScrollAnchorResetKeys = void 0;
5397
+ } else if (didMeasureUserScrollAnchorResetItem && (userScrollAnchorReset == null ? void 0 : userScrollAnchorReset.keys.size) === 0) {
5398
+ state.userScrollAnchorReset = void 0;
5325
5399
  }
5326
5400
  if (shouldMaintainScrollAtEnd) {
5327
5401
  if (maintainScrollAtEnd == null ? void 0 : maintainScrollAtEnd.onItemLayout) {
@@ -5331,7 +5405,7 @@ function updateItemSize(ctx, itemKey, sizeObj) {
5331
5405
  }
5332
5406
  }
5333
5407
  function updateOneItemSize(ctx, itemKey, sizeObj) {
5334
- var _a3;
5408
+ var _a3, _b;
5335
5409
  const state = ctx.state;
5336
5410
  const {
5337
5411
  indexByKey,
@@ -5341,13 +5415,23 @@ function updateOneItemSize(ctx, itemKey, sizeObj) {
5341
5415
  } = state;
5342
5416
  if (!data) return 0;
5343
5417
  const index = indexByKey.get(itemKey);
5344
- const prevSize = getItemSize(ctx, itemKey, index, data[index]);
5418
+ const itemData = data[index];
5419
+ let itemType;
5420
+ let fixedItemSize;
5421
+ if (getFixedItemSize) {
5422
+ itemType = getItemType ? (_a3 = getItemType(itemData, index)) != null ? _a3 : "" : "";
5423
+ fixedItemSize = getFixedItemSize(itemData, index, itemType);
5424
+ }
5425
+ const prevSize = getItemSize(ctx, itemKey, index, itemData);
5345
5426
  const rawSize = horizontal ? sizeObj.width : sizeObj.height;
5346
- const size = Platform.OS === "web" ? Math.round(rawSize) : roundSize(rawSize);
5347
5427
  const prevSizeKnown = sizesKnown.get(itemKey);
5428
+ if (Platform.OS !== "web" && prevSizeKnown !== void 0 && isNativeLayoutNoise(rawSize - prevSizeKnown)) {
5429
+ return 0;
5430
+ }
5431
+ const size = Platform.OS === "web" ? Math.round(rawSize) : roundSize(rawSize);
5348
5432
  sizesKnown.set(itemKey, size);
5349
- if (!getFixedItemSize && size > 0) {
5350
- const itemType = getItemType ? (_a3 = getItemType(data[index], index)) != null ? _a3 : "" : "";
5433
+ if (fixedItemSize === void 0 && size > 0) {
5434
+ itemType != null ? itemType : itemType = getItemType ? (_b = getItemType(itemData, index)) != null ? _b : "" : "";
5351
5435
  let averages = averageSizes[itemType];
5352
5436
  if (!averages) {
5353
5437
  averages = averageSizes[itemType] = { avg: 0, num: 0 };
@@ -5428,6 +5512,25 @@ function createColumnWrapperStyle(contentContainerStyle) {
5428
5512
  }
5429
5513
  }
5430
5514
 
5515
+ // src/core/scrollToEnd.ts
5516
+ function scrollToEnd(ctx, options) {
5517
+ const state = ctx.state;
5518
+ const data = state.props.data;
5519
+ const index = data.length - 1;
5520
+ if (index === -1) {
5521
+ return false;
5522
+ }
5523
+ const paddingBottom = state.props.stylePaddingBottom || 0;
5524
+ const footerSize = peek$(ctx, "footerSize") || 0;
5525
+ scrollToIndex(ctx, {
5526
+ ...options,
5527
+ index,
5528
+ viewOffset: -paddingBottom - footerSize + ((options == null ? void 0 : options.viewOffset) || 0),
5529
+ viewPosition: 1
5530
+ });
5531
+ return true;
5532
+ }
5533
+
5431
5534
  // src/utils/createImperativeHandle.ts
5432
5535
  var DEFAULT_AVERAGE_ITEM_SIZE_TYPE = "default";
5433
5536
  function getAverageItemSizes(state) {
@@ -5443,7 +5546,7 @@ function getAverageItemSizes(state) {
5443
5546
  }
5444
5547
  return averageItemSizes;
5445
5548
  }
5446
- function createImperativeHandle(ctx) {
5549
+ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
5447
5550
  const state = ctx.state;
5448
5551
  const IMPERATIVE_SCROLL_SETTLE_MAX_WAIT_MS = 800;
5449
5552
  const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2;
@@ -5460,15 +5563,10 @@ function createImperativeHandle(ctx) {
5460
5563
  if (targetIndex >= dataLength) {
5461
5564
  return false;
5462
5565
  }
5463
- if (anchorIndex === void 0 || anchorIndex < 0 || anchorIndex >= dataLength || targetIndex < anchorIndex || props.getFixedItemSize) {
5566
+ if (anchorIndex === void 0 || anchorIndex < 0 || anchorIndex >= dataLength || targetIndex < anchorIndex) {
5464
5567
  return true;
5465
5568
  }
5466
- for (let index = anchorIndex; index < dataLength; index++) {
5467
- if (!state.sizesKnown.has(getId(state, index))) {
5468
- return false;
5469
- }
5470
- }
5471
- return true;
5569
+ return areKnownOrFixedItemSizesAvailable(ctx, anchorIndex, dataLength - 1);
5472
5570
  };
5473
5571
  const runWhenReady = (token, run, isReady) => {
5474
5572
  const startedAt = Date.now();
@@ -5491,11 +5589,7 @@ function createImperativeHandle(ctx) {
5491
5589
  };
5492
5590
  requestAnimationFrame(check);
5493
5591
  };
5494
- const runScrollWithPromise = (run, isReady = () => true) => new Promise((resolve) => {
5495
- var _a3;
5496
- const token = ++imperativeScrollToken;
5497
- (_a3 = state.pendingScrollResolve) == null ? void 0 : _a3.call(state);
5498
- state.pendingScrollResolve = resolve;
5592
+ const runScrollRequest = (token, resolve, run, isReady = () => true) => {
5499
5593
  const runNow = () => {
5500
5594
  if (token !== imperativeScrollToken) {
5501
5595
  return;
@@ -5513,7 +5607,33 @@ function createImperativeHandle(ctx) {
5513
5607
  } else {
5514
5608
  runNow();
5515
5609
  }
5610
+ };
5611
+ const startImperativeScroll = (resolve) => {
5612
+ var _a3;
5613
+ const token = ++imperativeScrollToken;
5614
+ state.pendingScrollToEnd = void 0;
5615
+ (_a3 = state.pendingScrollResolve) == null ? void 0 : _a3.call(state);
5616
+ state.pendingScrollResolve = resolve;
5617
+ return token;
5618
+ };
5619
+ const runScrollWithPromise = (run, isReady = () => true) => new Promise((resolve) => {
5620
+ const token = startImperativeScroll(resolve);
5621
+ runScrollRequest(token, resolve, run, isReady);
5516
5622
  });
5623
+ state.runPendingScrollToEnd = () => {
5624
+ const pendingScroll = state.pendingScrollToEnd;
5625
+ if (pendingScroll) {
5626
+ state.pendingScrollToEnd = void 0;
5627
+ if (pendingScroll.token === imperativeScrollToken) {
5628
+ runScrollRequest(
5629
+ pendingScroll.token,
5630
+ pendingScroll.resolve,
5631
+ () => scrollToEnd(ctx, pendingScroll.options),
5632
+ () => isScrollToIndexReady(state.props.data.length - 1, true)
5633
+ );
5634
+ }
5635
+ }
5636
+ };
5517
5637
  const scrollIndexIntoView = (options) => {
5518
5638
  if (state) {
5519
5639
  const { index, ...rest } = options;
@@ -5612,26 +5732,20 @@ function createImperativeHandle(ctx) {
5612
5732
  }
5613
5733
  return false;
5614
5734
  }),
5615
- scrollToEnd: (options) => runScrollWithPromise(
5616
- () => {
5617
- const data = state.props.data;
5618
- const stylePaddingBottom = state.props.stylePaddingBottom;
5619
- const index = data.length - 1;
5620
- if (index !== -1) {
5621
- const paddingBottom = stylePaddingBottom || 0;
5622
- const footerSize = peek$(ctx, "footerSize") || 0;
5623
- scrollToIndex(ctx, {
5624
- ...options,
5625
- index,
5626
- viewOffset: -paddingBottom - footerSize + ((options == null ? void 0 : options.viewOffset) || 0),
5627
- viewPosition: 1
5628
- });
5629
- return true;
5630
- }
5631
- return false;
5632
- },
5633
- () => isScrollToIndexReady(state.props.data.length - 1, true)
5634
- ),
5735
+ scrollToEnd: (options) => new Promise((resolve) => {
5736
+ var _a3;
5737
+ const token = startImperativeScroll(resolve);
5738
+ state.pendingScrollToEnd = {
5739
+ options,
5740
+ resolve,
5741
+ token
5742
+ };
5743
+ if (scheduleImperativeScrollCommit) {
5744
+ scheduleImperativeScrollCommit();
5745
+ } else {
5746
+ (_a3 = state.runPendingScrollToEnd) == null ? void 0 : _a3.call(state);
5747
+ }
5748
+ }),
5635
5749
  scrollToIndex: (params) => {
5636
5750
  return runScrollWithPromise(
5637
5751
  () => {
@@ -6001,6 +6115,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
6001
6115
  viewOffset: 0
6002
6116
  } : void 0;
6003
6117
  const [canRender, setCanRender] = React2__namespace.useState(!IsNewArchitecture);
6118
+ const [, scheduleImperativeScrollCommit] = React2__namespace.useReducer((value) => value + 1, 0);
6004
6119
  const ctx = useStateContext();
6005
6120
  ctx.columnWrapperStyle = columnWrapperStyle || (contentContainerStyle ? createColumnWrapperStyle(contentContainerStyle) : void 0);
6006
6121
  const refScroller = React2.useRef(null);
@@ -6373,7 +6488,11 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
6373
6488
  doInitialAllocateContainers(ctx);
6374
6489
  }
6375
6490
  });
6376
- React2.useImperativeHandle(forwardedRef, () => createImperativeHandle(ctx), []);
6491
+ React2.useImperativeHandle(forwardedRef, () => createImperativeHandle(ctx, scheduleImperativeScrollCommit), []);
6492
+ React2.useLayoutEffect(() => {
6493
+ var _a4;
6494
+ (_a4 = state.runPendingScrollToEnd) == null ? void 0 : _a4.call(state);
6495
+ });
6377
6496
  React2.useEffect(() => {
6378
6497
  if (Platform.OS !== "web" || usesBootstrapInitialScroll) {
6379
6498
  return;