@react-native/virtualized-lists 0.72.4 → 0.72.5

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.
@@ -73,6 +73,10 @@ type ViewabilityHelperCallbackTuple = {
73
73
  type State = {
74
74
  renderMask: CellRenderMask,
75
75
  cellsAroundViewport: {first: number, last: number},
76
+ // Used to track items added at the start of the list for maintainVisibleContentPosition.
77
+ firstVisibleItemKey: ?string,
78
+ // When > 0 the scroll position available in JS is considered stale and should not be used.
79
+ pendingScrollUpdateCount: number,
76
80
  };
77
81
 
78
82
  /**
@@ -448,9 +452,24 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
448
452
 
449
453
  const initialRenderRegion = VirtualizedList._initialRenderRegion(props);
450
454
 
455
+ const minIndexForVisible =
456
+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0;
457
+
451
458
  this.state = {
452
459
  cellsAroundViewport: initialRenderRegion,
453
460
  renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion),
461
+ firstVisibleItemKey:
462
+ this.props.getItemCount(this.props.data) > minIndexForVisible
463
+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible)
464
+ : null,
465
+ // When we have a non-zero initialScrollIndex, we will receive a
466
+ // scroll event later so this will prevent the window from updating
467
+ // until we get a valid offset.
468
+ pendingScrollUpdateCount:
469
+ this.props.initialScrollIndex != null &&
470
+ this.props.initialScrollIndex > 0
471
+ ? 1
472
+ : 0,
454
473
  };
455
474
  }
456
475
 
@@ -502,6 +521,40 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
502
521
  }
503
522
  }
504
523
 
524
+ static _findItemIndexWithKey(
525
+ props: Props,
526
+ key: string,
527
+ hint: ?number,
528
+ ): ?number {
529
+ const itemCount = props.getItemCount(props.data);
530
+ if (hint != null && hint >= 0 && hint < itemCount) {
531
+ const curKey = VirtualizedList._getItemKey(props, hint);
532
+ if (curKey === key) {
533
+ return hint;
534
+ }
535
+ }
536
+ for (let ii = 0; ii < itemCount; ii++) {
537
+ const curKey = VirtualizedList._getItemKey(props, ii);
538
+ if (curKey === key) {
539
+ return ii;
540
+ }
541
+ }
542
+ return null;
543
+ }
544
+
545
+ static _getItemKey(
546
+ props: {
547
+ data: Props['data'],
548
+ getItem: Props['getItem'],
549
+ keyExtractor: Props['keyExtractor'],
550
+ ...
551
+ },
552
+ index: number,
553
+ ): string {
554
+ const item = props.getItem(props.data, index);
555
+ return VirtualizedList._keyExtractor(item, index, props);
556
+ }
557
+
505
558
  static _createRenderMask(
506
559
  props: Props,
507
560
  cellsAroundViewport: {first: number, last: number},
@@ -585,6 +638,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
585
638
  _adjustCellsAroundViewport(
586
639
  props: Props,
587
640
  cellsAroundViewport: {first: number, last: number},
641
+ pendingScrollUpdateCount: number,
588
642
  ): {first: number, last: number} {
589
643
  const {data, getItemCount} = props;
590
644
  const onEndReachedThreshold = onEndReachedThresholdOrDefault(
@@ -616,21 +670,9 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
616
670
  ),
617
671
  };
618
672
  } else {
619
- // If we have a non-zero initialScrollIndex and run this before we've scrolled,
620
- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex.
621
- // So let's wait until we've scrolled the view to the right place. And until then,
622
- // we will trust the initialScrollIndex suggestion.
623
-
624
- // Thus, we want to recalculate the windowed render limits if any of the following hold:
625
- // - initialScrollIndex is undefined or is 0
626
- // - initialScrollIndex > 0 AND scrolling is complete
627
- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case
628
- // where the list is shorter than the visible area)
629
- if (
630
- props.initialScrollIndex &&
631
- !this._scrollMetrics.offset &&
632
- Math.abs(distanceFromEnd) >= Number.EPSILON
633
- ) {
673
+ // If we have a pending scroll update, we should not adjust the render window as it
674
+ // might override the correct window.
675
+ if (pendingScrollUpdateCount > 0) {
634
676
  return cellsAroundViewport.last >= getItemCount(data)
635
677
  ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props)
636
678
  : cellsAroundViewport;
@@ -712,14 +754,59 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
712
754
  return prevState;
713
755
  }
714
756
 
757
+ let maintainVisibleContentPositionAdjustment: ?number = null;
758
+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey;
759
+ const minIndexForVisible =
760
+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0;
761
+ const newFirstVisibleItemKey =
762
+ newProps.getItemCount(newProps.data) > minIndexForVisible
763
+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible)
764
+ : null;
765
+ if (
766
+ newProps.maintainVisibleContentPosition != null &&
767
+ prevFirstVisibleItemKey != null &&
768
+ newFirstVisibleItemKey != null
769
+ ) {
770
+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) {
771
+ // Fast path if items were added at the start of the list.
772
+ const hint =
773
+ itemCount - prevState.renderMask.numCells() + minIndexForVisible;
774
+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(
775
+ newProps,
776
+ prevFirstVisibleItemKey,
777
+ hint,
778
+ );
779
+ maintainVisibleContentPositionAdjustment =
780
+ firstVisibleItemIndex != null
781
+ ? firstVisibleItemIndex - minIndexForVisible
782
+ : null;
783
+ } else {
784
+ maintainVisibleContentPositionAdjustment = null;
785
+ }
786
+ }
787
+
715
788
  const constrainedCells = VirtualizedList._constrainToItemCount(
716
- prevState.cellsAroundViewport,
789
+ maintainVisibleContentPositionAdjustment != null
790
+ ? {
791
+ first:
792
+ prevState.cellsAroundViewport.first +
793
+ maintainVisibleContentPositionAdjustment,
794
+ last:
795
+ prevState.cellsAroundViewport.last +
796
+ maintainVisibleContentPositionAdjustment,
797
+ }
798
+ : prevState.cellsAroundViewport,
717
799
  newProps,
718
800
  );
719
801
 
720
802
  return {
721
803
  cellsAroundViewport: constrainedCells,
722
804
  renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells),
805
+ firstVisibleItemKey: newFirstVisibleItemKey,
806
+ pendingScrollUpdateCount:
807
+ maintainVisibleContentPositionAdjustment != null
808
+ ? prevState.pendingScrollUpdateCount + 1
809
+ : prevState.pendingScrollUpdateCount,
723
810
  };
724
811
  }
725
812
 
@@ -751,7 +838,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
751
838
 
752
839
  for (let ii = first; ii <= last; ii++) {
753
840
  const item = getItem(data, ii);
754
- const key = this._keyExtractor(item, ii, this.props);
841
+ const key = VirtualizedList._keyExtractor(item, ii, this.props);
755
842
 
756
843
  this._indicesToKeys.set(ii, key);
757
844
  if (stickyIndicesFromProps.has(ii + stickyOffset)) {
@@ -824,15 +911,14 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
824
911
  _getSpacerKey = (isVertical: boolean): string =>
825
912
  isVertical ? 'height' : 'width';
826
913
 
827
- _keyExtractor(
914
+ static _keyExtractor(
828
915
  item: Item,
829
916
  index: number,
830
917
  props: {
831
918
  keyExtractor?: ?(item: Item, index: number) => string,
832
919
  ...
833
920
  },
834
- // $FlowFixMe[missing-local-annot]
835
- ) {
921
+ ): string {
836
922
  if (props.keyExtractor != null) {
837
923
  return props.keyExtractor(item, index);
838
924
  }
@@ -878,6 +964,10 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
878
964
  cellKey={this._getCellKey() + '-header'}
879
965
  key="$header">
880
966
  <View
967
+ // We expect that header component will be a single native view so make it
968
+ // not collapsable to avoid this view being flattened and make this assumption
969
+ // no longer true.
970
+ collapsable={false}
881
971
  onLayout={this._onLayoutHeader}
882
972
  style={StyleSheet.compose(
883
973
  inversionStyle,
@@ -1035,6 +1125,16 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1035
1125
  style: inversionStyle
1036
1126
  ? [inversionStyle, this.props.style]
1037
1127
  : this.props.style,
1128
+ maintainVisibleContentPosition:
1129
+ this.props.maintainVisibleContentPosition != null
1130
+ ? {
1131
+ ...this.props.maintainVisibleContentPosition,
1132
+ // Adjust index to account for ListHeaderComponent.
1133
+ minIndexForVisible:
1134
+ this.props.maintainVisibleContentPosition.minIndexForVisible +
1135
+ (this.props.ListHeaderComponent ? 1 : 0),
1136
+ }
1137
+ : undefined,
1038
1138
  };
1039
1139
 
1040
1140
  this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1;
@@ -1456,8 +1556,13 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1456
1556
  onStartReachedThreshold,
1457
1557
  onEndReached,
1458
1558
  onEndReachedThreshold,
1459
- initialScrollIndex,
1460
1559
  } = this.props;
1560
+ // If we have any pending scroll updates it means that the scroll metrics
1561
+ // are out of date and we should not call any of the edge reached callbacks.
1562
+ if (this.state.pendingScrollUpdateCount > 0) {
1563
+ return;
1564
+ }
1565
+
1461
1566
  const {contentLength, visibleLength, offset} = this._scrollMetrics;
1462
1567
  let distanceFromStart = offset;
1463
1568
  let distanceFromEnd = contentLength - visibleLength - offset;
@@ -1509,14 +1614,8 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1509
1614
  isWithinStartThreshold &&
1510
1615
  this._scrollMetrics.contentLength !== this._sentStartForContentLength
1511
1616
  ) {
1512
- // On initial mount when using initialScrollIndex the offset will be 0 initially
1513
- // and will trigger an unexpected onStartReached. To avoid this we can use
1514
- // timestamp to differentiate between the initial scroll metrics and when we actually
1515
- // received the first scroll event.
1516
- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) {
1517
- this._sentStartForContentLength = this._scrollMetrics.contentLength;
1518
- onStartReached({distanceFromStart});
1519
- }
1617
+ this._sentStartForContentLength = this._scrollMetrics.contentLength;
1618
+ onStartReached({distanceFromStart});
1520
1619
  }
1521
1620
 
1522
1621
  // If the user scrolls away from the start or end and back again,
@@ -1643,6 +1742,11 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1643
1742
  visibleLength,
1644
1743
  zoomScale,
1645
1744
  };
1745
+ if (this.state.pendingScrollUpdateCount > 0) {
1746
+ this.setState(state => ({
1747
+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1,
1748
+ }));
1749
+ }
1646
1750
  this._updateViewableItems(this.props, this.state.cellsAroundViewport);
1647
1751
  if (!this.props) {
1648
1752
  return;
@@ -1758,6 +1862,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1758
1862
  const cellsAroundViewport = this._adjustCellsAroundViewport(
1759
1863
  props,
1760
1864
  state.cellsAroundViewport,
1865
+ state.pendingScrollUpdateCount,
1761
1866
  );
1762
1867
  const renderMask = VirtualizedList._createRenderMask(
1763
1868
  props,
@@ -1788,7 +1893,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1788
1893
  return {
1789
1894
  index,
1790
1895
  item,
1791
- key: this._keyExtractor(item, index, props),
1896
+ key: VirtualizedList._keyExtractor(item, index, props),
1792
1897
  isViewable,
1793
1898
  };
1794
1899
  };
@@ -1849,13 +1954,12 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1849
1954
  inLayout?: boolean,
1850
1955
  ...
1851
1956
  } => {
1852
- const {data, getItem, getItemCount, getItemLayout} = props;
1957
+ const {data, getItemCount, getItemLayout} = props;
1853
1958
  invariant(
1854
1959
  index >= 0 && index < getItemCount(data),
1855
1960
  'Tried to get frame for out of range index ' + index,
1856
1961
  );
1857
- const item = getItem(data, index);
1858
- const frame = this._frames[this._keyExtractor(item, index, props)];
1962
+ const frame = this._frames[VirtualizedList._getItemKey(props, index)];
1859
1963
  if (!frame || frame.index !== index) {
1860
1964
  if (getItemLayout) {
1861
1965
  /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment
@@ -1890,11 +1994,8 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1890
1994
  // where it is.
1891
1995
  if (
1892
1996
  focusedCellIndex >= itemCount ||
1893
- this._keyExtractor(
1894
- props.getItem(props.data, focusedCellIndex),
1895
- focusedCellIndex,
1896
- props,
1897
- ) !== this._lastFocusedCellKey
1997
+ VirtualizedList._getItemKey(props, focusedCellIndex) !==
1998
+ this._lastFocusedCellKey
1898
1999
  ) {
1899
2000
  return [];
1900
2001
  }
@@ -1935,6 +2036,11 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
1935
2036
  props: FrameMetricProps,
1936
2037
  cellsAroundViewport: {first: number, last: number},
1937
2038
  ) {
2039
+ // If we have any pending scroll updates it means that the scroll metrics
2040
+ // are out of date and we should not call any of the visibility callbacks.
2041
+ if (this.state.pendingScrollUpdateCount > 0) {
2042
+ return;
2043
+ }
1938
2044
  this._viewabilityTuples.forEach(tuple => {
1939
2045
  tuple.viewabilityHelper.onUpdate(
1940
2046
  props,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native/virtualized-lists",
3
- "version": "0.72.4",
3
+ "version": "0.72.5",
4
4
  "description": "Virtualized lists for React Native.",
5
5
  "repository": {
6
6
  "type": "git",