@expo/ui 58.0.5 → 58.0.6

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 (61) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/android/build.gradle +2 -2
  3. package/build/swift-ui/List/DataListForEach.d.ts +59 -0
  4. package/build/swift-ui/List/DataListForEach.js +137 -0
  5. package/build/swift-ui/List/DataListForEach.js.map +1 -0
  6. package/build/swift-ui/List/ListForEach.d.ts +15 -4
  7. package/build/swift-ui/List/ListForEach.js +40 -3
  8. package/build/swift-ui/List/ListForEach.js.map +1 -1
  9. package/build/swift-ui/List/index.d.ts +1 -1
  10. package/build/swift-ui/List/index.js.map +1 -1
  11. package/build/swift-ui/List/window.d.ts +5 -0
  12. package/build/swift-ui/List/window.js +28 -0
  13. package/build/swift-ui/List/window.js.map +1 -0
  14. package/build/swift-ui/modifiers/index.d.ts +16 -7
  15. package/build/swift-ui/modifiers/index.js +10 -5
  16. package/build/swift-ui/modifiers/index.js.map +1 -1
  17. package/expo-module.config.json +1 -1
  18. package/ios/DataListForEachView.swift +255 -0
  19. package/ios/ExpoUIModule.swift +3 -0
  20. package/ios/ListForEachView.swift +12 -3
  21. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5.module → 58.0.6/expo.modules.ui-58.0.6.module} +7 -7
  22. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.module.md5 +1 -0
  23. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.module.sha1 +1 -0
  24. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.module.sha256 +1 -0
  25. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.module.sha512 +1 -0
  26. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5.pom → 58.0.6/expo.modules.ui-58.0.6.pom} +1 -1
  27. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.pom.md5 +1 -0
  28. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.pom.sha1 +1 -0
  29. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.pom.sha256 +1 -0
  30. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.pom.sha512 +1 -0
  31. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  32. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  33. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  34. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  35. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  36. package/package.json +2 -2
  37. package/prebuilds/output/debug/xcframeworks/ExpoUI.tar.gz +0 -0
  38. package/prebuilds/output/release/xcframeworks/ExpoUI.tar.gz +0 -0
  39. package/src/swift-ui/List/DataListForEach.tsx +249 -0
  40. package/src/swift-ui/List/ListForEach.tsx +56 -6
  41. package/src/swift-ui/List/index.tsx +1 -1
  42. package/src/swift-ui/List/window.ts +28 -0
  43. package/src/swift-ui/modifiers/index.ts +50 -14
  44. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.module.md5 +0 -1
  45. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.module.sha1 +0 -1
  46. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.module.sha256 +0 -1
  47. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.module.sha512 +0 -1
  48. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.pom.md5 +0 -1
  49. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.pom.sha1 +0 -1
  50. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.pom.sha256 +0 -1
  51. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.5/expo.modules.ui-58.0.5.pom.sha512 +0 -1
  52. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5-sources.jar → 58.0.6/expo.modules.ui-58.0.6-sources.jar} +0 -0
  53. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5-sources.jar.md5 → 58.0.6/expo.modules.ui-58.0.6-sources.jar.md5} +0 -0
  54. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5-sources.jar.sha1 → 58.0.6/expo.modules.ui-58.0.6-sources.jar.sha1} +0 -0
  55. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5-sources.jar.sha256 → 58.0.6/expo.modules.ui-58.0.6-sources.jar.sha256} +0 -0
  56. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5-sources.jar.sha512 → 58.0.6/expo.modules.ui-58.0.6-sources.jar.sha512} +0 -0
  57. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5.aar → 58.0.6/expo.modules.ui-58.0.6.aar} +0 -0
  58. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5.aar.md5 → 58.0.6/expo.modules.ui-58.0.6.aar.md5} +0 -0
  59. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5.aar.sha1 → 58.0.6/expo.modules.ui-58.0.6.aar.sha1} +0 -0
  60. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5.aar.sha256 → 58.0.6/expo.modules.ui-58.0.6.aar.sha256} +0 -0
  61. /package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.5/expo.modules.ui-58.0.5.aar.sha512 → 58.0.6/expo.modules.ui-58.0.6.aar.sha512} +0 -0
