@legendapp/list 2.0.0-next.1 → 2.0.0-next.2

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.
Files changed (260) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.cursor/rules/changelog.mdc +60 -0
  3. package/.github/FUNDING.yml +15 -0
  4. package/.gitignore +5 -0
  5. package/.prettierrc.json +5 -0
  6. package/.vscode/settings.json +14 -0
  7. package/CLAUDE.md +126 -0
  8. package/biome.json +46 -0
  9. package/bun.lock +1289 -0
  10. package/bunfig.toml +2 -0
  11. package/dist/CHANGELOG.md +119 -0
  12. package/dist/LICENSE +21 -0
  13. package/dist/README.md +139 -0
  14. package/{animated.d.mts → dist/animated.d.mts} +1 -1
  15. package/{animated.d.ts → dist/animated.d.ts} +1 -1
  16. package/{index.d.mts → dist/index.d.mts} +16 -10
  17. package/{index.d.ts → dist/index.d.ts} +16 -10
  18. package/{index.js → dist/index.js} +52 -32
  19. package/{index.mjs → dist/index.mjs} +52 -32
  20. package/{keyboard-controller.d.mts → dist/keyboard-controller.d.mts} +4 -4
  21. package/{keyboard-controller.d.ts → dist/keyboard-controller.d.ts} +4 -4
  22. package/dist/package.json +35 -0
  23. package/example/README.md +40 -0
  24. package/example/api/data/genres.json +23 -0
  25. package/example/api/data/playlist/10402-10749.json +1 -0
  26. package/example/api/data/playlist/10402-10770.json +1 -0
  27. package/example/api/data/playlist/10402-37.json +1 -0
  28. package/example/api/data/playlist/10749-10752.json +1 -0
  29. package/example/api/data/playlist/10749-10770.json +1 -0
  30. package/example/api/data/playlist/10749-37.json +1 -0
  31. package/example/api/data/playlist/10749-878.json +1 -0
  32. package/example/api/data/playlist/10751-10402.json +1 -0
  33. package/example/api/data/playlist/10751-10752.json +1 -0
  34. package/example/api/data/playlist/10751-37.json +1 -0
  35. package/example/api/data/playlist/10751-53.json +1 -0
  36. package/example/api/data/playlist/10751-878.json +1 -0
  37. package/example/api/data/playlist/10751-9648.json +1 -0
  38. package/example/api/data/playlist/10752-37.json +1 -0
  39. package/example/api/data/playlist/12-10402.json +1 -0
  40. package/example/api/data/playlist/12-10749.json +1 -0
  41. package/example/api/data/playlist/12-18.json +1 -0
  42. package/example/api/data/playlist/12-27.json +1 -0
  43. package/example/api/data/playlist/12-35.json +1 -0
  44. package/example/api/data/playlist/14-36.json +1 -0
  45. package/example/api/data/playlist/14-878.json +1 -0
  46. package/example/api/data/playlist/16-10751.json +1 -0
  47. package/example/api/data/playlist/16-10770.json +1 -0
  48. package/example/api/data/playlist/16-35.json +1 -0
  49. package/example/api/data/playlist/16-36.json +1 -0
  50. package/example/api/data/playlist/16-53.json +1 -0
  51. package/example/api/data/playlist/18-10751.json +1 -0
  52. package/example/api/data/playlist/18-10752.json +1 -0
  53. package/example/api/data/playlist/18-37.json +1 -0
  54. package/example/api/data/playlist/18-53.json +1 -0
  55. package/example/api/data/playlist/18-878.json +1 -0
  56. package/example/api/data/playlist/27-10749.json +1 -0
  57. package/example/api/data/playlist/27-10770.json +1 -0
  58. package/example/api/data/playlist/28-10749.json +1 -0
  59. package/example/api/data/playlist/28-10751.json +1 -0
  60. package/example/api/data/playlist/28-10770.json +1 -0
  61. package/example/api/data/playlist/28-16.json +1 -0
  62. package/example/api/data/playlist/28-18.json +1 -0
  63. package/example/api/data/playlist/28-36.json +1 -0
  64. package/example/api/data/playlist/28-37.json +1 -0
  65. package/example/api/data/playlist/28-53.json +1 -0
  66. package/example/api/data/playlist/28-80.json +1 -0
  67. package/example/api/data/playlist/28-99.json +1 -0
  68. package/example/api/data/playlist/35-10749.json +1 -0
  69. package/example/api/data/playlist/35-10751.json +1 -0
  70. package/example/api/data/playlist/35-10752.json +1 -0
  71. package/example/api/data/playlist/35-27.json +1 -0
  72. package/example/api/data/playlist/35-36.json +1 -0
  73. package/example/api/data/playlist/35-53.json +1 -0
  74. package/example/api/data/playlist/35-80.json +1 -0
  75. package/example/api/data/playlist/36-37.json +1 -0
  76. package/example/api/data/playlist/36-878.json +1 -0
  77. package/example/api/data/playlist/36-9648.json +1 -0
  78. package/example/api/data/playlist/53-10752.json +1 -0
  79. package/example/api/data/playlist/80-10770.json +1 -0
  80. package/example/api/data/playlist/80-14.json +1 -0
  81. package/example/api/data/playlist/80-18.json +1 -0
  82. package/example/api/data/playlist/80-37.json +1 -0
  83. package/example/api/data/playlist/878-37.json +1 -0
  84. package/example/api/data/playlist/9648-10770.json +1 -0
  85. package/example/api/data/playlist/9648-37.json +1 -0
  86. package/example/api/data/playlist/9648-53.json +1 -0
  87. package/example/api/data/playlist/9648-878.json +1 -0
  88. package/example/api/data/playlist/99-10749.json +1 -0
  89. package/example/api/data/playlist/99-14.json +1 -0
  90. package/example/api/data/playlist/99-18.json +1 -0
  91. package/example/api/data/playlist/99-27.json +1 -0
  92. package/example/api/data/playlist/99-53.json +1 -0
  93. package/example/api/data/playlist/99-9648.json +1 -0
  94. package/example/api/data/playlist/index.ts +73 -0
  95. package/example/api/data/rows.json +1 -0
  96. package/example/api/index.ts +36 -0
  97. package/example/app/(tabs)/_layout.tsx +60 -0
  98. package/example/app/(tabs)/cards.tsx +81 -0
  99. package/example/app/(tabs)/index.tsx +205 -0
  100. package/example/app/(tabs)/moviesL.tsx +7 -0
  101. package/example/app/(tabs)/moviesLR.tsx +7 -0
  102. package/example/app/+not-found.tsx +32 -0
  103. package/example/app/_layout.tsx +34 -0
  104. package/example/app/accurate-scrollto/index.tsx +125 -0
  105. package/example/app/accurate-scrollto-2/index.tsx +52 -0
  106. package/example/app/accurate-scrollto-huge/index.tsx +128 -0
  107. package/example/app/add-to-end/index.tsx +82 -0
  108. package/example/app/ai-chat/index.tsx +236 -0
  109. package/example/app/bidirectional-infinite-list/index.tsx +133 -0
  110. package/example/app/cards-columns/index.tsx +37 -0
  111. package/example/app/cards-flashlist/index.tsx +122 -0
  112. package/example/app/cards-flatlist/index.tsx +94 -0
  113. package/example/app/cards-no-recycle/index.tsx +110 -0
  114. package/example/app/cards-renderItem.tsx +354 -0
  115. package/example/app/chat-example/index.tsx +167 -0
  116. package/example/app/chat-infinite/index.tsx +239 -0
  117. package/example/app/chat-keyboard/index.tsx +248 -0
  118. package/example/app/chat-resize-outer/index.tsx +247 -0
  119. package/example/app/columns/index.tsx +78 -0
  120. package/example/app/countries/index.tsx +182 -0
  121. package/example/app/countries-flashlist/index.tsx +163 -0
  122. package/example/app/countries-reorder/index.tsx +187 -0
  123. package/example/app/extra-data/index.tsx +86 -0
  124. package/example/app/filter-elements/filter-data-provider.tsx +55 -0
  125. package/example/app/filter-elements/index.tsx +118 -0
  126. package/example/app/initial-scroll-index/index.tsx +106 -0
  127. package/example/app/initial-scroll-index/renderFixedItem.tsx +215 -0
  128. package/example/app/initial-scroll-index-free-height/index.tsx +70 -0
  129. package/example/app/initial-scroll-index-keyed/index.tsx +62 -0
  130. package/example/app/lazy-list/index.tsx +123 -0
  131. package/example/app/movies-flashlist/index.tsx +7 -0
  132. package/example/app/mutable-cells/index.tsx +104 -0
  133. package/example/app/video-feed/index.tsx +119 -0
  134. package/example/app.config.js +22 -0
  135. package/example/app.json +45 -0
  136. package/example/assets/fonts/SpaceMono-Regular.ttf +0 -0
  137. package/example/assets/images/adaptive-icon.png +0 -0
  138. package/example/assets/images/favicon.png +0 -0
  139. package/example/assets/images/icon.png +0 -0
  140. package/example/assets/images/partial-react-logo.png +0 -0
  141. package/example/assets/images/react-logo.png +0 -0
  142. package/example/assets/images/react-logo@2x.png +0 -0
  143. package/example/assets/images/react-logo@3x.png +0 -0
  144. package/example/assets/images/splash-icon.png +0 -0
  145. package/example/autoscroll.sh +101 -0
  146. package/example/bun.lock +2266 -0
  147. package/example/bunfig.toml +2 -0
  148. package/example/components/Breathe.tsx +54 -0
  149. package/example/components/Circle.tsx +69 -0
  150. package/example/components/Collapsible.tsx +44 -0
  151. package/example/components/ExternalLink.tsx +24 -0
  152. package/example/components/HapticTab.tsx +18 -0
  153. package/example/components/HelloWave.tsx +37 -0
  154. package/example/components/Movies.tsx +179 -0
  155. package/example/components/ParallaxScrollView.tsx +81 -0
  156. package/example/components/ThemedText.tsx +60 -0
  157. package/example/components/ThemedView.tsx +14 -0
  158. package/example/components/__tests__/ThemedText-test.tsx +10 -0
  159. package/example/components/__tests__/__snapshots__/ThemedText-test.tsx.snap +24 -0
  160. package/example/components/ui/IconSymbol.ios.tsx +32 -0
  161. package/example/components/ui/IconSymbol.tsx +43 -0
  162. package/example/components/ui/TabBarBackground.ios.tsx +22 -0
  163. package/example/components/ui/TabBarBackground.tsx +6 -0
  164. package/example/constants/Colors.ts +26 -0
  165. package/example/constants/constants.ts +5 -0
  166. package/example/constants/useScrollTest.ts +19 -0
  167. package/example/hooks/useColorScheme.ts +1 -0
  168. package/example/hooks/useColorScheme.web.ts +8 -0
  169. package/example/hooks/useThemeColor.ts +22 -0
  170. package/example/ios/.xcode.env +11 -0
  171. package/example/ios/Podfile +64 -0
  172. package/example/ios/Podfile.lock +2767 -0
  173. package/example/ios/Podfile.properties.json +5 -0
  174. package/example/ios/listtest/AppDelegate.swift +70 -0
  175. package/example/ios/listtest/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
  176. package/example/ios/listtest/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
  177. package/example/ios/listtest/Images.xcassets/Contents.json +6 -0
  178. package/example/ios/listtest/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
  179. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/Contents.json +23 -0
  180. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/image.png +0 -0
  181. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/image@2x.png +0 -0
  182. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/image@3x.png +0 -0
  183. package/example/ios/listtest/Info.plist +85 -0
  184. package/example/ios/listtest/PrivacyInfo.xcprivacy +48 -0
  185. package/example/ios/listtest/SplashScreen.storyboard +42 -0
  186. package/example/ios/listtest/Supporting/Expo.plist +12 -0
  187. package/example/ios/listtest/listtest-Bridging-Header.h +3 -0
  188. package/example/ios/listtest/listtest.entitlements +5 -0
  189. package/example/ios/listtest.xcodeproj/project.pbxproj +547 -0
  190. package/example/ios/listtest.xcodeproj/xcshareddata/xcschemes/listtest.xcscheme +88 -0
  191. package/example/ios/listtest.xcworkspace/contents.xcworkspacedata +10 -0
  192. package/example/metro.config.js +16 -0
  193. package/example/package.json +73 -0
  194. package/example/scripts/reset-project.js +84 -0
  195. package/example/tsconfig.json +26 -0
  196. package/package.json +88 -34
  197. package/posttsup.ts +24 -0
  198. package/src/Container.tsx +176 -0
  199. package/src/Containers.tsx +85 -0
  200. package/src/ContextContainer.ts +145 -0
  201. package/src/DebugView.tsx +83 -0
  202. package/src/LazyLegendList.tsx +41 -0
  203. package/src/LeanView.tsx +18 -0
  204. package/src/LegendList.tsx +558 -0
  205. package/src/ListComponent.tsx +191 -0
  206. package/src/ScrollAdjust.tsx +24 -0
  207. package/src/ScrollAdjustHandler.ts +26 -0
  208. package/src/Separator.tsx +14 -0
  209. package/src/animated.tsx +6 -0
  210. package/src/calculateItemsInView.ts +363 -0
  211. package/src/calculateOffsetForIndex.ts +23 -0
  212. package/src/calculateOffsetWithOffsetPosition.ts +26 -0
  213. package/src/checkAllSizesKnown.ts +17 -0
  214. package/src/checkAtBottom.ts +36 -0
  215. package/src/checkAtTop.ts +27 -0
  216. package/src/checkThreshold.ts +30 -0
  217. package/src/constants.ts +11 -0
  218. package/src/createColumnWrapperStyle.ts +16 -0
  219. package/src/doInitialAllocateContainers.ts +40 -0
  220. package/src/doMaintainScrollAtEnd.ts +34 -0
  221. package/src/findAvailableContainers.ts +98 -0
  222. package/src/finishScrollTo.ts +8 -0
  223. package/src/getId.ts +21 -0
  224. package/src/getItemSize.ts +52 -0
  225. package/src/getRenderedItem.ts +34 -0
  226. package/src/getScrollVelocity.ts +47 -0
  227. package/src/handleLayout.ts +70 -0
  228. package/src/helpers.ts +39 -0
  229. package/src/index.ts +11 -0
  230. package/src/keyboard-controller.tsx +63 -0
  231. package/src/onScroll.ts +66 -0
  232. package/src/prepareMVCP.ts +50 -0
  233. package/src/reanimated.tsx +63 -0
  234. package/src/requestAdjust.ts +41 -0
  235. package/src/scrollTo.ts +40 -0
  236. package/src/scrollToIndex.ts +34 -0
  237. package/src/setDidLayout.ts +25 -0
  238. package/src/setPaddingTop.ts +28 -0
  239. package/src/state.tsx +304 -0
  240. package/src/types.ts +610 -0
  241. package/src/updateAlignItemsPaddingTop.ts +18 -0
  242. package/src/updateAllPositions.ts +130 -0
  243. package/src/updateItemSize.ts +203 -0
  244. package/src/updateTotalSize.ts +44 -0
  245. package/src/useAnimatedValue.ts +6 -0
  246. package/src/useCombinedRef.ts +22 -0
  247. package/src/useInit.ts +17 -0
  248. package/src/useSyncLayout.tsx +68 -0
  249. package/src/useValue$.ts +53 -0
  250. package/src/viewability.ts +279 -0
  251. package/tsconfig.json +59 -0
  252. package/tsup.config.ts +21 -0
  253. /package/{animated.js → dist/animated.js} +0 -0
  254. /package/{animated.mjs → dist/animated.mjs} +0 -0
  255. /package/{keyboard-controller.js → dist/keyboard-controller.js} +0 -0
  256. /package/{keyboard-controller.mjs → dist/keyboard-controller.mjs} +0 -0
  257. /package/{reanimated.d.mts → dist/reanimated.d.mts} +0 -0
  258. /package/{reanimated.d.ts → dist/reanimated.d.ts} +0 -0
  259. /package/{reanimated.js → dist/reanimated.js} +0 -0
  260. /package/{reanimated.mjs → dist/reanimated.mjs} +0 -0
