@legendapp/list 2.0.14 → 2.0.16

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,12 @@
1
+ ## 2.0.16
2
+ - Feat: Add KeyboardAvoidingLegendList component for better keyboard handling integration
3
+ - Fix: Stale containers are not being removed and overlap with new data when using getItemType #335
4
+ - Fix: Suppress keyExtractor warning when using lazy list mode #330
5
+
6
+ ## 2.0.15
7
+ - Fix: Container allocation for sticky headers could duplicate containers, causing rendering issues
8
+ - Fix: Sticky positioned components scrolling out of viewport after scrolling distance exceeded 5000
9
+
1
10
  ## 2.0.14
2
11
  - Feat: Add dataVersion prop to trigger re-render when mutating the data array in place
3
12
 
package/index.d.mts CHANGED
@@ -246,6 +246,7 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> {
246
246
  refreshing?: boolean;
247
247
  /**
248
248
  * Render custom ScrollView component.
249
+ * Note: When using `stickyHeaderIndices`, you must provide an Animated ScrollView component.
249
250
  * @default (props) => <ScrollView {...props} />
250
251
  */
251
252
  renderScrollComponent?: (props: ScrollViewProps) => React.ReactElement<ScrollViewProps>;
package/index.d.ts CHANGED
@@ -246,6 +246,7 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> {
246
246
  refreshing?: boolean;
247
247
  /**
248
248
  * Render custom ScrollView component.
249
+ * Note: When using `stickyHeaderIndices`, you must provide an Animated ScrollView component.
249
250
  * @default (props) => <ScrollView {...props} />
250
251
  */
251
252
  renderScrollComponent?: (props: ScrollViewProps) => React.ReactElement<ScrollViewProps>;
package/index.js CHANGED
@@ -286,7 +286,8 @@ var PositionViewSticky = typedMemo(function PositionViewSticky2({
286
286
  const transform = React2__namespace.useMemo(() => {
287
287
  if (animatedScrollY && stickyOffset !== void 0) {
288
288
  const stickyPosition = animatedScrollY.interpolate({
289
- extrapolate: "clamp",
289
+ extrapolateLeft: "clamp",
290
+ extrapolateRight: "extend",
290
291
  inputRange: [position + headerSize, position + 5e3 + headerSize],
291
292
  outputRange: [position, position + 5e3]
292
293
  });
@@ -1549,7 +1550,7 @@ function findAvailableContainers(ctx, state, numNeeded, startBuffered, endBuffer
1549
1550
  for (const containerIndex of stickyContainerPool) {
1550
1551
  const key = peek$(ctx, `containerItemKey${containerIndex}`);
1551
1552
  const isPendingRemoval = pendingRemovalSet.has(containerIndex);
1552
- if ((key === void 0 || isPendingRemoval) && canReuseContainer(containerIndex, requiredType)) {
1553
+ if ((key === void 0 || isPendingRemoval) && canReuseContainer(containerIndex, requiredType) && !result.includes(containerIndex)) {
1553
1554
  result.push(containerIndex);
1554
1555
  if (isPendingRemoval && pendingRemovalSet.delete(containerIndex)) {
1555
1556
  pendingRemovalChanged = true;
@@ -1571,14 +1572,14 @@ function findAvailableContainers(ctx, state, numNeeded, startBuffered, endBuffer
1571
1572
  continue;
1572
1573
  }
1573
1574
  const key = peek$(ctx, `containerItemKey${u}`);
1574
- let isOk = key === void 0;
1575
- if (!isOk && pendingRemovalSet.has(u)) {
1576
- pendingRemovalSet.delete(u);
1577
- pendingRemovalChanged = true;
1578
- const requiredType = neededTypes[typeIndex];
1579
- isOk = canReuseContainer(u, requiredType);
1580
- }
1581
- if (isOk) {
1575
+ const requiredType = neededTypes[typeIndex];
1576
+ const isPending = key !== void 0 && pendingRemovalSet.has(u);
1577
+ const canUse = key === void 0 || isPending && canReuseContainer(u, requiredType);
1578
+ if (canUse) {
1579
+ if (isPending) {
1580
+ pendingRemovalSet.delete(u);
1581
+ pendingRemovalChanged = true;
1582
+ }
1582
1583
  result.push(u);
1583
1584
  if (requiredItemTypes) {
1584
1585
  typeIndex++;
@@ -2721,6 +2722,7 @@ var LegendList = typedMemo(
2721
2722
  const isChildrenMode = children !== void 0 && dataProp === void 0;
2722
2723
  const processedProps = isChildrenMode ? {
2723
2724
  ...restProps,
2725
+ childrenMode: true,
2724
2726
  data: (isArray(children) ? children : React2__namespace.Children.toArray(children)).flat(1),
2725
2727
  renderItem: ({ item }) => item
2726
2728
  } : {
@@ -2787,6 +2789,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
2787
2789
  waitForInitialLayout = true,
2788
2790
  ...rest
2789
2791
  } = props;
2792
+ const { childrenMode } = rest;
2790
2793
  const [renderNum, setRenderNum] = React2.useState(0);
2791
2794
  const initialScroll = initialScrollIndexProp || initialScrollOffsetProp ? typeof initialScrollIndexProp === "object" ? { index: initialScrollIndexProp.index || 0, viewOffset: initialScrollIndexProp.viewOffset || 0 } : { index: initialScrollIndexProp || 0, viewOffset: initialScrollOffsetProp || 0 } : void 0;
2792
2795
  const [canRender, setCanRender] = React2__namespace.useState(!IsNewArchitecture);
@@ -2959,7 +2962,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
2959
2962
  if (isFirst || didDataChange || numColumnsProp !== peek$(ctx, "numColumns")) {
2960
2963
  refState.current.lastBatchingAction = Date.now();
2961
2964
  if (!keyExtractorProp && !isFirst && didDataChange) {
2962
- __DEV__ && warnDevOnce(
2965
+ __DEV__ && !childrenMode && warnDevOnce(
2963
2966
  "keyExtractor",
2964
2967
  "Changing data without a keyExtractor can cause slow performance and resetting scroll. If your list data can change you should use a keyExtractor with a unique id for best performance and behavior."
2965
2968
  );
package/index.mjs CHANGED
@@ -265,7 +265,8 @@ var PositionViewSticky = typedMemo(function PositionViewSticky2({
265
265
  const transform = React2.useMemo(() => {
266
266
  if (animatedScrollY && stickyOffset !== void 0) {
267
267
  const stickyPosition = animatedScrollY.interpolate({
268
- extrapolate: "clamp",
268
+ extrapolateLeft: "clamp",
269
+ extrapolateRight: "extend",
269
270
  inputRange: [position + headerSize, position + 5e3 + headerSize],
270
271
  outputRange: [position, position + 5e3]
271
272
  });
@@ -1528,7 +1529,7 @@ function findAvailableContainers(ctx, state, numNeeded, startBuffered, endBuffer
1528
1529
  for (const containerIndex of stickyContainerPool) {
1529
1530
  const key = peek$(ctx, `containerItemKey${containerIndex}`);
1530
1531
  const isPendingRemoval = pendingRemovalSet.has(containerIndex);
1531
- if ((key === void 0 || isPendingRemoval) && canReuseContainer(containerIndex, requiredType)) {
1532
+ if ((key === void 0 || isPendingRemoval) && canReuseContainer(containerIndex, requiredType) && !result.includes(containerIndex)) {
1532
1533
  result.push(containerIndex);
1533
1534
  if (isPendingRemoval && pendingRemovalSet.delete(containerIndex)) {
1534
1535
  pendingRemovalChanged = true;
@@ -1550,14 +1551,14 @@ function findAvailableContainers(ctx, state, numNeeded, startBuffered, endBuffer
1550
1551
  continue;
1551
1552
  }
1552
1553
  const key = peek$(ctx, `containerItemKey${u}`);
1553
- let isOk = key === void 0;
1554
- if (!isOk && pendingRemovalSet.has(u)) {
1555
- pendingRemovalSet.delete(u);
1556
- pendingRemovalChanged = true;
1557
- const requiredType = neededTypes[typeIndex];
1558
- isOk = canReuseContainer(u, requiredType);
1559
- }
1560
- if (isOk) {
1554
+ const requiredType = neededTypes[typeIndex];
1555
+ const isPending = key !== void 0 && pendingRemovalSet.has(u);
1556
+ const canUse = key === void 0 || isPending && canReuseContainer(u, requiredType);
1557
+ if (canUse) {
1558
+ if (isPending) {
1559
+ pendingRemovalSet.delete(u);
1560
+ pendingRemovalChanged = true;
1561
+ }
1561
1562
  result.push(u);
1562
1563
  if (requiredItemTypes) {
1563
1564
  typeIndex++;
@@ -2700,6 +2701,7 @@ var LegendList = typedMemo(
2700
2701
  const isChildrenMode = children !== void 0 && dataProp === void 0;
2701
2702
  const processedProps = isChildrenMode ? {
2702
2703
  ...restProps,
2704
+ childrenMode: true,
2703
2705
  data: (isArray(children) ? children : React2.Children.toArray(children)).flat(1),
2704
2706
  renderItem: ({ item }) => item
2705
2707
  } : {
@@ -2766,6 +2768,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
2766
2768
  waitForInitialLayout = true,
2767
2769
  ...rest
2768
2770
  } = props;
2771
+ const { childrenMode } = rest;
2769
2772
  const [renderNum, setRenderNum] = useState(0);
2770
2773
  const initialScroll = initialScrollIndexProp || initialScrollOffsetProp ? typeof initialScrollIndexProp === "object" ? { index: initialScrollIndexProp.index || 0, viewOffset: initialScrollIndexProp.viewOffset || 0 } : { index: initialScrollIndexProp || 0, viewOffset: initialScrollOffsetProp || 0 } : void 0;
2771
2774
  const [canRender, setCanRender] = React2.useState(!IsNewArchitecture);
@@ -2938,7 +2941,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
2938
2941
  if (isFirst || didDataChange || numColumnsProp !== peek$(ctx, "numColumns")) {
2939
2942
  refState.current.lastBatchingAction = Date.now();
2940
2943
  if (!keyExtractorProp && !isFirst && didDataChange) {
2941
- __DEV__ && warnDevOnce(
2944
+ __DEV__ && !childrenMode && warnDevOnce(
2942
2945
  "keyExtractor",
2943
2946
  "Changing data without a keyExtractor can cause slow performance and resetting scroll. If your list data can change you should use a keyExtractor with a unique id for best performance and behavior."
2944
2947
  );
package/keyboard.d.mts ADDED
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+ import { Insets } from 'react-native';
3
+ import { ReanimatedScrollEvent } from 'react-native-reanimated/lib/typescript/hook/commonTypes';
4
+ import { LegendListRef } from '@legendapp/list';
5
+ import { AnimatedLegendListProps } from '@legendapp/list/reanimated';
6
+
7
+ declare const KeyboardAvoidingLegendList: <ItemT>(props: Omit<AnimatedLegendListProps<ItemT>, "contentInset" | "onScroll"> & {
8
+ onScroll?: (event: ReanimatedScrollEvent) => void;
9
+ contentInset?: Insets;
10
+ safeAreaInsetBottom?: number;
11
+ } & React.RefAttributes<LegendListRef>) => React.ReactNode;
12
+
13
+ export { KeyboardAvoidingLegendList, KeyboardAvoidingLegendList as LegendList };
package/keyboard.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+ import { Insets } from 'react-native';
3
+ import { ReanimatedScrollEvent } from 'react-native-reanimated/lib/typescript/hook/commonTypes';
4
+ import { LegendListRef } from '@legendapp/list';
5
+ import { AnimatedLegendListProps } from '@legendapp/list/reanimated';
6
+
7
+ declare const KeyboardAvoidingLegendList: <ItemT>(props: Omit<AnimatedLegendListProps<ItemT>, "contentInset" | "onScroll"> & {
8
+ onScroll?: (event: ReanimatedScrollEvent) => void;
9
+ contentInset?: Insets;
10
+ safeAreaInsetBottom?: number;
11
+ } & React.RefAttributes<LegendListRef>) => React.ReactNode;
12
+
13
+ export { KeyboardAvoidingLegendList, KeyboardAvoidingLegendList as LegendList };
package/keyboard.js ADDED
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var reactNative = require('react-native');
5
+ var reactNativeKeyboardController = require('react-native-keyboard-controller');
6
+ var reactNativeReanimated = require('react-native-reanimated');
7
+ var reanimated = require('@legendapp/list/reanimated');
8
+
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ var n = Object.create(null);
12
+ if (e) {
13
+ Object.keys(e).forEach(function (k) {
14
+ if (k !== 'default') {
15
+ var d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: function () { return e[k]; }
19
+ });
20
+ }
21
+ });
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+
27
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
28
+
29
+ // src/integrations/keyboard.tsx
30
+ var KeyboardAvoidingLegendList = React.forwardRef(function KeyboardAvoidingLegendList2(props, forwardedRef) {
31
+ const {
32
+ contentInset: contentInsetProp,
33
+ horizontal,
34
+ onScroll: onScrollProp,
35
+ scrollEventThrottle,
36
+ safeAreaInsetBottom = 0,
37
+ ...rest
38
+ } = props;
39
+ const resolvedScrollEventThrottle = scrollEventThrottle != null ? scrollEventThrottle : 16;
40
+ const scrollViewRef = reactNativeReanimated.useAnimatedRef();
41
+ const scrollOffsetY = reactNativeReanimated.useSharedValue(0);
42
+ const scrollOffsetAtKeyboardStart = reactNativeReanimated.useSharedValue(0);
43
+ const keyboardInset = reactNativeReanimated.useSharedValue(0);
44
+ const scrollHandler = reactNativeReanimated.useAnimatedScrollHandler(
45
+ (event) => {
46
+ scrollOffsetY.value = event.contentOffset[horizontal ? "x" : "y"];
47
+ if (onScrollProp) {
48
+ reactNativeReanimated.runOnJS(onScrollProp)(event);
49
+ }
50
+ },
51
+ [onScrollProp, horizontal]
52
+ );
53
+ reactNativeKeyboardController.useKeyboardHandler(
54
+ // biome-ignore assist/source/useSortedKeys: prefer start/move/end
55
+ {
56
+ onStart: () => {
57
+ "worklet";
58
+ scrollOffsetAtKeyboardStart.value = scrollOffsetY.value;
59
+ },
60
+ onMove: (event) => {
61
+ "worklet";
62
+ const targetOffset = scrollOffsetAtKeyboardStart.value + event.height;
63
+ scrollOffsetY.value = targetOffset;
64
+ reactNativeReanimated.scrollTo(scrollViewRef, 0, targetOffset, false);
65
+ if (!horizontal) {
66
+ keyboardInset.value = Math.max(0, event.height - safeAreaInsetBottom);
67
+ }
68
+ },
69
+ onEnd: (event) => {
70
+ "worklet";
71
+ const targetOffset = scrollOffsetAtKeyboardStart.value + event.height;
72
+ scrollOffsetY.value = targetOffset;
73
+ reactNativeReanimated.scrollTo(scrollViewRef, 0, targetOffset, false);
74
+ if (!horizontal) {
75
+ keyboardInset.value = Math.max(0, event.height - safeAreaInsetBottom);
76
+ }
77
+ }
78
+ },
79
+ [scrollViewRef, safeAreaInsetBottom]
80
+ );
81
+ const animatedProps = reactNative.Platform.OS === "ios" ? reactNativeReanimated.useAnimatedProps(() => {
82
+ "worklet";
83
+ var _a, _b, _c, _d;
84
+ return {
85
+ contentInset: {
86
+ bottom: ((_a = contentInsetProp == null ? void 0 : contentInsetProp.bottom) != null ? _a : 0) + (horizontal ? 0 : keyboardInset.value),
87
+ left: (_b = contentInsetProp == null ? void 0 : contentInsetProp.left) != null ? _b : 0,
88
+ right: (_c = contentInsetProp == null ? void 0 : contentInsetProp.right) != null ? _c : 0,
89
+ top: (_d = contentInsetProp == null ? void 0 : contentInsetProp.top) != null ? _d : 0
90
+ }
91
+ };
92
+ }) : void 0;
93
+ const style = reactNative.Platform.OS !== "ios" ? reactNativeReanimated.useAnimatedStyle(() => ({
94
+ marginBottom: keyboardInset.value
95
+ })) : void 0;
96
+ return /* @__PURE__ */ React__namespace.createElement(
97
+ reanimated.AnimatedLegendList,
98
+ {
99
+ ...rest,
100
+ animatedProps,
101
+ keyboardDismissMode: "interactive",
102
+ onScroll: scrollHandler,
103
+ ref: forwardedRef,
104
+ refScrollView: scrollViewRef,
105
+ scrollEventThrottle: resolvedScrollEventThrottle,
106
+ scrollIndicatorInsets: { bottom: 0, top: 0 },
107
+ style
108
+ }
109
+ );
110
+ });
111
+
112
+ exports.KeyboardAvoidingLegendList = KeyboardAvoidingLegendList;
113
+ exports.LegendList = KeyboardAvoidingLegendList;
package/keyboard.mjs ADDED
@@ -0,0 +1,91 @@
1
+ import * as React from 'react';
2
+ import { forwardRef } from 'react';
3
+ import { Platform } from 'react-native';
4
+ import { useKeyboardHandler } from 'react-native-keyboard-controller';
5
+ import { useAnimatedRef, useSharedValue, useAnimatedScrollHandler, runOnJS, scrollTo, useAnimatedProps, useAnimatedStyle } from 'react-native-reanimated';
6
+ import { AnimatedLegendList } from '@legendapp/list/reanimated';
7
+
8
+ // src/integrations/keyboard.tsx
9
+ var KeyboardAvoidingLegendList = forwardRef(function KeyboardAvoidingLegendList2(props, forwardedRef) {
10
+ const {
11
+ contentInset: contentInsetProp,
12
+ horizontal,
13
+ onScroll: onScrollProp,
14
+ scrollEventThrottle,
15
+ safeAreaInsetBottom = 0,
16
+ ...rest
17
+ } = props;
18
+ const resolvedScrollEventThrottle = scrollEventThrottle != null ? scrollEventThrottle : 16;
19
+ const scrollViewRef = useAnimatedRef();
20
+ const scrollOffsetY = useSharedValue(0);
21
+ const scrollOffsetAtKeyboardStart = useSharedValue(0);
22
+ const keyboardInset = useSharedValue(0);
23
+ const scrollHandler = useAnimatedScrollHandler(
24
+ (event) => {
25
+ scrollOffsetY.value = event.contentOffset[horizontal ? "x" : "y"];
26
+ if (onScrollProp) {
27
+ runOnJS(onScrollProp)(event);
28
+ }
29
+ },
30
+ [onScrollProp, horizontal]
31
+ );
32
+ useKeyboardHandler(
33
+ // biome-ignore assist/source/useSortedKeys: prefer start/move/end
34
+ {
35
+ onStart: () => {
36
+ "worklet";
37
+ scrollOffsetAtKeyboardStart.value = scrollOffsetY.value;
38
+ },
39
+ onMove: (event) => {
40
+ "worklet";
41
+ const targetOffset = scrollOffsetAtKeyboardStart.value + event.height;
42
+ scrollOffsetY.value = targetOffset;
43
+ scrollTo(scrollViewRef, 0, targetOffset, false);
44
+ if (!horizontal) {
45
+ keyboardInset.value = Math.max(0, event.height - safeAreaInsetBottom);
46
+ }
47
+ },
48
+ onEnd: (event) => {
49
+ "worklet";
50
+ const targetOffset = scrollOffsetAtKeyboardStart.value + event.height;
51
+ scrollOffsetY.value = targetOffset;
52
+ scrollTo(scrollViewRef, 0, targetOffset, false);
53
+ if (!horizontal) {
54
+ keyboardInset.value = Math.max(0, event.height - safeAreaInsetBottom);
55
+ }
56
+ }
57
+ },
58
+ [scrollViewRef, safeAreaInsetBottom]
59
+ );
60
+ const animatedProps = Platform.OS === "ios" ? useAnimatedProps(() => {
61
+ "worklet";
62
+ var _a, _b, _c, _d;
63
+ return {
64
+ contentInset: {
65
+ bottom: ((_a = contentInsetProp == null ? void 0 : contentInsetProp.bottom) != null ? _a : 0) + (horizontal ? 0 : keyboardInset.value),
66
+ left: (_b = contentInsetProp == null ? void 0 : contentInsetProp.left) != null ? _b : 0,
67
+ right: (_c = contentInsetProp == null ? void 0 : contentInsetProp.right) != null ? _c : 0,
68
+ top: (_d = contentInsetProp == null ? void 0 : contentInsetProp.top) != null ? _d : 0
69
+ }
70
+ };
71
+ }) : void 0;
72
+ const style = Platform.OS !== "ios" ? useAnimatedStyle(() => ({
73
+ marginBottom: keyboardInset.value
74
+ })) : void 0;
75
+ return /* @__PURE__ */ React.createElement(
76
+ AnimatedLegendList,
77
+ {
78
+ ...rest,
79
+ animatedProps,
80
+ keyboardDismissMode: "interactive",
81
+ onScroll: scrollHandler,
82
+ ref: forwardedRef,
83
+ refScrollView: scrollViewRef,
84
+ scrollEventThrottle: resolvedScrollEventThrottle,
85
+ scrollIndicatorInsets: { bottom: 0, top: 0 },
86
+ style
87
+ }
88
+ );
89
+ });
90
+
91
+ export { KeyboardAvoidingLegendList, KeyboardAvoidingLegendList as LegendList };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/list",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
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,