@legendapp/list 3.0.0-beta.55 → 3.0.0-beta.56

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/index.native.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React2 from 'react';
2
- import React2__default, { useReducer, useEffect, createContext, useRef, useState, useMemo, useCallback, useLayoutEffect, useImperativeHandle, useContext } from 'react';
2
+ import { useReducer, useEffect, createContext, useRef, useState, useMemo, useCallback, useLayoutEffect, useImperativeHandle, useContext } from 'react';
3
3
  import * as ReactNative from 'react-native';
4
4
  import { Animated, Platform as Platform$1, View as View$1, Text as Text$1, StyleSheet as StyleSheet$1, RefreshControl, Dimensions, I18nManager } from 'react-native';
5
5
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
@@ -1120,6 +1120,26 @@ function WebAnchoredEndSpace({ horizontal }) {
1120
1120
  const style = horizontal ? { height: "100%", width: anchoredEndSpaceSize || 0 } : { height: anchoredEndSpaceSize || 0 };
1121
1121
  return /* @__PURE__ */ React2.createElement("div", { style }, null);
1122
1122
  }
1123
+ function useLatestRef(value) {
1124
+ const ref = React2.useRef(value);
1125
+ ref.current = value;
1126
+ return ref;
1127
+ }
1128
+
1129
+ // src/hooks/useStableRenderComponent.tsx
1130
+ function useStableRenderComponent(renderComponent, mapProps) {
1131
+ const renderComponentRef = useLatestRef(renderComponent);
1132
+ const mapPropsRef = useLatestRef(mapProps);
1133
+ return React2.useMemo(
1134
+ () => React2.forwardRef(
1135
+ (props, ref) => {
1136
+ var _a3, _b;
1137
+ return (_b = (_a3 = renderComponentRef.current) == null ? void 0 : _a3.call(renderComponentRef, mapPropsRef.current(props, ref))) != null ? _b : null;
1138
+ }
1139
+ ),
1140
+ [mapPropsRef, renderComponentRef]
1141
+ );
1142
+ }
1123
1143
  var LayoutView = ({ onLayoutChange, refView, ...rest }) => {
1124
1144
  const localRef = useRef(null);
1125
1145
  const ref = refView != null ? refView : localRef;
@@ -1164,14 +1184,11 @@ var ListComponent = typedMemo(function ListComponent2({
1164
1184
  needsOtherAxisSize: ctx.state.needsOtherAxisSize,
1165
1185
  otherAxisSize
1166
1186
  });
1167
- const ScrollComponent = useMemo(() => {
1168
- if (!renderScrollComponent) {
1169
- return ListComponentScrollView;
1170
- }
1171
- return React2.forwardRef(
1172
- (props, ref) => renderScrollComponent({ ...props, ref })
1173
- );
1174
- }, [renderScrollComponent]);
1187
+ const CustomScrollComponent = useStableRenderComponent(
1188
+ renderScrollComponent,
1189
+ (props, ref) => ({ ...props, ref })
1190
+ );
1191
+ const ScrollComponent = renderScrollComponent ? CustomScrollComponent : ListComponentScrollView;
1175
1192
  const SnapOrScroll = snapToIndices ? SnapWrapper : ScrollComponent;
1176
1193
  useLayoutEffect(() => {
1177
1194
  if (!ListHeaderComponent) {
@@ -2652,11 +2669,51 @@ function getObservedBootstrapInitialScrollOffset(state) {
2652
2669
  const observedOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getCurrentScrollOffset) == null ? void 0 : _b.call(_a3);
2653
2670
  return typeof observedOffset === "number" && Number.isFinite(observedOffset) ? observedOffset : (_d = (_c = state.scrollPending) != null ? _c : state.scroll) != null ? _d : 0;
2654
2671
  }
2672
+ function getPreservedEndAnchorOffsetDiff(ctx) {
2673
+ var _a3;
2674
+ const state = ctx.state;
2675
+ const initialScroll = state.initialScroll;
2676
+ if (!state.didFinishInitialScroll || ((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) || !initialScroll || initialScroll.viewPosition !== 1 || state.props.data.length === 0 || isOffsetInitialScrollSession(state)) {
2677
+ return;
2678
+ }
2679
+ const currentOffset = typeof state.lastNativeScroll === "number" && Number.isFinite(state.lastNativeScroll) ? state.lastNativeScroll : getObservedBootstrapInitialScrollOffset(state);
2680
+ return resolveInitialScrollOffset(ctx, initialScroll) - currentOffset;
2681
+ }
2682
+ function schedulePreservedEndAnchorCorrection(ctx) {
2683
+ if (getPreservedEndAnchorOffsetDiff(ctx) === void 0) {
2684
+ return false;
2685
+ }
2686
+ const correction = {};
2687
+ schedulePreservedEndAnchorCorrectionFrame(ctx, correction);
2688
+ return true;
2689
+ }
2690
+ function schedulePreservedEndAnchorCorrectionFrame(ctx, correction) {
2691
+ const state = ctx.state;
2692
+ state.preservedEndAnchorCorrection = correction;
2693
+ requestAnimationFrame(() => {
2694
+ var _a3;
2695
+ const activeCorrection = state.preservedEndAnchorCorrection;
2696
+ if (activeCorrection !== correction) {
2697
+ return;
2698
+ }
2699
+ const offsetDiff = getPreservedEndAnchorOffsetDiff(ctx);
2700
+ if (offsetDiff === void 0 || Math.abs(offsetDiff) <= DEFAULT_BOOTSTRAP_REVEAL_EPSILON) {
2701
+ state.preservedEndAnchorCorrection = void 0;
2702
+ return;
2703
+ }
2704
+ const hasObservedNativeScrollAfterRequest = !activeCorrection.lastRequestTime || ((_a3 = state.lastNativeScrollTime) != null ? _a3 : 0) > activeCorrection.lastRequestTime;
2705
+ if (hasObservedNativeScrollAfterRequest) {
2706
+ activeCorrection.lastRequestTime = Date.now();
2707
+ requestAdjust(ctx, offsetDiff);
2708
+ }
2709
+ schedulePreservedEndAnchorCorrectionFrame(ctx, correction);
2710
+ });
2711
+ }
2655
2712
  function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
2656
2713
  var _a3, _b;
2657
2714
  const state = ctx.state;
2658
2715
  const initialScroll = state.initialScroll;
2659
- if (!state.didFinishInitialScroll || ((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) || (initialScroll == null ? void 0 : initialScroll.viewPosition) !== 1) {
2716
+ if (!state.didFinishInitialScroll || ((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) || (initialScroll == null ? void 0 : initialScroll.viewPosition) !== 1 || state.preservedEndAnchorCorrection) {
2660
2717
  return;
2661
2718
  }
2662
2719
  if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {
@@ -2813,7 +2870,7 @@ function handleBootstrapInitialScrollFooterLayout(ctx, options) {
2813
2870
  }
2814
2871
  }
2815
2872
  function handleBootstrapInitialScrollLayoutChange(ctx) {
2816
- var _a3, _b, _c, _d;
2873
+ var _a3, _b, _c;
2817
2874
  const state = ctx.state;
2818
2875
  const initialScroll = state.initialScroll;
2819
2876
  const bootstrapInitialScroll = getBootstrapInitialScrollSession(state);
@@ -2824,7 +2881,9 @@ function handleBootstrapInitialScrollLayoutChange(ctx) {
2824
2881
  const currentOffset = scrollingTo ? (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset : getObservedBootstrapInitialScrollOffset(state);
2825
2882
  const offsetDiff = resolvedOffset - currentOffset;
2826
2883
  if (Math.abs(offsetDiff) > DEFAULT_BOOTSTRAP_REVEAL_EPSILON) {
2827
- if (scrollingTo) {
2884
+ if (state.didFinishInitialScroll) {
2885
+ schedulePreservedEndAnchorCorrection(ctx);
2886
+ } else if (scrollingTo) {
2828
2887
  const existingWatchdog = initialScrollWatchdog.get(state);
2829
2888
  scrollingTo.offset = resolvedOffset;
2830
2889
  scrollingTo.targetOffset = resolvedOffset;
@@ -2837,14 +2896,8 @@ function handleBootstrapInitialScrollLayoutChange(ctx) {
2837
2896
  startScroll: (_c = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _c : state.scroll,
2838
2897
  targetOffset: resolvedOffset
2839
2898
  });
2899
+ requestAdjust(ctx, offsetDiff);
2840
2900
  }
2841
- requestAdjust(ctx, offsetDiff);
2842
- if (state.didFinishInitialScroll) {
2843
- (_d = state.triggerCalculateItemsInView) == null ? void 0 : _d.call(state, { forceFullItemPositions: true });
2844
- }
2845
- }
2846
- if (state.didFinishInitialScroll) {
2847
- clearFinishedViewportRetargetableInitialScroll(state);
2848
2901
  }
2849
2902
  } else {
2850
2903
  rearmBootstrapInitialScroll(ctx, {
@@ -2961,7 +3014,10 @@ function retargetActiveInitialScrollAtEnd(ctx) {
2961
3014
  var _a3;
2962
3015
  const state = ctx.state;
2963
3016
  const initialScroll = state.initialScroll;
2964
- if (!initialScroll || state.didFinishInitialScroll || ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset" || initialScroll.viewPosition !== 1 || state.props.data.length === 0) {
3017
+ if (state.didFinishInitialScroll) {
3018
+ return schedulePreservedEndAnchorCorrection(ctx);
3019
+ }
3020
+ if (!initialScroll || ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset" || initialScroll.viewPosition !== 1 || state.props.data.length === 0) {
2965
3021
  return false;
2966
3022
  }
2967
3023
  return advanceCurrentInitialScrollSession(ctx, { forceScroll: true });
@@ -4975,7 +5031,7 @@ function cloneScrollEvent(event) {
4975
5031
  };
4976
5032
  }
4977
5033
  function onScroll(ctx, event) {
4978
- var _a3, _b, _c, _d, _e;
5034
+ var _a3, _b, _c, _d, _e, _f;
4979
5035
  const state = ctx.state;
4980
5036
  const { scrollProcessingEnabled } = state;
4981
5037
  if (scrollProcessingEnabled === false) {
@@ -4997,6 +5053,13 @@ function onScroll(ctx, event) {
4997
5053
  if (state.props.horizontal) {
4998
5054
  newScroll = toLogicalHorizontalOffset(state, newScroll, (_e = event.nativeEvent.contentSize) == null ? void 0 : _e.width);
4999
5055
  }
5056
+ const isFinishedEndInitialScroll = state.didFinishInitialScroll && ((_f = state.initialScroll) == null ? void 0 : _f.viewPosition) === 1 && state.scroll > state.scrollLength;
5057
+ const shouldIgnoreNegativeInsetChange = Platform.OS !== "web" && insetChanged && newScroll < 0 && isFinishedEndInitialScroll;
5058
+ if (shouldIgnoreNegativeInsetChange) {
5059
+ return;
5060
+ }
5061
+ state.lastNativeScroll = newScroll;
5062
+ state.lastNativeScrollTime = Date.now();
5000
5063
  if (state.scrollingTo && state.scrollingTo.offset >= newScroll) {
5001
5064
  const maxOffset = clampScrollOffset(ctx, newScroll, state.scrollingTo);
5002
5065
  if (newScroll !== maxOffset && Math.abs(newScroll - maxOffset) > 1) {
@@ -5637,6 +5700,8 @@ function getAlwaysRenderIndices(config, data, keyExtractor, anchoredEndSpaceAnch
5637
5700
  indices.sort(sortAsc);
5638
5701
  return indices;
5639
5702
  }
5703
+
5704
+ // src/utils/getRenderedItem.ts
5640
5705
  function getRenderedItem(ctx, key) {
5641
5706
  var _a3;
5642
5707
  const state = ctx.state;
@@ -5662,7 +5727,7 @@ function getRenderedItem(ctx, key) {
5662
5727
  item,
5663
5728
  type: getItemType ? (_a3 = getItemType(item, index)) != null ? _a3 : "" : ""
5664
5729
  };
5665
- renderedItem = React2__default.createElement(renderItem, itemProps);
5730
+ renderedItem = renderItem(itemProps);
5666
5731
  }
5667
5732
  return { index, item: data[index], renderedItem };
5668
5733
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/list",
3
- "version": "3.0.0-beta.55",
3
+ "version": "3.0.0-beta.56",
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
@@ -97,13 +97,11 @@ interface DataModeProps<ItemT, TItemType extends string | undefined> {
97
97
  */
98
98
  data: ReadonlyArray<ItemT>;
99
99
  /**
100
- * Function or React component to render each item in the list.
101
- * Can be either:
102
- * - A function: (props: LegendListRenderItemProps<ItemT>) => ReactNode
103
- * - A React component: React.ComponentType<LegendListRenderItemProps<ItemT>>
100
+ * Callback to render each item in the list.
101
+ * To use hooks in an item component, return that component from this callback.
104
102
  * @required when using data mode
105
103
  */
106
- renderItem: ((props: LegendListRenderItemProps<ItemT, TItemType>) => React.ReactNode) | React.ComponentType<LegendListRenderItemProps<ItemT, TItemType>>;
104
+ renderItem: (props: LegendListRenderItemProps<ItemT, TItemType>) => React.ReactNode;
107
105
  children?: never;
108
106
  }
109
107
  interface ChildrenModeProps {
package/react-native.js CHANGED
@@ -1141,6 +1141,26 @@ function WebAnchoredEndSpace({ horizontal }) {
1141
1141
  const style = horizontal ? { height: "100%", width: anchoredEndSpaceSize || 0 } : { height: anchoredEndSpaceSize || 0 };
1142
1142
  return /* @__PURE__ */ React2__namespace.createElement("div", { style }, null);
1143
1143
  }
1144
+ function useLatestRef(value) {
1145
+ const ref = React2__namespace.useRef(value);
1146
+ ref.current = value;
1147
+ return ref;
1148
+ }
1149
+
1150
+ // src/hooks/useStableRenderComponent.tsx
1151
+ function useStableRenderComponent(renderComponent, mapProps) {
1152
+ const renderComponentRef = useLatestRef(renderComponent);
1153
+ const mapPropsRef = useLatestRef(mapProps);
1154
+ return React2__namespace.useMemo(
1155
+ () => React2__namespace.forwardRef(
1156
+ (props, ref) => {
1157
+ var _a3, _b;
1158
+ return (_b = (_a3 = renderComponentRef.current) == null ? void 0 : _a3.call(renderComponentRef, mapPropsRef.current(props, ref))) != null ? _b : null;
1159
+ }
1160
+ ),
1161
+ [mapPropsRef, renderComponentRef]
1162
+ );
1163
+ }
1144
1164
  var LayoutView = ({ onLayoutChange, refView, ...rest }) => {
1145
1165
  const localRef = React2.useRef(null);
1146
1166
  const ref = refView != null ? refView : localRef;
@@ -1185,14 +1205,11 @@ var ListComponent = typedMemo(function ListComponent2({
1185
1205
  needsOtherAxisSize: ctx.state.needsOtherAxisSize,
1186
1206
  otherAxisSize
1187
1207
  });
1188
- const ScrollComponent = React2.useMemo(() => {
1189
- if (!renderScrollComponent) {
1190
- return ListComponentScrollView;
1191
- }
1192
- return React2__namespace.forwardRef(
1193
- (props, ref) => renderScrollComponent({ ...props, ref })
1194
- );
1195
- }, [renderScrollComponent]);
1208
+ const CustomScrollComponent = useStableRenderComponent(
1209
+ renderScrollComponent,
1210
+ (props, ref) => ({ ...props, ref })
1211
+ );
1212
+ const ScrollComponent = renderScrollComponent ? CustomScrollComponent : ListComponentScrollView;
1196
1213
  const SnapOrScroll = snapToIndices ? SnapWrapper : ScrollComponent;
1197
1214
  React2.useLayoutEffect(() => {
1198
1215
  if (!ListHeaderComponent) {
@@ -2673,11 +2690,51 @@ function getObservedBootstrapInitialScrollOffset(state) {
2673
2690
  const observedOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getCurrentScrollOffset) == null ? void 0 : _b.call(_a3);
2674
2691
  return typeof observedOffset === "number" && Number.isFinite(observedOffset) ? observedOffset : (_d = (_c = state.scrollPending) != null ? _c : state.scroll) != null ? _d : 0;
2675
2692
  }
2693
+ function getPreservedEndAnchorOffsetDiff(ctx) {
2694
+ var _a3;
2695
+ const state = ctx.state;
2696
+ const initialScroll = state.initialScroll;
2697
+ if (!state.didFinishInitialScroll || ((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) || !initialScroll || initialScroll.viewPosition !== 1 || state.props.data.length === 0 || isOffsetInitialScrollSession(state)) {
2698
+ return;
2699
+ }
2700
+ const currentOffset = typeof state.lastNativeScroll === "number" && Number.isFinite(state.lastNativeScroll) ? state.lastNativeScroll : getObservedBootstrapInitialScrollOffset(state);
2701
+ return resolveInitialScrollOffset(ctx, initialScroll) - currentOffset;
2702
+ }
2703
+ function schedulePreservedEndAnchorCorrection(ctx) {
2704
+ if (getPreservedEndAnchorOffsetDiff(ctx) === void 0) {
2705
+ return false;
2706
+ }
2707
+ const correction = {};
2708
+ schedulePreservedEndAnchorCorrectionFrame(ctx, correction);
2709
+ return true;
2710
+ }
2711
+ function schedulePreservedEndAnchorCorrectionFrame(ctx, correction) {
2712
+ const state = ctx.state;
2713
+ state.preservedEndAnchorCorrection = correction;
2714
+ requestAnimationFrame(() => {
2715
+ var _a3;
2716
+ const activeCorrection = state.preservedEndAnchorCorrection;
2717
+ if (activeCorrection !== correction) {
2718
+ return;
2719
+ }
2720
+ const offsetDiff = getPreservedEndAnchorOffsetDiff(ctx);
2721
+ if (offsetDiff === void 0 || Math.abs(offsetDiff) <= DEFAULT_BOOTSTRAP_REVEAL_EPSILON) {
2722
+ state.preservedEndAnchorCorrection = void 0;
2723
+ return;
2724
+ }
2725
+ const hasObservedNativeScrollAfterRequest = !activeCorrection.lastRequestTime || ((_a3 = state.lastNativeScrollTime) != null ? _a3 : 0) > activeCorrection.lastRequestTime;
2726
+ if (hasObservedNativeScrollAfterRequest) {
2727
+ activeCorrection.lastRequestTime = Date.now();
2728
+ requestAdjust(ctx, offsetDiff);
2729
+ }
2730
+ schedulePreservedEndAnchorCorrectionFrame(ctx, correction);
2731
+ });
2732
+ }
2676
2733
  function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
2677
2734
  var _a3, _b;
2678
2735
  const state = ctx.state;
2679
2736
  const initialScroll = state.initialScroll;
2680
- if (!state.didFinishInitialScroll || ((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) || (initialScroll == null ? void 0 : initialScroll.viewPosition) !== 1) {
2737
+ if (!state.didFinishInitialScroll || ((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) || (initialScroll == null ? void 0 : initialScroll.viewPosition) !== 1 || state.preservedEndAnchorCorrection) {
2681
2738
  return;
2682
2739
  }
2683
2740
  if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {
@@ -2834,7 +2891,7 @@ function handleBootstrapInitialScrollFooterLayout(ctx, options) {
2834
2891
  }
2835
2892
  }
2836
2893
  function handleBootstrapInitialScrollLayoutChange(ctx) {
2837
- var _a3, _b, _c, _d;
2894
+ var _a3, _b, _c;
2838
2895
  const state = ctx.state;
2839
2896
  const initialScroll = state.initialScroll;
2840
2897
  const bootstrapInitialScroll = getBootstrapInitialScrollSession(state);
@@ -2845,7 +2902,9 @@ function handleBootstrapInitialScrollLayoutChange(ctx) {
2845
2902
  const currentOffset = scrollingTo ? (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset : getObservedBootstrapInitialScrollOffset(state);
2846
2903
  const offsetDiff = resolvedOffset - currentOffset;
2847
2904
  if (Math.abs(offsetDiff) > DEFAULT_BOOTSTRAP_REVEAL_EPSILON) {
2848
- if (scrollingTo) {
2905
+ if (state.didFinishInitialScroll) {
2906
+ schedulePreservedEndAnchorCorrection(ctx);
2907
+ } else if (scrollingTo) {
2849
2908
  const existingWatchdog = initialScrollWatchdog.get(state);
2850
2909
  scrollingTo.offset = resolvedOffset;
2851
2910
  scrollingTo.targetOffset = resolvedOffset;
@@ -2858,14 +2917,8 @@ function handleBootstrapInitialScrollLayoutChange(ctx) {
2858
2917
  startScroll: (_c = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _c : state.scroll,
2859
2918
  targetOffset: resolvedOffset
2860
2919
  });
2920
+ requestAdjust(ctx, offsetDiff);
2861
2921
  }
2862
- requestAdjust(ctx, offsetDiff);
2863
- if (state.didFinishInitialScroll) {
2864
- (_d = state.triggerCalculateItemsInView) == null ? void 0 : _d.call(state, { forceFullItemPositions: true });
2865
- }
2866
- }
2867
- if (state.didFinishInitialScroll) {
2868
- clearFinishedViewportRetargetableInitialScroll(state);
2869
2922
  }
2870
2923
  } else {
2871
2924
  rearmBootstrapInitialScroll(ctx, {
@@ -2982,7 +3035,10 @@ function retargetActiveInitialScrollAtEnd(ctx) {
2982
3035
  var _a3;
2983
3036
  const state = ctx.state;
2984
3037
  const initialScroll = state.initialScroll;
2985
- if (!initialScroll || state.didFinishInitialScroll || ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset" || initialScroll.viewPosition !== 1 || state.props.data.length === 0) {
3038
+ if (state.didFinishInitialScroll) {
3039
+ return schedulePreservedEndAnchorCorrection(ctx);
3040
+ }
3041
+ if (!initialScroll || ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset" || initialScroll.viewPosition !== 1 || state.props.data.length === 0) {
2986
3042
  return false;
2987
3043
  }
2988
3044
  return advanceCurrentInitialScrollSession(ctx, { forceScroll: true });
@@ -4996,7 +5052,7 @@ function cloneScrollEvent(event) {
4996
5052
  };
4997
5053
  }
4998
5054
  function onScroll(ctx, event) {
4999
- var _a3, _b, _c, _d, _e;
5055
+ var _a3, _b, _c, _d, _e, _f;
5000
5056
  const state = ctx.state;
5001
5057
  const { scrollProcessingEnabled } = state;
5002
5058
  if (scrollProcessingEnabled === false) {
@@ -5018,6 +5074,13 @@ function onScroll(ctx, event) {
5018
5074
  if (state.props.horizontal) {
5019
5075
  newScroll = toLogicalHorizontalOffset(state, newScroll, (_e = event.nativeEvent.contentSize) == null ? void 0 : _e.width);
5020
5076
  }
5077
+ const isFinishedEndInitialScroll = state.didFinishInitialScroll && ((_f = state.initialScroll) == null ? void 0 : _f.viewPosition) === 1 && state.scroll > state.scrollLength;
5078
+ const shouldIgnoreNegativeInsetChange = Platform.OS !== "web" && insetChanged && newScroll < 0 && isFinishedEndInitialScroll;
5079
+ if (shouldIgnoreNegativeInsetChange) {
5080
+ return;
5081
+ }
5082
+ state.lastNativeScroll = newScroll;
5083
+ state.lastNativeScrollTime = Date.now();
5021
5084
  if (state.scrollingTo && state.scrollingTo.offset >= newScroll) {
5022
5085
  const maxOffset = clampScrollOffset(ctx, newScroll, state.scrollingTo);
5023
5086
  if (newScroll !== maxOffset && Math.abs(newScroll - maxOffset) > 1) {
@@ -5658,6 +5721,8 @@ function getAlwaysRenderIndices(config, data, keyExtractor, anchoredEndSpaceAnch
5658
5721
  indices.sort(sortAsc);
5659
5722
  return indices;
5660
5723
  }
5724
+
5725
+ // src/utils/getRenderedItem.ts
5661
5726
  function getRenderedItem(ctx, key) {
5662
5727
  var _a3;
5663
5728
  const state = ctx.state;
@@ -5683,7 +5748,7 @@ function getRenderedItem(ctx, key) {
5683
5748
  item,
5684
5749
  type: getItemType ? (_a3 = getItemType(item, index)) != null ? _a3 : "" : ""
5685
5750
  };
5686
- renderedItem = React2__namespace.default.createElement(renderItem, itemProps);
5751
+ renderedItem = renderItem(itemProps);
5687
5752
  }
5688
5753
  return { index, item: data[index], renderedItem };
5689
5754
  }
@@ -6393,6 +6458,8 @@ var internal = {
6393
6458
  typedMemo,
6394
6459
  useArr$,
6395
6460
  useCombinedRef,
6461
+ useLatestRef,
6462
+ useStableRenderComponent,
6396
6463
  useStateContext
6397
6464
  };
6398
6465
 
package/react-native.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React2 from 'react';
2
- import React2__default, { useReducer, useEffect, createContext, useRef, useState, useMemo, useCallback, useLayoutEffect, useImperativeHandle, useContext } from 'react';
2
+ import { useReducer, useEffect, createContext, useRef, useState, useMemo, useCallback, useLayoutEffect, useImperativeHandle, useContext } from 'react';
3
3
  import * as ReactNative from 'react-native';
4
4
  import { Animated, Platform as Platform$1, View as View$1, Text as Text$1, StyleSheet as StyleSheet$1, RefreshControl, Dimensions, I18nManager } from 'react-native';
5
5
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
@@ -1120,6 +1120,26 @@ function WebAnchoredEndSpace({ horizontal }) {
1120
1120
  const style = horizontal ? { height: "100%", width: anchoredEndSpaceSize || 0 } : { height: anchoredEndSpaceSize || 0 };
1121
1121
  return /* @__PURE__ */ React2.createElement("div", { style }, null);
1122
1122
  }
1123
+ function useLatestRef(value) {
1124
+ const ref = React2.useRef(value);
1125
+ ref.current = value;
1126
+ return ref;
1127
+ }
1128
+
1129
+ // src/hooks/useStableRenderComponent.tsx
1130
+ function useStableRenderComponent(renderComponent, mapProps) {
1131
+ const renderComponentRef = useLatestRef(renderComponent);
1132
+ const mapPropsRef = useLatestRef(mapProps);
1133
+ return React2.useMemo(
1134
+ () => React2.forwardRef(
1135
+ (props, ref) => {
1136
+ var _a3, _b;
1137
+ return (_b = (_a3 = renderComponentRef.current) == null ? void 0 : _a3.call(renderComponentRef, mapPropsRef.current(props, ref))) != null ? _b : null;
1138
+ }
1139
+ ),
1140
+ [mapPropsRef, renderComponentRef]
1141
+ );
1142
+ }
1123
1143
  var LayoutView = ({ onLayoutChange, refView, ...rest }) => {
1124
1144
  const localRef = useRef(null);
1125
1145
  const ref = refView != null ? refView : localRef;
@@ -1164,14 +1184,11 @@ var ListComponent = typedMemo(function ListComponent2({
1164
1184
  needsOtherAxisSize: ctx.state.needsOtherAxisSize,
1165
1185
  otherAxisSize
1166
1186
  });
1167
- const ScrollComponent = useMemo(() => {
1168
- if (!renderScrollComponent) {
1169
- return ListComponentScrollView;
1170
- }
1171
- return React2.forwardRef(
1172
- (props, ref) => renderScrollComponent({ ...props, ref })
1173
- );
1174
- }, [renderScrollComponent]);
1187
+ const CustomScrollComponent = useStableRenderComponent(
1188
+ renderScrollComponent,
1189
+ (props, ref) => ({ ...props, ref })
1190
+ );
1191
+ const ScrollComponent = renderScrollComponent ? CustomScrollComponent : ListComponentScrollView;
1175
1192
  const SnapOrScroll = snapToIndices ? SnapWrapper : ScrollComponent;
1176
1193
  useLayoutEffect(() => {
1177
1194
  if (!ListHeaderComponent) {
@@ -2652,11 +2669,51 @@ function getObservedBootstrapInitialScrollOffset(state) {
2652
2669
  const observedOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getCurrentScrollOffset) == null ? void 0 : _b.call(_a3);
2653
2670
  return typeof observedOffset === "number" && Number.isFinite(observedOffset) ? observedOffset : (_d = (_c = state.scrollPending) != null ? _c : state.scroll) != null ? _d : 0;
2654
2671
  }
2672
+ function getPreservedEndAnchorOffsetDiff(ctx) {
2673
+ var _a3;
2674
+ const state = ctx.state;
2675
+ const initialScroll = state.initialScroll;
2676
+ if (!state.didFinishInitialScroll || ((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) || !initialScroll || initialScroll.viewPosition !== 1 || state.props.data.length === 0 || isOffsetInitialScrollSession(state)) {
2677
+ return;
2678
+ }
2679
+ const currentOffset = typeof state.lastNativeScroll === "number" && Number.isFinite(state.lastNativeScroll) ? state.lastNativeScroll : getObservedBootstrapInitialScrollOffset(state);
2680
+ return resolveInitialScrollOffset(ctx, initialScroll) - currentOffset;
2681
+ }
2682
+ function schedulePreservedEndAnchorCorrection(ctx) {
2683
+ if (getPreservedEndAnchorOffsetDiff(ctx) === void 0) {
2684
+ return false;
2685
+ }
2686
+ const correction = {};
2687
+ schedulePreservedEndAnchorCorrectionFrame(ctx, correction);
2688
+ return true;
2689
+ }
2690
+ function schedulePreservedEndAnchorCorrectionFrame(ctx, correction) {
2691
+ const state = ctx.state;
2692
+ state.preservedEndAnchorCorrection = correction;
2693
+ requestAnimationFrame(() => {
2694
+ var _a3;
2695
+ const activeCorrection = state.preservedEndAnchorCorrection;
2696
+ if (activeCorrection !== correction) {
2697
+ return;
2698
+ }
2699
+ const offsetDiff = getPreservedEndAnchorOffsetDiff(ctx);
2700
+ if (offsetDiff === void 0 || Math.abs(offsetDiff) <= DEFAULT_BOOTSTRAP_REVEAL_EPSILON) {
2701
+ state.preservedEndAnchorCorrection = void 0;
2702
+ return;
2703
+ }
2704
+ const hasObservedNativeScrollAfterRequest = !activeCorrection.lastRequestTime || ((_a3 = state.lastNativeScrollTime) != null ? _a3 : 0) > activeCorrection.lastRequestTime;
2705
+ if (hasObservedNativeScrollAfterRequest) {
2706
+ activeCorrection.lastRequestTime = Date.now();
2707
+ requestAdjust(ctx, offsetDiff);
2708
+ }
2709
+ schedulePreservedEndAnchorCorrectionFrame(ctx, correction);
2710
+ });
2711
+ }
2655
2712
  function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
2656
2713
  var _a3, _b;
2657
2714
  const state = ctx.state;
2658
2715
  const initialScroll = state.initialScroll;
2659
- if (!state.didFinishInitialScroll || ((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) || (initialScroll == null ? void 0 : initialScroll.viewPosition) !== 1) {
2716
+ if (!state.didFinishInitialScroll || ((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) || (initialScroll == null ? void 0 : initialScroll.viewPosition) !== 1 || state.preservedEndAnchorCorrection) {
2660
2717
  return;
2661
2718
  }
2662
2719
  if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {
@@ -2813,7 +2870,7 @@ function handleBootstrapInitialScrollFooterLayout(ctx, options) {
2813
2870
  }
2814
2871
  }
2815
2872
  function handleBootstrapInitialScrollLayoutChange(ctx) {
2816
- var _a3, _b, _c, _d;
2873
+ var _a3, _b, _c;
2817
2874
  const state = ctx.state;
2818
2875
  const initialScroll = state.initialScroll;
2819
2876
  const bootstrapInitialScroll = getBootstrapInitialScrollSession(state);
@@ -2824,7 +2881,9 @@ function handleBootstrapInitialScrollLayoutChange(ctx) {
2824
2881
  const currentOffset = scrollingTo ? (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset : getObservedBootstrapInitialScrollOffset(state);
2825
2882
  const offsetDiff = resolvedOffset - currentOffset;
2826
2883
  if (Math.abs(offsetDiff) > DEFAULT_BOOTSTRAP_REVEAL_EPSILON) {
2827
- if (scrollingTo) {
2884
+ if (state.didFinishInitialScroll) {
2885
+ schedulePreservedEndAnchorCorrection(ctx);
2886
+ } else if (scrollingTo) {
2828
2887
  const existingWatchdog = initialScrollWatchdog.get(state);
2829
2888
  scrollingTo.offset = resolvedOffset;
2830
2889
  scrollingTo.targetOffset = resolvedOffset;
@@ -2837,14 +2896,8 @@ function handleBootstrapInitialScrollLayoutChange(ctx) {
2837
2896
  startScroll: (_c = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _c : state.scroll,
2838
2897
  targetOffset: resolvedOffset
2839
2898
  });
2899
+ requestAdjust(ctx, offsetDiff);
2840
2900
  }
2841
- requestAdjust(ctx, offsetDiff);
2842
- if (state.didFinishInitialScroll) {
2843
- (_d = state.triggerCalculateItemsInView) == null ? void 0 : _d.call(state, { forceFullItemPositions: true });
2844
- }
2845
- }
2846
- if (state.didFinishInitialScroll) {
2847
- clearFinishedViewportRetargetableInitialScroll(state);
2848
2901
  }
2849
2902
  } else {
2850
2903
  rearmBootstrapInitialScroll(ctx, {
@@ -2961,7 +3014,10 @@ function retargetActiveInitialScrollAtEnd(ctx) {
2961
3014
  var _a3;
2962
3015
  const state = ctx.state;
2963
3016
  const initialScroll = state.initialScroll;
2964
- if (!initialScroll || state.didFinishInitialScroll || ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset" || initialScroll.viewPosition !== 1 || state.props.data.length === 0) {
3017
+ if (state.didFinishInitialScroll) {
3018
+ return schedulePreservedEndAnchorCorrection(ctx);
3019
+ }
3020
+ if (!initialScroll || ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset" || initialScroll.viewPosition !== 1 || state.props.data.length === 0) {
2965
3021
  return false;
2966
3022
  }
2967
3023
  return advanceCurrentInitialScrollSession(ctx, { forceScroll: true });
@@ -4975,7 +5031,7 @@ function cloneScrollEvent(event) {
4975
5031
  };
4976
5032
  }
4977
5033
  function onScroll(ctx, event) {
4978
- var _a3, _b, _c, _d, _e;
5034
+ var _a3, _b, _c, _d, _e, _f;
4979
5035
  const state = ctx.state;
4980
5036
  const { scrollProcessingEnabled } = state;
4981
5037
  if (scrollProcessingEnabled === false) {
@@ -4997,6 +5053,13 @@ function onScroll(ctx, event) {
4997
5053
  if (state.props.horizontal) {
4998
5054
  newScroll = toLogicalHorizontalOffset(state, newScroll, (_e = event.nativeEvent.contentSize) == null ? void 0 : _e.width);
4999
5055
  }
5056
+ const isFinishedEndInitialScroll = state.didFinishInitialScroll && ((_f = state.initialScroll) == null ? void 0 : _f.viewPosition) === 1 && state.scroll > state.scrollLength;
5057
+ const shouldIgnoreNegativeInsetChange = Platform.OS !== "web" && insetChanged && newScroll < 0 && isFinishedEndInitialScroll;
5058
+ if (shouldIgnoreNegativeInsetChange) {
5059
+ return;
5060
+ }
5061
+ state.lastNativeScroll = newScroll;
5062
+ state.lastNativeScrollTime = Date.now();
5000
5063
  if (state.scrollingTo && state.scrollingTo.offset >= newScroll) {
5001
5064
  const maxOffset = clampScrollOffset(ctx, newScroll, state.scrollingTo);
5002
5065
  if (newScroll !== maxOffset && Math.abs(newScroll - maxOffset) > 1) {
@@ -5637,6 +5700,8 @@ function getAlwaysRenderIndices(config, data, keyExtractor, anchoredEndSpaceAnch
5637
5700
  indices.sort(sortAsc);
5638
5701
  return indices;
5639
5702
  }
5703
+
5704
+ // src/utils/getRenderedItem.ts
5640
5705
  function getRenderedItem(ctx, key) {
5641
5706
  var _a3;
5642
5707
  const state = ctx.state;
@@ -5662,7 +5727,7 @@ function getRenderedItem(ctx, key) {
5662
5727
  item,
5663
5728
  type: getItemType ? (_a3 = getItemType(item, index)) != null ? _a3 : "" : ""
5664
5729
  };
5665
- renderedItem = React2__default.createElement(renderItem, itemProps);
5730
+ renderedItem = renderItem(itemProps);
5666
5731
  }
5667
5732
  return { index, item: data[index], renderedItem };
5668
5733
  }
@@ -6372,6 +6437,8 @@ var internal = {
6372
6437
  typedMemo,
6373
6438
  useArr$,
6374
6439
  useCombinedRef,
6440
+ useLatestRef,
6441
+ useStableRenderComponent,
6375
6442
  useStateContext
6376
6443
  };
6377
6444
 
@@ -120,13 +120,11 @@ interface DataModeProps<ItemT, TItemType extends string | undefined> {
120
120
  */
121
121
  data: ReadonlyArray<ItemT>;
122
122
  /**
123
- * Function or React component to render each item in the list.
124
- * Can be either:
125
- * - A function: (props: LegendListRenderItemProps<ItemT>) => ReactNode
126
- * - A React component: React.ComponentType<LegendListRenderItemProps<ItemT>>
123
+ * Callback to render each item in the list.
124
+ * To use hooks in an item component, return that component from this callback.
127
125
  * @required when using data mode
128
126
  */
129
- renderItem: ((props: LegendListRenderItemProps<ItemT, TItemType>) => React.ReactNode) | React.ComponentType<LegendListRenderItemProps<ItemT, TItemType>>;
127
+ renderItem: (props: LegendListRenderItemProps<ItemT, TItemType>) => React.ReactNode;
130
128
  children?: never;
131
129
  }
132
130
  interface ChildrenModeProps {