@legendapp/list 3.0.3 → 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,9 @@
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
+
1
7
  ## 3.0.3
2
8
 
3
9
  - Fix: MVCP was getting batched to improve big jumps, but was making scroll worse
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.3",
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
@@ -1831,17 +1831,42 @@ function setSize(ctx, itemKey, size, notifyTotalSize = true) {
1831
1831
  }
1832
1832
 
1833
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
+ }
1834
1860
  function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize) {
1835
1861
  var _a3, _b, _c;
1836
1862
  const state = ctx.state;
1837
1863
  const {
1838
- sizesKnown,
1839
1864
  sizes,
1840
1865
  averageSizes,
1841
- props: { estimatedItemSize, getFixedItemSize, getItemType },
1866
+ props: { estimatedItemSize, getItemType },
1842
1867
  scrollingTo
1843
1868
  } = state;
1844
- const sizeKnown = sizesKnown.get(key);
1869
+ const sizeKnown = state.sizesKnown.get(key);
1845
1870
  if (sizeKnown !== void 0) {
1846
1871
  return sizeKnown;
1847
1872
  }
@@ -1852,14 +1877,13 @@ function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, no
1852
1877
  return renderedSize;
1853
1878
  }
1854
1879
  }
1855
- const itemType = getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
1856
- if (getFixedItemSize) {
1857
- size = getFixedItemSize(data, index, itemType);
1858
- if (size !== void 0) {
1859
- sizesKnown.set(key, size);
1860
- }
1880
+ size = getKnownOrFixedSize(ctx, key, index, data);
1881
+ if (size !== void 0) {
1882
+ setSize(ctx, key, size, notifyTotalSize);
1883
+ return size;
1861
1884
  }
1862
- if (size === void 0 && useAverageSize && sizeKnown === void 0 && !scrollingTo) {
1885
+ const itemType = getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
1886
+ if (useAverageSize && !scrollingTo) {
1863
1887
  const averageSizeForType = (_b = averageSizes[itemType]) == null ? void 0 : _b.avg;
1864
1888
  if (averageSizeForType !== void 0) {
1865
1889
  size = roundSize(averageSizeForType);
@@ -1868,7 +1892,7 @@ function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, no
1868
1892
  if (size === void 0 && renderedSize !== void 0) {
1869
1893
  return renderedSize;
1870
1894
  }
1871
- if (size === void 0 && useAverageSize && sizeKnown === void 0 && scrollingTo) {
1895
+ if (size === void 0 && useAverageSize && scrollingTo) {
1872
1896
  const averageSizeForType = (_c = scrollingTo.averageSizeSnapshot) == null ? void 0 : _c[itemType];
1873
1897
  if (averageSizeForType !== void 0) {
1874
1898
  size = roundSize(averageSizeForType);
@@ -5189,22 +5213,27 @@ var ScrollAdjustHandler = class {
5189
5213
 
5190
5214
  // src/core/updateAnchoredEndSpace.ts
5191
5215
  function maybeUpdateAnchoredEndSpace(ctx) {
5192
- var _a3;
5216
+ var _a3, _b;
5193
5217
  const state = ctx.state;
5194
5218
  const anchoredEndSpace = state.props.anchoredEndSpace;
5195
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;
5196
5225
  let nextSize = 0;
5197
5226
  if (anchoredEndSpace) {
5198
5227
  const { anchorIndex, anchorMaxSize, anchorOffset = 0 } = anchoredEndSpace;
5199
5228
  const { data } = state.props;
5200
5229
  if (anchorIndex >= 0 && anchorIndex < data.length && state.scrollLength > 0) {
5230
+ nextAnchorKey = getId(state, anchorIndex);
5201
5231
  let contentBelowAnchor = 0;
5202
5232
  const footerSize = ctx.values.get("footerSize") || 0;
5203
5233
  const stylePaddingBottom = state.props.stylePaddingBottom || 0;
5204
5234
  let hasUnknownTailSize = false;
5205
5235
  for (let index = anchorIndex; index < data.length; index++) {
5206
- const itemKey = getId(state, index);
5207
- const size = itemKey ? state.sizesKnown.get(itemKey) : void 0;
5236
+ const size = getKnownOrFixedItemSize(ctx, index);
5208
5237
  const effectiveSize = index === anchorIndex && anchorMaxSize !== void 0 ? Math.min(size || 0, Math.max(0, anchorMaxSize)) : size;
5209
5238
  if (size === void 0) {
5210
5239
  hasUnknownTailSize = true;
@@ -5214,15 +5243,25 @@ function maybeUpdateAnchoredEndSpace(ctx) {
5214
5243
  }
5215
5244
  }
5216
5245
  contentBelowAnchor += footerSize + stylePaddingBottom;
5246
+ isReady = !hasUnknownTailSize;
5217
5247
  nextSize = hasUnknownTailSize ? previousSize || 0 : Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset);
5248
+ } else if (anchorIndex >= 0) {
5249
+ isReady = false;
5218
5250
  }
5219
5251
  }
5220
- if (previousSize !== nextSize) {
5221
- set$(ctx, "anchoredEndSpaceSize", nextSize);
5222
- (_a3 = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onSizeChanged) == null ? void 0 : _a3.call(anchoredEndSpace, nextSize);
5223
- 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)) {
5224
5262
  updateScroll(ctx, state.scroll, true);
5225
5263
  }
5264
+ (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });
5226
5265
  }
5227
5266
  return nextSize;
5228
5267
  }
@@ -5473,6 +5512,25 @@ function createColumnWrapperStyle(contentContainerStyle) {
5473
5512
  }
5474
5513
  }
5475
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
+
5476
5534
  // src/utils/createImperativeHandle.ts
5477
5535
  var DEFAULT_AVERAGE_ITEM_SIZE_TYPE = "default";
5478
5536
  function getAverageItemSizes(state) {
@@ -5488,7 +5546,7 @@ function getAverageItemSizes(state) {
5488
5546
  }
5489
5547
  return averageItemSizes;
5490
5548
  }
5491
- function createImperativeHandle(ctx) {
5549
+ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
5492
5550
  const state = ctx.state;
5493
5551
  const IMPERATIVE_SCROLL_SETTLE_MAX_WAIT_MS = 800;
5494
5552
  const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2;
@@ -5505,15 +5563,10 @@ function createImperativeHandle(ctx) {
5505
5563
  if (targetIndex >= dataLength) {
5506
5564
  return false;
5507
5565
  }
5508
- if (anchorIndex === void 0 || anchorIndex < 0 || anchorIndex >= dataLength || targetIndex < anchorIndex || props.getFixedItemSize) {
5566
+ if (anchorIndex === void 0 || anchorIndex < 0 || anchorIndex >= dataLength || targetIndex < anchorIndex) {
5509
5567
  return true;
5510
5568
  }
5511
- for (let index = anchorIndex; index < dataLength; index++) {
5512
- if (!state.sizesKnown.has(getId(state, index))) {
5513
- return false;
5514
- }
5515
- }
5516
- return true;
5569
+ return areKnownOrFixedItemSizesAvailable(ctx, anchorIndex, dataLength - 1);
5517
5570
  };
5518
5571
  const runWhenReady = (token, run, isReady) => {
5519
5572
  const startedAt = Date.now();
@@ -5536,11 +5589,7 @@ function createImperativeHandle(ctx) {
5536
5589
  };
5537
5590
  requestAnimationFrame(check);
5538
5591
  };
5539
- const runScrollWithPromise = (run, isReady = () => true) => new Promise((resolve) => {
5540
- var _a3;
5541
- const token = ++imperativeScrollToken;
5542
- (_a3 = state.pendingScrollResolve) == null ? void 0 : _a3.call(state);
5543
- state.pendingScrollResolve = resolve;
5592
+ const runScrollRequest = (token, resolve, run, isReady = () => true) => {
5544
5593
  const runNow = () => {
5545
5594
  if (token !== imperativeScrollToken) {
5546
5595
  return;
@@ -5558,7 +5607,33 @@ function createImperativeHandle(ctx) {
5558
5607
  } else {
5559
5608
  runNow();
5560
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);
5561
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
+ };
5562
5637
  const scrollIndexIntoView = (options) => {
5563
5638
  if (state) {
5564
5639
  const { index, ...rest } = options;
@@ -5657,26 +5732,20 @@ function createImperativeHandle(ctx) {
5657
5732
  }
5658
5733
  return false;
5659
5734
  }),
5660
- scrollToEnd: (options) => runScrollWithPromise(
5661
- () => {
5662
- const data = state.props.data;
5663
- const stylePaddingBottom = state.props.stylePaddingBottom;
5664
- const index = data.length - 1;
5665
- if (index !== -1) {
5666
- const paddingBottom = stylePaddingBottom || 0;
5667
- const footerSize = peek$(ctx, "footerSize") || 0;
5668
- scrollToIndex(ctx, {
5669
- ...options,
5670
- index,
5671
- viewOffset: -paddingBottom - footerSize + ((options == null ? void 0 : options.viewOffset) || 0),
5672
- viewPosition: 1
5673
- });
5674
- return true;
5675
- }
5676
- return false;
5677
- },
5678
- () => isScrollToIndexReady(state.props.data.length - 1, true)
5679
- ),
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
+ }),
5680
5749
  scrollToIndex: (params) => {
5681
5750
  return runScrollWithPromise(
5682
5751
  () => {
@@ -6046,6 +6115,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
6046
6115
  viewOffset: 0
6047
6116
  } : void 0;
6048
6117
  const [canRender, setCanRender] = React2__namespace.useState(!IsNewArchitecture);
6118
+ const [, scheduleImperativeScrollCommit] = React2__namespace.useReducer((value) => value + 1, 0);
6049
6119
  const ctx = useStateContext();
6050
6120
  ctx.columnWrapperStyle = columnWrapperStyle || (contentContainerStyle ? createColumnWrapperStyle(contentContainerStyle) : void 0);
6051
6121
  const refScroller = React2.useRef(null);
@@ -6418,7 +6488,11 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
6418
6488
  doInitialAllocateContainers(ctx);
6419
6489
  }
6420
6490
  });
6421
- 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
+ });
6422
6496
  React2.useEffect(() => {
6423
6497
  if (Platform.OS !== "web" || usesBootstrapInitialScroll) {
6424
6498
  return;