@@ -0,0 +1,255 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+ import SwiftUI
5
+
6
+ final class DataListForEachProps: UIBaseViewProps {
7
+ @Field var itemKeys: [String] = []
8
+ @Field var revision: Int = 0
9
+ @Field var estimatedItemSize: Double = 64
10
+ @Field var deleteEnabled: Bool = false
11
+ @Field var moveEnabled: Bool = false
12
+ var onDelete = EventDispatcher()
13
+ var onMove = EventDispatcher()
14
+ var onWindowChange = EventDispatcher()
15
+ }
16
+
17
+ final class DataListForEachItemProps: ExpoSwiftUI.ViewProps {
18
+ @Field var itemKey: String = ""
19
+ @Field var index: Int = 0
20
+ @Field var revision: Int = 0
21
+ }
22
+
23
+ struct DataListForEachItemView: ExpoSwiftUI.View {
24
+ @ObservedObject var props: DataListForEachItemProps
25
+
26
+ init(props: DataListForEachItemProps) {
27
+ self.props = props
28
+ }
29
+
30
+ var body: some View {
31
+ Children()
32
+ }
33
+ }
34
+
35
+ final class DataListForEachPoolProps: ExpoSwiftUI.ViewProps {}
36
+
37
+ // Holds the recycled slots, so mounting a slot never republishes the list's own props.
38
+ struct DataListForEachPoolView: ExpoSwiftUI.View {
39
+ @ObservedObject var props: DataListForEachPoolProps
40
+
41
+ init(props: DataListForEachPoolProps) {
42
+ self.props = props
43
+ }
44
+
45
+ var body: some View {
46
+ EmptyView()
47
+ }
48
+ }
49
+
50
+ struct DataListForEachView: ExpoSwiftUI.View {
51
+ @ObservedObject var props: DataListForEachProps
52
+ @StateObject private var window = DataListForEachWindow()
53
+
54
+ init(props: DataListForEachProps) {
55
+ self.props = props
56
+ }
57
+
58
+ var body: some View {
59
+ let revision = props.revision
60
+ let pool = (props.children ?? []).lazy.compactMap { ($0.childView as? DataListForEachPoolView)?.props }.first
61
+ if let pool {
62
+ ForEach(Array(props.itemKeys.enumerated()), id: \.element) { index, key in
63
+ DataListForEachRow(itemKey: key, index: index, listProps: props, poolProps: pool, window: window)
64
+ .tag(AnyHashable(key))
65
+ }
66
+ .onDelete(perform: props.deleteEnabled ? { offsets in
67
+ props.onDelete(["indices": Array(offsets), "revision": revision])
68
+ } : nil)
69
+ .onMove(perform: props.moveEnabled ? { sources, destination in
70
+ props.onMove(["sourceIndices": Array(sources), "destination": destination, "revision": revision])
71
+ } : nil)
72
+ }
73
+ }
74
+ }
75
+
76
+ private struct DataListForEachRow: View {
77
+ let itemKey: String
78
+ let index: Int
79
+ @ObservedObject var listProps: DataListForEachProps
80
+ @ObservedObject var poolProps: DataListForEachPoolProps
81
+ let window: DataListForEachWindow
82
+ @State private var appeared = false
83
+ @State private var visibilityID = UUID()
84
+
85
+ // Slots are the pool's children; JS assigns item `index` to slot `index % count`.
86
+ // Reading them here, not in the parent, keeps the row current when the slot count changes.
87
+ private var slot: DataListForEachItemView? {
88
+ guard let slots = poolProps.children, !slots.isEmpty else {
89
+ return nil
90
+ }
91
+ return slots[index % slots.count].childView as? DataListForEachItemView
92
+ }
93
+
94
+ var body: some View {
95
+ VStack(alignment: .leading, spacing: 0) {
96
+ if let slot {
97
+ DataListForEachSlotContent(
98
+ props: slot.props,
99
+ itemKey: itemKey,
100
+ index: index,
101
+ revision: listProps.revision,
102
+ estimatedHeight: CGFloat(listProps.estimatedItemSize),
103
+ window: window
104
+ )
105
+ } else {
106
+ Color.clear
107
+ .frame(height: window.height(for: itemKey, fallback: listProps.estimatedItemSize))
108
+ .accessibilityHidden(true)
109
+ }
110
+ }
111
+ .frame(maxWidth: .infinity, alignment: .leading)
112
+ .onAppear {
113
+ appeared = true
114
+ window.appear(itemKey, index: index, token: visibilityID, props: listProps)
115
+ }
116
+ .onChange(of: listProps.revision) { _ in
117
+ window.reset(listProps)
118
+ if appeared { window.appear(itemKey, index: index, token: visibilityID, props: listProps) }
119
+ }
120
+ .onGeometryChange(for: CGFloat.self, of: { $0.size.width }, action: { width in
121
+ window.updateWidth(width)
122
+ })
123
+ .onDisappear {
124
+ appeared = false
125
+ window.disappear(itemKey, token: visibilityID)
126
+ }
127
+ }
128
+ }
129
+
130
+ private struct DataListForEachSlotContent: View {
131
+ @ObservedObject var props: DataListForEachItemProps
132
+ let itemKey: String
133
+ let index: Int
134
+ let revision: Int
135
+ let estimatedHeight: CGFloat
136
+ let window: DataListForEachWindow
137
+ // we use this state so swiftui re-layouts when the slot is reused for a different item
138
+ @State private var showsContent = false
139
+
140
+ private var matches: Bool {
141
+ props.itemKey == itemKey && props.index == index && props.revision == revision
142
+ }
143
+
144
+ var body: some View {
145
+ Group {
146
+ if showsContent {
147
+ DataListForEachItemView(props: props)
148
+ .id(itemKey)
149
+ .onGeometryChange(for: CGFloat.self, of: { $0.size.height }, action: { height in
150
+ window.measure(height, for: itemKey, revision: revision)
151
+ })
152
+ } else {
153
+ // Hide the previous item while JS updates this slot.
154
+ Color.clear
155
+ .frame(height: window.height(for: itemKey, fallback: Double(estimatedHeight)))
156
+ .accessibilityHidden(true)
157
+ }
158
+ }
159
+ .onAppear { showsContent = matches }
160
+ .onChange(of: matches) { showsContent = $0 }
161
+ }
162
+ }
163
+
164
+ // Tracks rows without triggering view updates and batches window requests to JS.
165
+ private final class DataListForEachWindow: ObservableObject {
166
+ private weak var props: DataListForEachProps?
167
+ private var appeared: [String: Int] = [:]
168
+ private var visibilityTokens: [String: UUID] = [:]
169
+ private var heights: [String: CGFloat] = [:]
170
+ private var width: CGFloat = 0
171
+ private var revision = -1
172
+ private var pending: DispatchWorkItem?
173
+ private var lastSent: [Int] = []
174
+
175
+ deinit {
176
+ pending?.cancel()
177
+ }
178
+
179
+ func reset(_ props: DataListForEachProps) {
180
+ self.props = props
181
+ guard revision != props.revision else {
182
+ return
183
+ }
184
+ revision = props.revision
185
+ heights.removeAll()
186
+ let positions = Dictionary(uniqueKeysWithValues: props.itemKeys.enumerated().map { ($0.element, $0.offset) })
187
+ appeared = positions.filter { appeared[$0.key] != nil }
188
+ visibilityTokens = visibilityTokens.filter { positions[$0.key] != nil }
189
+ lastSent = []
190
+ schedule()
191
+ }
192
+
193
+ func updateWidth(_ newWidth: CGFloat) {
194
+ if width != newWidth {
195
+ if width > 0 {
196
+ heights.removeAll()
197
+ }
198
+ width = newWidth
199
+ }
200
+ }
201
+
202
+ func appear(_ key: String, index: Int, token: UUID, props: DataListForEachProps) {
203
+ reset(props)
204
+ guard props.itemKeys.indices.contains(index), props.itemKeys[index] == key else {
205
+ return
206
+ }
207
+ appeared[key] = index
208
+ visibilityTokens[key] = token
209
+ schedule()
210
+ }
211
+
212
+ func disappear(_ key: String, token: UUID) {
213
+ // Ignore callbacks from a row that has been replaced.
214
+ guard visibilityTokens[key] == token else {
215
+ return
216
+ }
217
+ appeared.removeValue(forKey: key)
218
+ visibilityTokens.removeValue(forKey: key)
219
+ if appeared.isEmpty { lastSent = [] }
220
+ schedule()
221
+ }
222
+
223
+ func height(for key: String, fallback: Double) -> CGFloat {
224
+ heights[key] ?? CGFloat(fallback)
225
+ }
226
+
227
+ func measure(_ height: CGFloat, for key: String, revision: Int) {
228
+ guard revision == self.revision, height.isFinite, height > 0 else {
229
+ return
230
+ }
231
+ heights[key] = height
232
+ }
233
+
234
+ private func schedule() {
235
+ guard pending == nil else {
236
+ return
237
+ }
238
+ let work = DispatchWorkItem { [weak self] in
239
+ guard let self else { return }
240
+ self.pending = nil
241
+ guard let props = self.props,
242
+ let first = self.appeared.values.min(), let last = self.appeared.values.max() else {
243
+ return
244
+ }
245
+ let event = [first, last, self.revision]
246
+ guard event != self.lastSent else {
247
+ return
248
+ }
249
+ self.lastSent = event
250
+ props.onWindowChange(["first": first, "last": last, "revision": self.revision])
251
+ }
252
+ pending = work
253
+ DispatchQueue.main.async(execute: work)
254
+ }
255
+ }
@@ -170,6 +170,9 @@ public final class ExpoUIModule: Module {
170
170
  ExpoUIView(ImageView.self)
171
171
  ExpoUIView(LabelView.self)
172
172
  ExpoUIView(ListView.self)
173
+ ExpoUIView(DataListForEachView.self)
174
+ View(DataListForEachItemView.self)
175
+ View(DataListForEachPoolView.self)
173
176
  ExpoUIView(ListForEachView.self)
174
177
 
175
178
  ExpoUIView(PickerView.self)
@@ -4,6 +4,8 @@ import ExpoModulesCore
4
4
  import SwiftUI
5
5
 
6
6
  final class ListForEachProps: UIBaseViewProps {
7
+ @Field var deleteEnabled: Bool = false
8
+ @Field var moveEnabled: Bool = false
7
9
  var onDelete = EventDispatcher()
8
10
  var onMove = EventDispatcher()
9
11
  }
@@ -16,9 +18,16 @@ struct ListForEachView: ExpoSwiftUI.View {
16
18
  }
17
19
 
18
20
  var body: some View {
19
- Children()
20
- .onDelete(perform: handleDelete)
21
- .onMove(perform: handleMove)
21
+ ForEach(props.children ?? [], id: \.childIdentity) { child in
22
+ if let item = child.childView as? DataListForEachItemView {
23
+ item.tag(AnyHashable(item.props.itemKey))
24
+ } else {
25
+ let view: any View = child.childView
26
+ AnyView(view)
27
+ }
28
+ }
29
+ .onDelete(perform: props.deleteEnabled ? handleDelete : nil)
30
+ .onMove(perform: props.moveEnabled ? handleMove : nil)
22
31
  }
23
32
 
24
33
  func handleDelete(at offsets: IndexSet) {
@@ -3,7 +3,7 @@
3
3
  "component": {
4
4
  "group": "expo.modules.ui",
5
5
  "module": "expo.modules.ui",
6
- "version": "58.0.5",
6
+ "version": "58.0.6",
7
7
  "attributes": {
8
8
  "org.gradle.status": "release"
9
9
  }
@@ -24,8 +24,8 @@
24
24
  },
25
25
  "files": [
26
26
  {
27
- "name": "expo.modules.ui-58.0.5.aar",
28
- "url": "expo.modules.ui-58.0.5.aar",
27
+ "name": "expo.modules.ui-58.0.6.aar",
28
+ "url": "expo.modules.ui-58.0.6.aar",
29
29
  "size": 1792968,
30
30
  "sha512": "ddcedbdcc0e95db83f0657a68090572e6d7c7fb9233e796ea875d86e9782c619560ef0e98316cdf6642abbed4b42eb43b5e8264be55130f47e473adbc0f77c11",
31
31
  "sha256": "9b228fbd60d559e9dfebe894e1846a8c7ce3c72e5c91936b4382c97295e9b072",
@@ -141,8 +141,8 @@
141
141
  ],
142
142
  "files": [
143
143
  {
144
- "name": "expo.modules.ui-58.0.5.aar",
145
- "url": "expo.modules.ui-58.0.5.aar",
144
+ "name": "expo.modules.ui-58.0.6.aar",
145
+ "url": "expo.modules.ui-58.0.6.aar",
146
146
  "size": 1792968,
147
147
  "sha512": "ddcedbdcc0e95db83f0657a68090572e6d7c7fb9233e796ea875d86e9782c619560ef0e98316cdf6642abbed4b42eb43b5e8264be55130f47e473adbc0f77c11",
148
148
  "sha256": "9b228fbd60d559e9dfebe894e1846a8c7ce3c72e5c91936b4382c97295e9b072",
@@ -161,8 +161,8 @@
161
161
  },
162
162
  "files": [
163
163
  {
164
- "name": "expo.modules.ui-58.0.5-sources.jar",
165
- "url": "expo.modules.ui-58.0.5-sources.jar",
164
+ "name": "expo.modules.ui-58.0.6-sources.jar",
165
+ "url": "expo.modules.ui-58.0.6-sources.jar",
166
166
  "size": 106293,
167
167
  "sha512": "5a6dc466f00aa1d74a9e1df2c4a06f3bf9c00b9eebe03e54e1ae68db446806f3f47f98e4a1d04bd90db6683459dd11785d22499d9cef48dd979293f8a88104d3",
168
168
  "sha256": "d4e08c526d892c05d1ab6e42bd451df874e4da5cc58c18080ad730fc0ae59dfa",
@@ -0,0 +1 @@
1
+ 351d8fdc59045d3033183991bc4a41010bbdf928
@@ -0,0 +1 @@
1
+ 67875636aba690f3ed01c1885a8f5dd5c72824b9028ed012d77016a5c5bfef85
@@ -0,0 +1 @@
1
+ b7469559d94d21ec59ac3c32700d5c1651d2384574457e83c5f11eea0d6e947dd0ae5b68209f09056c30a0493e561c3a1dd74ea51cd3ec0435e8887603646187
@@ -9,7 +9,7 @@
9
9
  <modelVersion>4.0.0</modelVersion>
10
10
  <groupId>expo.modules.ui</groupId>
11
11
  <artifactId>expo.modules.ui</artifactId>
12
- <version>58.0.5</version>
12
+ <version>58.0.6</version>
13
13
  <packaging>aar</packaging>
14
14
  <name>expo.modules.ui</name>
15
15
  <url>https://github.com/expo/expo</url>
@@ -0,0 +1 @@
1
+ df98a93ccb93778ea5f6d701bf92e3caf1565000
@@ -0,0 +1 @@
1
+ 3b1a0a5386bc7a66e1e3097896cf2e63f8faccfa982ad72fc4d9f81dd6d7b9b7
@@ -0,0 +1 @@
1
+ d66ff20ada5d5bcd01c7ca0506db62ecc07e8eae9113f3e174b776efa9588663ef4bb7ea2761fba9c7ae518b3732c4185a7aee70d4a64e6fb0fcd579abdb5d94
@@ -3,11 +3,11 @@
3
3
  <groupId>expo.modules.ui</groupId>
4
4
  <artifactId>expo.modules.ui</artifactId>
5
5
  <versioning>
6
- <latest>58.0.5</latest>
7
- <release>58.0.5</release>
6
+ <latest>58.0.6</latest>
7
+ <release>58.0.6</release>
8
8
  <versions>
9
- <version>58.0.5</version>
9
+ <version>58.0.6</version>
10
10
  </versions>
11
- <lastUpdated>20260922160135</lastUpdated>
11
+ <lastUpdated>20260923154017</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- c4131ab7d3611c2ad7222519c1ac6dbc
1
+ 96559b0eabb8170769333ad05cdcfafe
@@ -1 +1 @@
1
- 40d27ef259a3a4594d9eb687a6acf1489c4d23a9
1
+ 9b146187e50f805cfcdce4249b9d56e1e1fdf9d6
@@ -1 +1 @@
1
- be1b14600c8809450004a609ef15392fa45790d99c199668b2fdbeeb38072cdf
1
+ 89c63e4e2a7fce899cd9101093d5c1105c62feffaea5ec288e6a39e5b8739446
@@ -1 +1 @@
1
- aa2b01073faddd2b62d7984ff6ff1637f53cdc23fb83803f432a6fc824ad2a88349638c7ab718dd0961d11ff3e7e0a7ee92ceea63c880257947348b3501c39fa
1
+ 50a66b6bfe1bbcedd9157651a817cac9309fb48c2a649c274b5a34e625370c8cd487af0235f5801320779f19049a619e06bcb71e69df646a129d06bfb0041c27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/ui",
3
- "version": "58.0.5",
3
+ "version": "58.0.6",
4
4
  "description": "A collection of UI components",
5
5
  "keywords": [
6
6
  "Jetpack Compose",
@@ -171,7 +171,7 @@
171
171
  "optional": true
172
172
  }
173
173
  },
174
- "gitHead": "add8cbf49f477cfd2283ae05af35b4713a773c55",
174
+ "gitHead": "8aeb4368d396d2d71a2fc8c652c4d1d4d53ed858",
175
175
  "scripts": {
176
176
  "precompile-ios": "et prebuild-package-for-publish",
177
177
  "build": "pnpm run build:lib && pnpm run build:plugin",
@@ -0,0 +1,249 @@
1
+ import { requireNativeView } from 'expo';
2
+ import { memo, useLayoutEffect, useMemo, useRef, useState, type ReactElement } from 'react';
3
+
4
+ import { type ViewEvent } from '../../types';
5
+ import { createViewModifierEventListener } from '../modifiers/utils';
6
+ import { type CommonViewModifierProps } from '../types';
7
+ import { getSlotIndices, getWindow } from './window';
8
+
9
+ export interface ListForEachProps<ItemT> extends CommonViewModifierProps {
10
+ /** Called with deleted indices from this group's `data` array. */
11
+ onDelete?: (indices: number[]) => void;
12
+ /**
13
+ * Called with the source indices and the destination index. The destination index counts
14
+ * positions before the moved items are removed, so it can equal `data.length`.
15
+ */
16
+ onMove?: (sourceIndices: number[], destination: number) => void;
17
+ /** Items to display. Replace the array when updating data. */
18
+ data: readonly ItemT[];
19
+ /** Returns a stable, unique string key, also used for `List` selection. */
20
+ keyExtractor: (item: ItemT, index: number) => string;
21
+ /**
22
+ * Renders a row. When `recycling` is `true`, wrap it in `useCallback`, or every row
23
+ * re-renders on each parent render.
24
+ * Recycled rows are reused for other items, so their local state (`useState`) carries over.
25
+ * Reset it when the item changes, or keep the state outside the row.
26
+ */
27
+ children: (info: { item: ItemT; index: number }) => ReactElement;
28
+ /**
29
+ * Renders only the rows near the visible range and reuses them while scrolling. Set to `false` to
30
+ * render every row at once. Set it once; changing it remounts the rows.
31
+ * @default true
32
+ */
33
+ recycling?: boolean;
34
+ /**
35
+ * Extra rows to prepare on each side of the visible rows. Must be a non-negative integer.
36
+ * Ignored when `recycling` is `false`.
37
+ * @default 10
38
+ */
39
+ overscanCount?: number;
40
+ /**
41
+ * Placeholder height in points, excluding `List` insets, until a row is measured.
42
+ * Must be positive. Measurements reset when `data` or width changes.
43
+ * Ignored when `recycling` is `false`.
44
+ * @default 64
45
+ */
46
+ estimatedItemSize?: number;
47
+ }
48
+
49
+ type WindowEvent = { first: number; last: number; revision: number };
50
+ type NativeProps = CommonViewModifierProps &
51
+ ViewEvent<'onWindowChange', WindowEvent> &
52
+ ViewEvent<'onDelete', { indices: number[]; revision: number }> &
53
+ ViewEvent<'onMove', { sourceIndices: number[]; destination: number; revision: number }> & {
54
+ deleteEnabled: boolean;
55
+ moveEnabled: boolean;
56
+ itemKeys: string[];
57
+ revision: number;
58
+ estimatedItemSize: number;
59
+ children: ReactElement;
60
+ };
61
+ type SlotProps = {
62
+ itemKey: string;
63
+ index: number;
64
+ revision: number;
65
+ children: ReactElement;
66
+ };
67
+
68
+ const NativeList = requireNativeView<NativeProps>('ExpoUI', 'DataListForEachView');
69
+ export const NativeSlot = requireNativeView<SlotProps>('ExpoUI', 'DataListForEachItemView');
70
+ const NativePool = requireNativeView<{ children: ReactElement[] }>(
71
+ 'ExpoUI',
72
+ 'DataListForEachPoolView'
73
+ );
74
+
75
+ // Skip unchanged rows when the window moves.
76
+ const RecycledRow = memo(function RecycledRow<ItemT>({
77
+ item,
78
+ index,
79
+ itemKey,
80
+ revision,
81
+ renderItem,
82
+ }: SlotPropsWithoutChildren & {
83
+ item: ItemT;
84
+ renderItem: ListForEachProps<ItemT>['children'];
85
+ }) {
86
+ return (
87
+ <NativeSlot itemKey={itemKey} index={index} revision={revision}>
88
+ {renderItem({ item, index })}
89
+ </NativeSlot>
90
+ );
91
+ }) as <ItemT>(
92
+ props: SlotPropsWithoutChildren & {
93
+ item: ItemT;
94
+ renderItem: ListForEachProps<ItemT>['children'];
95
+ }
96
+ ) => ReactElement;
97
+ type SlotPropsWithoutChildren = Omit<SlotProps, 'children'>;
98
+
99
+ export function useItemKeys<ItemT>(
100
+ data: readonly ItemT[],
101
+ keyExtractor: ListForEachProps<ItemT>['keyExtractor']
102
+ ): string[] {
103
+ const keyExtractorRef = useRef(keyExtractor);
104
+ keyExtractorRef.current = keyExtractor;
105
+ return useMemo(() => {
106
+ const keys = data.map((item, index) => keyExtractorRef.current(item, index));
107
+ if (keys.some((key) => typeof key !== 'string') || new Set(keys).size !== keys.length) {
108
+ throw new Error('List.ForEach keyExtractor must return a unique string for every item.');
109
+ }
110
+ return keys;
111
+ }, [data]);
112
+ }
113
+
114
+ /**
115
+ * Renders a window of recycled rows inside List or Section.
116
+ * Rows show placeholders while JS prepares content. Native keys still cover the full data set.
117
+ * @platform ios
118
+ */
119
+ export function DataListForEach<ItemT>({
120
+ data,
121
+ keyExtractor,
122
+ children: renderItem,
123
+ overscanCount = 10,
124
+ estimatedItemSize = 64,
125
+ modifiers,
126
+ onDelete,
127
+ onMove,
128
+ ...props
129
+ }: ListForEachProps<ItemT>) {
130
+ if (!Number.isSafeInteger(overscanCount) || overscanCount < 0) {
131
+ throw new Error('List.ForEach overscanCount must be a non-negative integer.');
132
+ }
133
+ if (!Number.isFinite(estimatedItemSize) || estimatedItemSize <= 0) {
134
+ throw new Error('List.ForEach estimatedItemSize must be a positive finite number.');
135
+ }
136
+ const itemKeys = useItemKeys(data, keyExtractor);
137
+ const [state, setState] = useState({
138
+ keys: itemKeys,
139
+ first: 0,
140
+ last: 0,
141
+ revision: 0,
142
+ capacity: 0,
143
+ overscanCount,
144
+ });
145
+ let current = state;
146
+ if (state.keys !== itemKeys) {
147
+ const previousKey = state.keys[state.first];
148
+ const anchor = previousKey === undefined ? 0 : Math.max(0, itemKeys.indexOf(previousKey));
149
+ current = {
150
+ ...state,
151
+ keys: itemKeys,
152
+ first: Math.min(anchor, Math.max(0, data.length - 1)),
153
+ last: Math.min(anchor + state.last - state.first, Math.max(0, data.length - 1)),
154
+ revision: state.revision + 1,
155
+ };
156
+ }
157
+ if (current.overscanCount !== overscanCount) {
158
+ current = { ...current, overscanCount, capacity: 0 };
159
+ }
160
+ if (current !== state) setState(current);
161
+ const { start, capacity } = getWindow(
162
+ data.length,
163
+ current.first,
164
+ current.last,
165
+ overscanCount,
166
+ current.capacity
167
+ );
168
+ const committedRevision = useRef(current.revision);
169
+ useLayoutEffect(() => {
170
+ committedRevision.current = current.revision;
171
+ }, [current.revision]);
172
+ const indices = getSlotIndices(start, capacity);
173
+
174
+ return (
175
+ <NativeList
176
+ {...props}
177
+ modifiers={modifiers}
178
+ {...(modifiers ? createViewModifierEventListener(modifiers) : undefined)}
179
+ deleteEnabled={!!onDelete}
180
+ moveEnabled={!!onMove}
181
+ onDelete={
182
+ onDelete
183
+ ? ({ nativeEvent }) => {
184
+ if (nativeEvent.revision === committedRevision.current) onDelete(nativeEvent.indices);
185
+ }
186
+ : undefined
187
+ }
188
+ onMove={
189
+ onMove
190
+ ? ({ nativeEvent }) => {
191
+ if (nativeEvent.revision === committedRevision.current) {
192
+ onMove(nativeEvent.sourceIndices, nativeEvent.destination);
193
+ }
194
+ }
195
+ : undefined
196
+ }
197
+ itemKeys={itemKeys}
198
+ revision={current.revision}
199
+ estimatedItemSize={estimatedItemSize}
200
+ onWindowChange={({ nativeEvent: event }) => {
201
+ if (
202
+ !Number.isSafeInteger(event.first) ||
203
+ !Number.isSafeInteger(event.last) ||
204
+ event.last < event.first
205
+ ) {
206
+ return;
207
+ }
208
+ setState((previous) => {
209
+ // Ignore requests from before a data change.
210
+ if (event.revision !== previous.revision) return previous;
211
+ const maximum = Math.max(0, previous.keys.length - 1);
212
+ const first = Math.max(0, Math.min(event.first, maximum));
213
+ const last = Math.max(first, Math.min(event.last, maximum));
214
+ const { capacity } = getWindow(
215
+ previous.keys.length,
216
+ first,
217
+ last,
218
+ previous.overscanCount,
219
+ previous.capacity
220
+ );
221
+ return first === previous.first &&
222
+ last === previous.last &&
223
+ capacity === previous.capacity
224
+ ? previous
225
+ : { ...previous, first, last, capacity };
226
+ });
227
+ }}>
228
+ <NativePool>
229
+ {indices.map((index, slot) => {
230
+ const item = data[index];
231
+ const itemKey = itemKeys[index];
232
+ if (item === undefined || itemKey === undefined) {
233
+ throw new Error(`List.ForEach could not resolve the item at index ${index}.`);
234
+ }
235
+ return (
236
+ <RecycledRow
237
+ key={slot}
238
+ item={item}
239
+ index={index}
240
+ itemKey={itemKey}
241
+ revision={current.revision}
242
+ renderItem={renderItem}
243
+ />
244
+ );
245
+ })}
246
+ </NativePool>
247
+ </NativeList>
248
+ );
249
+ }