@@ -0,0 +1,187 @@
1
+ import { LegendList } from "@legendapp/list";
2
+ import { type TCountryCode, countries, getEmojiFlag } from "countries-list";
3
+ import { useMemo, useState } from "react";
4
+ import { Pressable, StatusBar, StyleSheet, Text, View } from "react-native";
5
+ import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
6
+
7
+ export const unstable_settings = {
8
+ initialRouteName: "index",
9
+ };
10
+
11
+ export const createTitle = () => "Countries";
12
+
13
+ type Country = {
14
+ id: string;
15
+ name: string;
16
+ flag: string;
17
+ };
18
+
19
+ // Convert countries object to array and add an id
20
+ const DATA: Country[] = Object.entries(countries)
21
+ // .slice(0, 5)
22
+ .map(([code, country]) => ({
23
+ id: code,
24
+ name: country.name,
25
+ flag: getEmojiFlag(code as TCountryCode),
26
+ }))
27
+ .sort((a, b) => a.name.localeCompare(b.name));
28
+
29
+ type ItemProps = {
30
+ item: Country;
31
+ onPress: () => void;
32
+ isSelected: boolean;
33
+ };
34
+
35
+ const Item = ({ item, onPress, isSelected }: ItemProps) => (
36
+ <Pressable
37
+ onPress={onPress}
38
+ style={({ pressed }) => [styles.item, isSelected && styles.selectedItem, pressed && styles.pressedItem]}
39
+ >
40
+ <View style={styles.flagContainer}>
41
+ <Text style={styles.flag}>{item.flag}</Text>
42
+ </View>
43
+ <View style={styles.contentContainer}>
44
+ <Text style={[styles.title, isSelected && styles.selectedText]}>
45
+ {item.name}
46
+ <Text style={styles.countryCode}> ({item.id})</Text>
47
+ </Text>
48
+ </View>
49
+ </Pressable>
50
+ );
51
+
52
+ const App = () => {
53
+ const [selectedId, setSelectedId] = useState<string>();
54
+ const [randomSeed, setRandomSeed] = useState(0);
55
+
56
+ // Display either ordered or randomized data based on state
57
+ const displayData = useMemo(() => {
58
+ if (randomSeed) {
59
+ // Randomize the order
60
+ return [...DATA].sort(() => Math.random() - 0.5);
61
+ }
62
+ // Return alphabetically sorted data
63
+ return [...DATA].sort((a, b) => a.name.localeCompare(b.name));
64
+ }, [randomSeed]);
65
+
66
+ const renderItem = ({ item }: { item: Country }) => {
67
+ const isSelected = item.id === selectedId;
68
+ return <Item item={item} onPress={() => setSelectedId(item.id)} isSelected={isSelected} />;
69
+ };
70
+
71
+ return (
72
+ <SafeAreaProvider>
73
+ <SafeAreaView style={styles.container}>
74
+ <View style={styles.headerContainer}>
75
+ <Pressable style={styles.reorderButton} onPress={() => setRandomSeed(randomSeed + 1)}>
76
+ <Text style={styles.buttonText}>Randomize Order</Text>
77
+ </Pressable>
78
+ </View>
79
+ <LegendList
80
+ data={displayData}
81
+ renderItem={renderItem}
82
+ keyExtractor={(item) => item.id}
83
+ extraData={selectedId}
84
+ estimatedItemSize={70}
85
+ recycleItems
86
+ onStartReachedThreshold={0.1}
87
+ onStartReached={({ distanceFromStart }) => {
88
+ console.log("onStartReached", distanceFromStart);
89
+ }}
90
+ onEndReachedThreshold={0.1}
91
+ onEndReached={({ distanceFromEnd }) => {
92
+ console.log("onEndReached", distanceFromEnd);
93
+ }}
94
+ // ListHeaderComponent={<View style={{ height: 200, backgroundColor: "red" }} />}
95
+ // ListFooterComponent={<View style={{ height: 200, backgroundColor: "blue" }} />}
96
+ ItemSeparatorComponent={Separator}
97
+ />
98
+ </SafeAreaView>
99
+ </SafeAreaProvider>
100
+ );
101
+ };
102
+
103
+ const Separator = () => <View style={{ height: 5, backgroundColor: "green" }} />;
104
+
105
+ export default App;
106
+
107
+ const styles = StyleSheet.create({
108
+ container: {
109
+ flex: 1,
110
+ marginTop: StatusBar.currentHeight || 0,
111
+ backgroundColor: "#f5f5f5",
112
+ },
113
+ headerContainer: {
114
+ padding: 8,
115
+ backgroundColor: "#fff",
116
+ borderBottomWidth: 1,
117
+ borderBottomColor: "#e0e0e0",
118
+ alignItems: "center",
119
+ },
120
+ reorderButton: {
121
+ backgroundColor: "#4a86e8",
122
+ paddingVertical: 10,
123
+ paddingHorizontal: 20,
124
+ borderRadius: 8,
125
+ width: "80%",
126
+ alignItems: "center",
127
+ },
128
+ buttonText: {
129
+ color: "#fff",
130
+ fontSize: 16,
131
+ fontWeight: "500",
132
+ },
133
+ item: {
134
+ paddingHorizontal: 16,
135
+ paddingVertical: 6,
136
+ flexDirection: "row",
137
+ alignItems: "center",
138
+ backgroundColor: "#fff",
139
+ borderRadius: 12,
140
+ // shadowColor: "#000",
141
+ // shadowOffset: {
142
+ // width: 0,
143
+ // height: 2,
144
+ // },
145
+ // shadowOpacity: 0.1,
146
+ // shadowRadius: 3,
147
+ // elevation: 3,
148
+ },
149
+ selectedItem: {
150
+ // backgroundColor: "#e3f2fd",
151
+ // borderColor: "#1976d2",
152
+ // borderWidth: 1,
153
+ },
154
+ pressedItem: {
155
+ // backgroundColor: "#f0f0f0",
156
+ },
157
+ flagContainer: {
158
+ marginRight: 16,
159
+ width: 40,
160
+ height: 40,
161
+ borderRadius: 20,
162
+ backgroundColor: "#f8f9fa",
163
+ alignItems: "center",
164
+ justifyContent: "center",
165
+ },
166
+ flag: {
167
+ fontSize: 28,
168
+ },
169
+ contentContainer: {
170
+ flex: 1,
171
+ justifyContent: "center",
172
+ },
173
+ title: {
174
+ fontSize: 16,
175
+ color: "#333",
176
+ fontWeight: "500",
177
+ },
178
+ selectedText: {
179
+ color: "#1976d2",
180
+ fontWeight: "600",
181
+ },
182
+ countryCode: {
183
+ fontSize: 14,
184
+ color: "#666",
185
+ fontWeight: "400",
186
+ },
187
+ });
@@ -0,0 +1,86 @@
1
+ import { LegendList } from "@legendapp/list";
2
+ import { useState } from "react";
3
+ import { StatusBar, StyleSheet, Text, TouchableOpacity } from "react-native";
4
+ import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
5
+
6
+ type ItemData = {
7
+ id: string;
8
+ title: string;
9
+ };
10
+
11
+ const DATA: ItemData[] = [
12
+ {
13
+ id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
14
+ title: "First Item",
15
+ },
16
+ {
17
+ id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
18
+ title: "Second Item",
19
+ },
20
+ {
21
+ id: "58694a0f-3da1-471f-bd96-145571e29d72",
22
+ title: "Third Item",
23
+ },
24
+ ];
25
+
26
+ type ItemProps = {
27
+ item: ItemData;
28
+ onPress: () => void;
29
+ backgroundColor: string;
30
+ textColor: string;
31
+ };
32
+
33
+ const Item = ({ item, onPress, backgroundColor, textColor }: ItemProps) => (
34
+ <TouchableOpacity onPress={onPress} style={[styles.item, { backgroundColor }]}>
35
+ <Text style={[styles.title, { color: textColor }]}>{item.title}</Text>
36
+ </TouchableOpacity>
37
+ );
38
+
39
+ const App = () => {
40
+ const [selectedId, setSelectedId] = useState<string>();
41
+
42
+ const renderItem = ({ item }: { item: ItemData }) => {
43
+ const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff";
44
+ const color = item.id === selectedId ? "white" : "black";
45
+
46
+ return (
47
+ <Item
48
+ item={item}
49
+ onPress={() => setSelectedId(item.id)}
50
+ backgroundColor={backgroundColor}
51
+ textColor={color}
52
+ />
53
+ );
54
+ };
55
+
56
+ return (
57
+ <SafeAreaProvider>
58
+ <SafeAreaView style={styles.container}>
59
+ <LegendList
60
+ data={DATA}
61
+ renderItem={renderItem}
62
+ keyExtractor={(item) => item.id}
63
+ extraData={selectedId}
64
+ estimatedItemSize={100}
65
+ />
66
+ </SafeAreaView>
67
+ </SafeAreaProvider>
68
+ );
69
+ };
70
+
71
+ export default App;
72
+
73
+ const styles = StyleSheet.create({
74
+ container: {
75
+ flex: 1,
76
+ marginTop: StatusBar.currentHeight || 0,
77
+ },
78
+ item: {
79
+ padding: 20,
80
+ marginVertical: 8,
81
+ marginHorizontal: 16,
82
+ },
83
+ title: {
84
+ fontSize: 32,
85
+ },
86
+ });
@@ -0,0 +1,55 @@
1
+ import type React from "react";
2
+ import { createContext, useCallback, useContext, useMemo, useState } from "react";
3
+
4
+ export interface CardItem {
5
+ id: string;
6
+ isExpanded: boolean;
7
+ }
8
+
9
+ const DataContext = createContext({
10
+ data: [] as CardItem[],
11
+ setExpanded: (id: string, expanded: boolean) => {},
12
+ filter: "",
13
+ setFilter: null as unknown as React.Dispatch<React.SetStateAction<string>>,
14
+ });
15
+
16
+ export const CardsDataProvider = ({
17
+ initialData,
18
+ children,
19
+ }: { initialData: CardItem[]; children: React.ReactNode }) => {
20
+ const [data, setData] = useState(initialData);
21
+ const [filter, setFilter] = useState("");
22
+
23
+ const setExpanded = useCallback((id: string, expanded: boolean) => {
24
+ setData((prevData) => {
25
+ return prevData.map((item) => {
26
+ if (item.id === id) {
27
+ return { ...item, isExpanded: expanded };
28
+ }
29
+ return item;
30
+ });
31
+ });
32
+ }, []);
33
+
34
+ const filteredData = useMemo(() => {
35
+ if (filter !== "") {
36
+ const filterLower = filter.toLowerCase();
37
+ return data.filter((item) => item.id.includes(filterLower));
38
+ }
39
+ return data;
40
+ }, [filter, data]);
41
+
42
+ return (
43
+ <DataContext.Provider value={{ data: filteredData, setExpanded, filter, setFilter }}>
44
+ {children}
45
+ </DataContext.Provider>
46
+ );
47
+ };
48
+
49
+ export const useCardData = () => {
50
+ const contextValue = useContext(DataContext);
51
+ if (!contextValue) {
52
+ throw new Error("useData must be used within a CardDataProvider");
53
+ }
54
+ return contextValue;
55
+ };
@@ -0,0 +1,118 @@
1
+ import { renderItem } from "@/app/cards-renderItem";
2
+ import { DRAW_DISTANCE, ESTIMATED_ITEM_LENGTH } from "@/constants/constants";
3
+ import { LegendList, type LegendListRef } from "@legendapp/list";
4
+ import { useNavigation } from "expo-router";
5
+ import { useEffect, useRef, useState } from "react";
6
+ import { Button, StyleSheet, Text, TextInput, View } from "react-native";
7
+ import { CardsDataProvider, useCardData } from "./filter-data-provider";
8
+
9
+ interface CardsProps {
10
+ numColumns?: number;
11
+ }
12
+
13
+ function FilteredCards({ numColumns = 1 }: CardsProps) {
14
+ const listRef = useRef<LegendListRef>(null);
15
+ const { data } = useCardData();
16
+ const navigation = useNavigation();
17
+ const [mvcp, setMvcp] = useState(false);
18
+ const [key, setKey] = useState(0);
19
+
20
+ useEffect(() => {
21
+ navigation.setOptions({
22
+ title: "Filter",
23
+ headerRight: () => (
24
+ <Button
25
+ title={`${mvcp ? "✓" : ""}MVCP`}
26
+ onPress={() => {
27
+ setMvcp((prev) => !prev);
28
+ setKey((prev) => prev + 1);
29
+ }}
30
+ color={mvcp ? "#00e" : "black"}
31
+ />
32
+ ),
33
+ });
34
+ }, [mvcp]);
35
+
36
+ return (
37
+ <View style={[StyleSheet.absoluteFill, styles.outerContainer]} key="legendlist">
38
+ <FilterInput />
39
+ <View style={{ flexGrow: 1 }}>
40
+ <LegendList
41
+ key={key} // LegendList react weird on the changing of maintainVisibleContentPosition on the fly, make sure to remount the list
42
+ ref={listRef}
43
+ style={[StyleSheet.absoluteFill, styles.scrollContainer]}
44
+ contentContainerStyle={styles.listContainer}
45
+ data={data}
46
+ renderItem={renderItem}
47
+ keyExtractor={(item) => `id${item.id}`}
48
+ estimatedItemSize={ESTIMATED_ITEM_LENGTH}
49
+ drawDistance={DRAW_DISTANCE}
50
+ maintainVisibleContentPosition={mvcp}
51
+ recycleItems={true}
52
+ numColumns={numColumns}
53
+ ListFooterComponent={<View />}
54
+ ListFooterComponentStyle={styles.listHeader}
55
+ ListEmptyComponent={
56
+ <View style={styles.listEmpty}>
57
+ <Text style={{ color: "white" }}>Empty</Text>
58
+ </View>
59
+ }
60
+ />
61
+ </View>
62
+ </View>
63
+ );
64
+ }
65
+
66
+ export default function CardsWrapper({ numColumns = 1 }: CardsProps) {
67
+ return (
68
+ <CardsDataProvider
69
+ initialData={
70
+ Array.from({ length: 1000 }, (_, i) => ({
71
+ id: i.toString(),
72
+ })) as any[]
73
+ }
74
+ >
75
+ <FilteredCards numColumns={numColumns} />
76
+ </CardsDataProvider>
77
+ );
78
+ }
79
+
80
+ const FilterInput = () => {
81
+ const { filter, setFilter } = useCardData();
82
+ return (
83
+ <TextInput
84
+ placeholder="Filter"
85
+ style={{ backgroundColor: "white", padding: 8, margin: 8, height: 40 }}
86
+ value={filter}
87
+ onChangeText={setFilter}
88
+ keyboardType="numeric"
89
+ />
90
+ );
91
+ };
92
+
93
+ const styles = StyleSheet.create({
94
+ listHeader: {
95
+ alignSelf: "center",
96
+ width: "100%",
97
+ backgroundColor: "#456AAA",
98
+ borderRadius: 12,
99
+ marginHorizontal: 8,
100
+ marginVertical: 8,
101
+ },
102
+ listEmpty: {
103
+ flex: 1,
104
+ justifyContent: "center",
105
+ alignItems: "center",
106
+ backgroundColor: "#6789AB",
107
+ paddingVertical: 16,
108
+ },
109
+ outerContainer: {
110
+ backgroundColor: "#456",
111
+ },
112
+ scrollContainer: {},
113
+ listContainer: {
114
+ width: 400,
115
+ maxWidth: "100%",
116
+ marginHorizontal: "auto",
117
+ },
118
+ });
@@ -0,0 +1,106 @@
1
+ import { LegendList, type LegendListRef } from "@legendapp/list";
2
+ import { useNavigation } from "expo-router";
3
+ import { useLayoutEffect, useRef, useState } from "react";
4
+ import { StyleSheet, Text, View } from "react-native";
5
+ import { type Item, renderItem } from "./renderFixedItem";
6
+
7
+ const ITEM_HEIGHT = 400;
8
+ const SEPARATOR_HEIGHT = 52;
9
+ const ESTIMATED_ITEM_LENGTH = 200;
10
+
11
+ type RenderItem = Item & { type: "separator" | "item" };
12
+
13
+ const RenderMultiItem = ({
14
+ item,
15
+ index,
16
+ }: {
17
+ item: RenderItem;
18
+ index: number;
19
+ }) => {
20
+ if (item.type === "separator") {
21
+ return (
22
+ <View
23
+ style={{
24
+ height: SEPARATOR_HEIGHT,
25
+ backgroundColor: "red",
26
+ justifyContent: "center",
27
+ alignItems: "center",
28
+ }}
29
+ >
30
+ <Text style={{ color: "white" }}>Separator {item.id}</Text>
31
+ </View>
32
+ );
33
+ }
34
+ return renderItem({ item, index, height: ITEM_HEIGHT });
35
+ };
36
+
37
+ export default function ScrollIndexDemo() {
38
+ const scrollViewRef = useRef<LegendListRef>(null);
39
+
40
+ const [data, setData] = useState<RenderItem[]>(
41
+ () =>
42
+ Array.from({ length: 500 }, (_, i) => ({
43
+ id: i.toString(),
44
+ type: i % 3 === 0 ? "separator" : "item",
45
+ })) as any[],
46
+ );
47
+
48
+ const navigation = useNavigation();
49
+ useLayoutEffect(() => {
50
+ navigation.setOptions({
51
+ title: "Initial scroll index",
52
+ });
53
+ }, []);
54
+
55
+ return (
56
+ <View style={[StyleSheet.absoluteFill, styles.outerContainer]}>
57
+ <LegendList
58
+ ref={scrollViewRef}
59
+ style={[StyleSheet.absoluteFill, styles.scrollContainer]}
60
+ contentContainerStyle={styles.listContainer}
61
+ data={data}
62
+ renderItem={RenderMultiItem}
63
+ keyExtractor={(item) => item.id}
64
+ getEstimatedItemSize={(i, item) => (data[i].type === "separator" ? 52 : 400)}
65
+ estimatedItemSize={ESTIMATED_ITEM_LENGTH}
66
+ drawDistance={1000}
67
+ recycleItems={true}
68
+ // alignItemsAtEnd
69
+ // maintainScrollAtEnd
70
+ onEndReached={({ distanceFromEnd }) => {
71
+ console.log("onEndReached", distanceFromEnd);
72
+ }}
73
+ ListHeaderComponent={<View />}
74
+ ListHeaderComponentStyle={styles.listHeader}
75
+ // initialScrollOffset={20000}
76
+ initialScrollIndex={50}
77
+ // inverted
78
+ // horizontal
79
+ />
80
+ </View>
81
+ );
82
+ }
83
+
84
+ const styles = StyleSheet.create({
85
+ listHeader: {
86
+ alignSelf: "center",
87
+ height: 100,
88
+ width: 100,
89
+ backgroundColor: "#456AAA",
90
+ borderRadius: 12,
91
+ marginHorizontal: 8,
92
+ marginTop: 8,
93
+ },
94
+ outerContainer: {
95
+ backgroundColor: "#456",
96
+ },
97
+ scrollContainer: {
98
+ paddingHorizontal: 16,
99
+ // paddingrVertical: 48,
100
+ },
101
+
102
+ listContainer: {
103
+ // paddingHorizontal: 16,
104
+ paddingTop: 48,
105
+ },
106
+ });