@legendapp/list 2.0.0-next.0 → 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 (262) 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} +63 -15
  17. package/{index.d.ts → dist/index.d.ts} +63 -15
  18. package/dist/index.js +2525 -0
  19. package/dist/index.mjs +2497 -0
  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/index.js +0 -2348
  254. package/index.mjs +0 -2320
  255. /package/{animated.js → dist/animated.js} +0 -0
  256. /package/{animated.mjs → dist/animated.mjs} +0 -0
  257. /package/{keyboard-controller.js → dist/keyboard-controller.js} +0 -0
  258. /package/{keyboard-controller.mjs → dist/keyboard-controller.mjs} +0 -0
  259. /package/{reanimated.d.mts → dist/reanimated.d.mts} +0 -0
  260. /package/{reanimated.d.ts → dist/reanimated.d.ts} +0 -0
  261. /package/{reanimated.js → dist/reanimated.js} +0 -0
  262. /package/{reanimated.mjs → dist/reanimated.mjs} +0 -0
@@ -0,0 +1,130 @@
1
+ import { getId } from "./getId";
2
+ import { getItemSize } from "./getItemSize";
3
+ import { getScrollVelocity } from "./getScrollVelocity";
4
+ import { roundSize } from "./helpers";
5
+ import { type StateContext, peek$ } from "./state";
6
+ import type { InternalState } from "./types";
7
+ import { updateTotalSize } from "./updateTotalSize";
8
+
9
+ export function updateAllPositions(ctx: StateContext, state: InternalState, dataChanged?: boolean) {
10
+ const { averageSizes, columns, indexByKey, positions, firstFullyOnScreenIndex, idCache, sizesKnown } = state;
11
+ const data = state.props.data;
12
+ const numColumns = peek$(ctx, "numColumns");
13
+ const indexByKeyForChecking = __DEV__ ? new Map() : undefined;
14
+ const scrollVelocity = getScrollVelocity(state);
15
+
16
+ if (dataChanged) {
17
+ indexByKey.clear();
18
+ idCache.clear();
19
+ }
20
+
21
+ // TODO: Hook this up to actual item types later once we have item types
22
+ const itemType = "";
23
+ let averageSize = averageSizes[itemType]?.avg;
24
+ if (averageSize !== undefined) {
25
+ averageSize = roundSize(averageSize);
26
+ }
27
+
28
+ // Check if we should use backwards optimization when scrolling up
29
+ const shouldUseBackwards =
30
+ !dataChanged && scrollVelocity < 0 && firstFullyOnScreenIndex > 5 && firstFullyOnScreenIndex < data!.length;
31
+
32
+ if (shouldUseBackwards && firstFullyOnScreenIndex !== undefined) {
33
+ // Get the current position of firstFullyOnScreenIndex as anchor
34
+ const anchorId = getId(state, firstFullyOnScreenIndex)!;
35
+ const anchorPosition = positions.get(anchorId);
36
+
37
+ // If we don't have the anchor position, fall back to regular behavior
38
+ if (anchorPosition !== undefined) {
39
+ // Start from the anchor and go backwards
40
+ let currentRowTop = anchorPosition;
41
+ let maxSizeInRow = 0;
42
+ let bailout = false;
43
+
44
+ // Process items backwards from firstFullyOnScreenIndex - 1 to 0
45
+ for (let i = firstFullyOnScreenIndex - 1; i >= 0; i--) {
46
+ const id = idCache.get(i) ?? getId(state, i)!;
47
+ const size = sizesKnown.get(id) ?? getItemSize(state, id, i, data[i], averageSize);
48
+ const itemColumn = columns.get(id)!;
49
+
50
+ maxSizeInRow = Math.max(maxSizeInRow, size);
51
+
52
+ // When we reach column 1, we're at the start of a new row going backwards
53
+ if (itemColumn === 1) {
54
+ currentRowTop -= maxSizeInRow;
55
+ maxSizeInRow = 0;
56
+ }
57
+
58
+ // Check if position goes too low - bail if so
59
+ if (currentRowTop < -2000) {
60
+ bailout = true;
61
+ break;
62
+ }
63
+
64
+ // Update position for this item (columns and indexByKey already set)
65
+ positions.set(id, currentRowTop);
66
+ }
67
+
68
+ if (!bailout) {
69
+ // We successfully processed backwards, we're done
70
+ updateTotalSize(ctx, state);
71
+ return;
72
+ }
73
+ }
74
+ }
75
+
76
+ // Regular ascending behavior (either not scrolling up or bailed out)
77
+ let currentRowTop = 0;
78
+ let column = 1;
79
+ let maxSizeInRow = 0;
80
+
81
+ const hasColumns = numColumns > 1;
82
+ const needsIndexByKey = dataChanged || indexByKey.size === 0;
83
+
84
+ // Note that this loop is micro-optimized because it's a hot path
85
+ const dataLength = data!.length;
86
+ for (let i = 0; i < dataLength; i++) {
87
+ // Inline the map get calls to avoid the overhead of the function call
88
+ const id = idCache.get(i) ?? getId(state, i)!;
89
+ const size = sizesKnown.get(id) ?? getItemSize(state, id, i, data[i], averageSize);
90
+
91
+ // Set index mapping for this item
92
+ if (__DEV__ && needsIndexByKey) {
93
+ if (indexByKeyForChecking!.has(id)) {
94
+ console.error(
95
+ `[legend-list] Error: Detected overlapping key (${id}) which causes missing items and gaps and other terrrible things. Check that keyExtractor returns unique values.`,
96
+ );
97
+ }
98
+ indexByKeyForChecking!.set(id, i);
99
+ }
100
+
101
+ // Set position for this item
102
+ positions.set(id, currentRowTop);
103
+
104
+ // Update indexByKey if needed
105
+ if (needsIndexByKey) {
106
+ indexByKey.set(id, i);
107
+ }
108
+
109
+ // Set column for this item
110
+ columns.set(id, column);
111
+
112
+ if (hasColumns) {
113
+ if (size > maxSizeInRow) {
114
+ maxSizeInRow = size;
115
+ }
116
+
117
+ column++;
118
+ if (column > numColumns) {
119
+ // Move to next row
120
+ currentRowTop += maxSizeInRow;
121
+ column = 1;
122
+ maxSizeInRow = 0;
123
+ }
124
+ } else {
125
+ currentRowTop += size;
126
+ }
127
+ }
128
+
129
+ updateTotalSize(ctx, state);
130
+ }
@@ -0,0 +1,203 @@
1
+ import type { LayoutRectangle } from "react-native";
2
+ import { calculateItemsInView } from "./calculateItemsInView";
3
+ import { checkAllSizesKnown } from "./checkAllSizesKnown";
4
+ import { IsNewArchitecture } from "./constants";
5
+ import { doMaintainScrollAtEnd } from "./doMaintainScrollAtEnd";
6
+ import { getItemSize } from "./getItemSize";
7
+ import { requestAdjust } from "./requestAdjust";
8
+ import { type StateContext, peek$, set$ } from "./state";
9
+ import type { InternalState, MaintainScrollAtEndOptions } from "./types";
10
+
11
+ export function updateItemSizes(
12
+ ctx: StateContext,
13
+ state: InternalState,
14
+ itemUpdates: { itemKey: string; sizeObj: { width: number; height: number } }[],
15
+ ) {
16
+ const {
17
+ props: {
18
+ horizontal,
19
+ maintainVisibleContentPosition,
20
+ suggestEstimatedItemSize,
21
+ onItemSizeChanged,
22
+ data,
23
+ maintainScrollAtEnd,
24
+ },
25
+ } = state;
26
+
27
+ if (!data) return;
28
+
29
+ let needsRecalculate = false;
30
+ let shouldMaintainScrollAtEnd = false;
31
+ let minIndexSizeChanged: number | undefined;
32
+ let maxOtherAxisSize = peek$(ctx, "otherAxisSize") || 0;
33
+
34
+ for (const { itemKey, sizeObj } of itemUpdates) {
35
+ const index = state.indexByKey.get(itemKey)!;
36
+ const prevSizeKnown = state.sizesKnown.get(itemKey);
37
+
38
+ const diff = updateOneItemSize(state, itemKey, sizeObj);
39
+ const size = Math.floor((horizontal ? sizeObj.width : sizeObj.height) * 8) / 8;
40
+
41
+ if (diff !== 0) {
42
+ minIndexSizeChanged = minIndexSizeChanged !== undefined ? Math.min(minIndexSizeChanged, index) : index;
43
+
44
+ // Handle scrolling adjustments
45
+ if (
46
+ state.scrollingTo?.viewPosition &&
47
+ maintainVisibleContentPosition &&
48
+ index === state.scrollingTo.index
49
+ ) {
50
+ requestAdjust(ctx, state, diff * state.scrollingTo.viewPosition);
51
+ }
52
+
53
+ // Check if item is in view
54
+ const { startBuffered, endBuffered } = state;
55
+ needsRecalculate ||= index >= startBuffered && index <= endBuffered;
56
+ if (!needsRecalculate) {
57
+ const numContainers = ctx.values.get("numContainers") as number;
58
+ for (let i = 0; i < numContainers; i++) {
59
+ if (peek$(ctx, `containerItemKey${i}`) === itemKey) {
60
+ needsRecalculate = true;
61
+ break;
62
+ }
63
+ }
64
+ }
65
+
66
+ // Handle other axis size
67
+ if (state.needsOtherAxisSize) {
68
+ const otherAxisSize = horizontal ? sizeObj.height : sizeObj.width;
69
+ maxOtherAxisSize = Math.max(maxOtherAxisSize, otherAxisSize);
70
+ }
71
+
72
+ // Check if we should maintain scroll at end
73
+ if (prevSizeKnown !== undefined && Math.abs(prevSizeKnown - size) > 5) {
74
+ shouldMaintainScrollAtEnd = true;
75
+ }
76
+
77
+ // Call onItemSizeChanged callback
78
+ onItemSizeChanged?.({
79
+ size,
80
+ previous: size - diff,
81
+ index,
82
+ itemKey,
83
+ itemData: state.props.data[index],
84
+ });
85
+ }
86
+ }
87
+
88
+ // Update state with minimum changed index
89
+ if (minIndexSizeChanged !== undefined) {
90
+ state.minIndexSizeChanged =
91
+ state.minIndexSizeChanged !== undefined
92
+ ? Math.min(state.minIndexSizeChanged, minIndexSizeChanged)
93
+ : minIndexSizeChanged;
94
+ }
95
+
96
+ // Handle dev warning about estimated size
97
+ if (__DEV__ && suggestEstimatedItemSize && minIndexSizeChanged !== undefined) {
98
+ if (state.timeoutSizeMessage) clearTimeout(state.timeoutSizeMessage);
99
+ state.timeoutSizeMessage = setTimeout(() => {
100
+ state.timeoutSizeMessage = undefined;
101
+ const num = state.sizesKnown.size;
102
+ const avg = state.averageSizes[""]?.avg;
103
+ console.warn(
104
+ `[legend-list] Based on the ${num} items rendered so far, the optimal estimated size is ${avg}.`,
105
+ );
106
+ }, 1000);
107
+ }
108
+
109
+ const cur = peek$(ctx, "otherAxisSize");
110
+ if (!cur || maxOtherAxisSize > cur) {
111
+ set$(ctx, "otherAxisSize", maxOtherAxisSize);
112
+ }
113
+
114
+ const containersDidLayout = peek$(ctx, "containersDidLayout");
115
+
116
+ if (containersDidLayout || checkAllSizesKnown(state)) {
117
+ if (needsRecalculate) {
118
+ state.scrollForNextCalculateItemsInView = undefined;
119
+
120
+ calculateItemsInView(ctx, state, { doMVCP: true });
121
+ }
122
+ if (shouldMaintainScrollAtEnd) {
123
+ if (maintainScrollAtEnd === true || (maintainScrollAtEnd as MaintainScrollAtEndOptions).onItemLayout) {
124
+ doMaintainScrollAtEnd(ctx, state, false);
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ export function updateItemSize(
131
+ ctx: StateContext,
132
+ state: InternalState,
133
+ itemKey: string,
134
+ sizeObj: { width: number; height: number },
135
+ ) {
136
+ if (IsNewArchitecture) {
137
+ const { sizesKnown } = state;
138
+ const numContainers = ctx.values.get("numContainers") as number;
139
+ const changes: { itemKey: string; sizeObj: { width: number; height: number } }[] = [];
140
+
141
+ // Run through all containers and if we don't already have a known size then measure the item
142
+ // This is useful because when multiple items render in one frame, the first container fires a
143
+ // useLayoutEffect and we can measure all containers before their useLayoutEffects fire after a delay.
144
+ // This lets use fix any gaps/overlaps that might be visible before the useLayoutEffects fire for each container.
145
+ for (let i = 0; i < numContainers; i++) {
146
+ const containerItemKey = peek$(ctx, `containerItemKey${i}`);
147
+ if (itemKey === containerItemKey) {
148
+ // If it's this item just use the param
149
+ changes.push({ itemKey, sizeObj });
150
+ } else if (!sizesKnown.has(containerItemKey) && containerItemKey !== undefined) {
151
+ const containerRef = ctx.viewRefs.get(i);
152
+ if (containerRef?.current) {
153
+ let measured: LayoutRectangle;
154
+ containerRef.current.measure((x, y, width, height) => {
155
+ measured = { x, y, width, height };
156
+ });
157
+
158
+ if (measured!) {
159
+ changes.push({ itemKey: containerItemKey, sizeObj: measured });
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ if (changes.length > 0) {
166
+ updateItemSizes(ctx, state, changes);
167
+ }
168
+ } else {
169
+ updateItemSizes(ctx, state, [{ itemKey, sizeObj }]);
170
+ }
171
+ }
172
+
173
+ export function updateOneItemSize(state: InternalState, itemKey: string, sizeObj: { width: number; height: number }) {
174
+ const {
175
+ sizes,
176
+ indexByKey,
177
+ sizesKnown,
178
+ averageSizes,
179
+ props: { data, horizontal },
180
+ } = state;
181
+ if (!data) return 0;
182
+
183
+ const index = indexByKey.get(itemKey)!;
184
+ const prevSize = getItemSize(state, itemKey, index, data as any);
185
+ const size = Math.floor((horizontal ? sizeObj.width : sizeObj.height) * 8) / 8;
186
+
187
+ sizesKnown.set(itemKey, size);
188
+
189
+ // Update averages
190
+ const itemType = "";
191
+ let averages = averageSizes[itemType];
192
+ if (!averages) {
193
+ averages = averageSizes[itemType] = { num: 0, avg: 0 };
194
+ }
195
+ averages.avg = (averages.avg * averages.num + size) / (averages.num + 1);
196
+ averages.num++;
197
+
198
+ if (!prevSize || Math.abs(prevSize - size) > 0.1) {
199
+ sizes.set(itemKey, size);
200
+ return size - prevSize;
201
+ }
202
+ return 0;
203
+ }
@@ -0,0 +1,44 @@
1
+ import { getId } from "./getId";
2
+ import { getItemSize } from "./getItemSize";
3
+ import { type StateContext, set$ } from "./state";
4
+ import type { InternalState } from "./types";
5
+ import { updateAlignItemsPaddingTop } from "./updateAlignItemsPaddingTop";
6
+
7
+ export function updateTotalSize(ctx: StateContext, state: InternalState) {
8
+ const {
9
+ positions,
10
+ props: { data },
11
+ } = state;
12
+
13
+ if (data.length === 0) {
14
+ addTotalSize(ctx, state, null, 0);
15
+ } else {
16
+ const lastId = getId(state, data.length - 1);
17
+ if (lastId !== undefined) {
18
+ const lastPosition = positions.get(lastId);
19
+ if (lastPosition !== undefined) {
20
+ const lastSize = getItemSize(state, lastId, data.length - 1, data[data.length - 1]);
21
+ // TODO: This is likely incorrect for columns with rows having different heights, need to get max size of the last row
22
+ if (lastSize !== undefined) {
23
+ const totalSize = lastPosition + lastSize;
24
+ addTotalSize(ctx, state, null, totalSize);
25
+ }
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ function addTotalSize(ctx: StateContext, state: InternalState, key: string | null, add: number) {
32
+ const { alignItemsAtEnd } = state.props;
33
+ if (key === null) {
34
+ state.totalSize = add;
35
+ } else {
36
+ state.totalSize += add;
37
+ }
38
+
39
+ set$(ctx, "totalSize", state.totalSize);
40
+
41
+ if (alignItemsAtEnd) {
42
+ updateAlignItemsPaddingTop(ctx, state);
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ import { useRef } from "react";
2
+ import { Animated } from "react-native";
3
+
4
+ export const useAnimatedValue = (initialValue: number): Animated.Value => {
5
+ return useRef(new Animated.Value(initialValue)).current;
6
+ };
@@ -0,0 +1,22 @@
1
+ import { useCallback } from "react";
2
+ import { isFunction } from "./helpers";
3
+
4
+ type RefItem<T> = ((element: T | null) => void) | React.MutableRefObject<T | null> | null | undefined;
5
+
6
+ export const useCombinedRef = <T>(...refs: RefItem<T>[]) => {
7
+ const callback = useCallback((element: T | null) => {
8
+ for (const ref of refs) {
9
+ if (!ref) {
10
+ continue;
11
+ }
12
+
13
+ if (isFunction(ref)) {
14
+ ref(element);
15
+ } else {
16
+ ref.current = element;
17
+ }
18
+ }
19
+ }, refs);
20
+
21
+ return callback;
22
+ };
package/src/useInit.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { useRef } from "react";
2
+
3
+ const symbolFirst = Symbol();
4
+ // A hook that runs a callback only once during the first render.
5
+ // It should happen during render, not in useEffect, so that any setState calls during the callback
6
+ // will trigger a re-render immediately rather than waiting for a next render.
7
+ // See https://react.dev/reference/react/useState#storing-information-from-previous-renders
8
+ export function useInit<T>(cb: () => T) {
9
+ const refValue = useRef<T | typeof symbolFirst>(symbolFirst);
10
+
11
+ // Run inline during first render only
12
+ if (refValue.current === symbolFirst) {
13
+ refValue.current = cb();
14
+ }
15
+
16
+ return refValue.current;
17
+ }
@@ -0,0 +1,68 @@
1
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
2
+ import type { LayoutChangeEvent, LayoutRectangle, View } from "react-native";
3
+
4
+ export function useSyncLayoutState<T extends View = View>({
5
+ getValue,
6
+ debounce,
7
+ onChange: onChangeProp,
8
+ }: {
9
+ getValue: (rectangle: LayoutRectangle) => number;
10
+ debounce?: number | undefined;
11
+ onChange: (rectangle: LayoutRectangle, fromLayoutEffect: boolean) => void;
12
+ }) {
13
+ const debounceTimeoutRef = useRef<any>(null);
14
+ const [value, setValue] = useState(0);
15
+
16
+ const onChange = useCallback(
17
+ (rectangle: LayoutRectangle, fromLayoutEffect: boolean) => {
18
+ const height = getValue(rectangle);
19
+
20
+ if (debounce === undefined) {
21
+ setValue(height);
22
+ } else {
23
+ // Clear previous timeout if it exists
24
+ if (debounceTimeoutRef.current) {
25
+ clearTimeout(debounceTimeoutRef.current);
26
+ }
27
+
28
+ // Debounce the setViewHeight call
29
+ debounceTimeoutRef.current = setTimeout(() => {
30
+ debounceTimeoutRef.current = null;
31
+ setValue(height);
32
+ }, debounce);
33
+ }
34
+
35
+ onChangeProp?.(rectangle, fromLayoutEffect);
36
+ },
37
+ [getValue, debounce],
38
+ );
39
+
40
+ const { onLayout, ref } = useSyncLayout<T>({ onChange });
41
+
42
+ return { value, onLayout, ref };
43
+ }
44
+
45
+ export function useSyncLayout<T extends View = View>({
46
+ onChange,
47
+ }: {
48
+ onChange: (rectangle: LayoutRectangle, fromLayoutEffect: boolean) => void;
49
+ }) {
50
+ const ref = useRef<T | null>(null);
51
+
52
+ const onLayout = useCallback(
53
+ (event: LayoutChangeEvent) => {
54
+ onChange(event.nativeEvent.layout, false);
55
+ },
56
+ [onChange],
57
+ );
58
+
59
+ useLayoutEffect(() => {
60
+ if (ref.current) {
61
+ ref.current.measure((x, y, width, height) => {
62
+ onChange({ x, y, width, height }, true);
63
+ });
64
+ }
65
+ }, []);
66
+
67
+ return { onLayout, ref };
68
+ }
@@ -0,0 +1,53 @@
1
+ import { useMemo } from "react";
2
+ import { listen$, peek$, useStateContext } from "./state";
3
+ import type { ListenerType } from "./state";
4
+ import { useAnimatedValue } from "./useAnimatedValue";
5
+
6
+ export function useValue$(
7
+ key: ListenerType,
8
+ params?: {
9
+ getValue?: (value: number) => number;
10
+ delay?: number | ((value: number, prevValue: number | undefined) => number);
11
+ },
12
+ ) {
13
+ const { getValue, delay } = params || {};
14
+ const ctx = useStateContext();
15
+ const animValue = useAnimatedValue((getValue ? getValue(peek$(ctx, key)) : peek$(ctx, key)) ?? 0);
16
+ useMemo(() => {
17
+ let newValue: number | undefined = undefined;
18
+ let prevValue: number | undefined = undefined;
19
+ let didQueueTask = false;
20
+ listen$(ctx, key, (v) => {
21
+ newValue = getValue ? getValue(v) : v;
22
+
23
+ if (delay !== undefined) {
24
+ // Queue into a microtask because setting the value immediately was making the value
25
+ // not actually set. I think it has to do with setting during useLayoutEffect, but I'm not sure.
26
+ // This seems to be an optimization for setting totalSize because that can happen multiple times per frame
27
+ // so we skip setting the value immediately if using the microtask version.
28
+ const fn = () => {
29
+ didQueueTask = false;
30
+ if (newValue !== undefined) {
31
+ animValue.setValue(newValue!);
32
+ }
33
+ };
34
+ const delayValue = typeof delay === "function" ? delay(newValue!, prevValue) : delay;
35
+ prevValue = newValue;
36
+ if (!didQueueTask) {
37
+ didQueueTask = true;
38
+ if (delayValue === 0) {
39
+ queueMicrotask(fn);
40
+ } else {
41
+ // We're not clearing the timeout because we want it to run in the timeout from the first change
42
+ // but just not run multiple times.
43
+ setTimeout(fn, delayValue);
44
+ }
45
+ }
46
+ } else {
47
+ animValue.setValue(newValue!);
48
+ }
49
+ });
50
+ }, []);
51
+
52
+ return animValue;
53
+ }