@expo/ui 58.0.6 → 58.0.8

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 (113) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +9 -0
  4. package/android/src/main/java/expo/modules/ui/LazyColumnView.kt +6 -1
  5. package/android/src/main/java/expo/modules/ui/LazyItemsView.kt +313 -0
  6. package/android/src/main/java/expo/modules/ui/LazyRowView.kt +6 -1
  7. package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +11 -3
  8. package/android/src/main/java/expo/modules/ui/RNHostView.kt +18 -4
  9. package/android/src/main/java/expo/modules/ui/SemanticsModifier.kt +2 -1
  10. package/build/jetpack-compose/LazyColumn/index.d.ts +6 -0
  11. package/build/jetpack-compose/LazyColumn/index.js +2 -0
  12. package/build/jetpack-compose/LazyColumn/index.js.map +1 -1
  13. package/build/jetpack-compose/LazyItems/index.d.ts +54 -0
  14. package/build/jetpack-compose/LazyItems/index.js +63 -0
  15. package/build/jetpack-compose/LazyItems/index.js.map +1 -0
  16. package/build/jetpack-compose/LazyRow/index.d.ts +6 -0
  17. package/build/jetpack-compose/LazyRow/index.js +2 -0
  18. package/build/jetpack-compose/LazyRow/index.js.map +1 -1
  19. package/build/jetpack-compose/index.d.ts +1 -0
  20. package/build/jetpack-compose/index.js.map +1 -1
  21. package/build/jetpack-compose/modifiers/index.d.ts +15 -3
  22. package/build/jetpack-compose/modifiers/index.js.map +1 -1
  23. package/build/recycling/useRecycledRows.d.ts +52 -0
  24. package/build/recycling/useRecycledRows.js +121 -0
  25. package/build/recycling/useRecycledRows.js.map +1 -0
  26. package/build/recycling/window.js.map +1 -0
  27. package/build/swift-ui/List/DataListForEach.d.ts +2 -9
  28. package/build/swift-ui/List/DataListForEach.js +15 -106
  29. package/build/swift-ui/List/DataListForEach.js.map +1 -1
  30. package/build/swift-ui/List/ListForEach.js +3 -2
  31. package/build/swift-ui/List/ListForEach.js.map +1 -1
  32. package/build/swift-ui/modifiers/index.d.ts +5 -1
  33. package/build/swift-ui/modifiers/index.js.map +1 -1
  34. package/build/swift-ui/modifiers/presentationModifiers.d.ts +8 -0
  35. package/build/swift-ui/modifiers/presentationModifiers.js +9 -0
  36. package/build/swift-ui/modifiers/presentationModifiers.js.map +1 -1
  37. package/build/universal/State.android.d.ts +1 -3
  38. package/build/universal/State.d.ts +2 -3
  39. package/build/universal/State.ios.d.ts +1 -3
  40. package/build/universal/State.js +10 -4
  41. package/build/universal/State.js.map +1 -1
  42. package/build/universal/transformStyle.ios.js +4 -3
  43. package/build/universal/transformStyle.ios.js.map +1 -1
  44. package/build/universal/types.d.ts +24 -0
  45. package/build/universal/types.js +8 -2
  46. package/build/universal/types.js.map +1 -1
  47. package/expo-module.config.json +1 -1
  48. package/ios/Modifiers/PresentationModifiers.swift +10 -0
  49. package/ios/Modifiers/ViewModifierRegistry.swift +4 -0
  50. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.6/expo.modules.ui-58.0.6-sources.jar → 58.0.8/expo.modules.ui-58.0.8-sources.jar} +0 -0
  51. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8-sources.jar.md5 +1 -0
  52. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8-sources.jar.sha1 +1 -0
  53. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8-sources.jar.sha256 +1 -0
  54. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8-sources.jar.sha512 +1 -0
  55. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.aar +0 -0
  56. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.aar.md5 +1 -0
  57. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.aar.sha1 +1 -0
  58. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.aar.sha256 +1 -0
  59. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.aar.sha512 +1 -0
  60. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.6/expo.modules.ui-58.0.6.module → 58.0.8/expo.modules.ui-58.0.8.module} +24 -24
  61. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.module.md5 +1 -0
  62. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.module.sha1 +1 -0
  63. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.module.sha256 +1 -0
  64. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.module.sha512 +1 -0
  65. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.6/expo.modules.ui-58.0.6.pom → 58.0.8/expo.modules.ui-58.0.8.pom} +3 -3
  66. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.pom.md5 +1 -0
  67. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.pom.sha1 +1 -0
  68. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.pom.sha256 +1 -0
  69. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.8/expo.modules.ui-58.0.8.pom.sha512 +1 -0
  70. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  71. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  72. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  73. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  74. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  75. package/package.json +4 -4
  76. package/prebuilds/output/debug/xcframeworks/ExpoUI.tar.gz +0 -0
  77. package/prebuilds/output/release/xcframeworks/ExpoUI.tar.gz +0 -0
  78. package/src/jetpack-compose/LazyColumn/index.tsx +3 -0
  79. package/src/jetpack-compose/LazyItems/index.tsx +121 -0
  80. package/src/jetpack-compose/LazyRow/index.tsx +3 -0
  81. package/src/jetpack-compose/index.ts +1 -0
  82. package/src/jetpack-compose/modifiers/index.ts +16 -1
  83. package/src/recycling/useRecycledRows.tsx +200 -0
  84. package/src/swift-ui/List/DataListForEach.tsx +21 -142
  85. package/src/swift-ui/List/ListForEach.tsx +3 -2
  86. package/src/swift-ui/modifiers/index.ts +5 -1
  87. package/src/swift-ui/modifiers/presentationModifiers.ts +10 -0
  88. package/src/universal/State.android.ts +1 -1
  89. package/src/universal/State.ios.ts +1 -1
  90. package/src/universal/State.ts +12 -4
  91. package/src/universal/transformStyle.ios.ts +5 -2
  92. package/src/universal/types.ts +27 -0
  93. package/build/swift-ui/List/window.js.map +0 -1
  94. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6-sources.jar.md5 +0 -1
  95. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6-sources.jar.sha1 +0 -1
  96. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6-sources.jar.sha256 +0 -1
  97. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6-sources.jar.sha512 +0 -1
  98. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.aar +0 -0
  99. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.aar.md5 +0 -1
  100. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.aar.sha1 +0 -1
  101. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.aar.sha256 +0 -1
  102. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.aar.sha512 +0 -1
  103. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.module.md5 +0 -1
  104. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.module.sha1 +0 -1
  105. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.module.sha256 +0 -1
  106. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.module.sha512 +0 -1
  107. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.pom.md5 +0 -1
  108. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.pom.sha1 +0 -1
  109. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.pom.sha256 +0 -1
  110. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.6/expo.modules.ui-58.0.6.pom.sha512 +0 -1
  111. /package/build/{swift-ui/List → recycling}/window.d.ts +0 -0
  112. /package/build/{swift-ui/List → recycling}/window.js +0 -0
  113. /package/src/{swift-ui/List → recycling}/window.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,24 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 58.0.8 — 2026-09-28
14
+
15
+ ### 🎉 New features
16
+
17
+ - [iOS] Export the `FrameAlignment` type for the `alignment` parameter of `frame()`. ([#50702](https://github.com/expo/expo/pull/50702) by [@amandeepmittal](https://github.com/amandeepmittal))
18
+ - [Android] Add `contentDescription` to the `semantics` modifier, so TalkBack can announce a label other than the visible content. ([#50688](https://github.com/expo/expo/pull/50688) by [@enavermate](https://github.com/enavermate))
19
+ - Added `get()` and `set()` to the universal `ObservableState` type and to the web implementation of `useNativeState`, as React Compiler-compliant alternatives to `.value`. ([#50684](https://github.com/expo/expo/pull/50684) by [@Den1Marshall](https://github.com/Den1Marshall))
20
+
21
+ ## 58.0.7 — 2026-09-25
22
+
23
+ ### 🎉 New features
24
+
25
+ - [iOS] Added `presentationCornerRadius` modifier. ([#50632](https://github.com/expo/expo/pull/50632) by [@shanelord01](https://github.com/shanelord01))
26
+
27
+ ### 🐛 Bug fixes
28
+
29
+ - [iOS] Fixed `onPress` on universal components such as `Row` and `Column` not firing when tapping empty space, such as a `Spacer`. ([#50599](https://github.com/expo/expo/pull/50599) by [@expo-bot](https://github.com/expo-bot))
30
+
13
31
  ## 58.0.6 — 2026-09-23
14
32
 
15
33
  ### 🛠 Breaking changes
@@ -19,6 +37,7 @@
19
37
  ### 🎉 New features
20
38
 
21
39
  - [iOS] `List.ForEach` recycles rows from a small pool around the visible range, so large lists only render the rows near the viewport. Deprecated element children in favor of this form. ([#49975](https://github.com/expo/expo/pull/49975) by [@nishan](https://github.com/intergalacticspacehighway))
40
+ - [Android] Added `LazyColumn.Items` and `LazyRow.Items`, which render rows from `data` and `keyExtractor` with a `children` function: `{({ item, index }) => <Row item={item} />}`. Rows are recycled from a small pool around the visible range, so large lists only render the rows near the viewport. Set `recycling={false}` to render every row. ([#50258](https://github.com/expo/expo/pull/50258) by [@nishan](https://github.com/intergalacticspacehighway))
22
41
 
23
42
  ## 58.0.5 — 2026-09-22
24
43
 
@@ -101,6 +120,7 @@
101
120
 
102
121
  ### 🐛 Bug fixes
103
122
 
123
+ - [Android] Fix hosted React Native content sliding off the top of a `BottomSheet` when the keyboard opens. ([#49399](https://github.com/expo/expo/issues/49399) by [@starsky-nev](https://github.com/starsky-nev)) ([#49427](https://github.com/expo/expo/pull/49427) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
104
124
  - [Android] Fix `DatePickerDialog` preselecting today when `initialDate` is omitted, and keep its confirm button disabled while no date is selected so `onDateSelected` never receives an invalid date. ([#49898](https://github.com/expo/expo/pull/49898) by [@pataar](https://github.com/pataar))
105
125
  - [Android][iOS] Fix `community/bottom-sheet` content shrinking to its own width instead of filling the sheet when the sheet sizes to its content. ([#49742](https://github.com/expo/expo/issues/49742) by [@agung-adhinata](https://github.com/agung-adhinata)) ([#49762](https://github.com/expo/expo/pull/49762) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
106
126
  - [Android] Fixed a `Text` or an `Icon` with no explicit color rendering black inside `Host`, which made it unreadable in the dark color scheme. `Host` now provides `LocalContentColor` from the color scheme. ([#49697](https://github.com/expo/expo/pull/49697) by [@expo-bot](https://github.com/expo-bot))
@@ -12,13 +12,13 @@ apply plugin: 'expo-module-gradle-plugin'
12
12
  apply plugin: 'org.jetbrains.kotlin.plugin.compose'
13
13
 
14
14
  group = 'expo.modules.ui'
15
- version = '58.0.6'
15
+ version = '58.0.8'
16
16
 
17
17
  android {
18
18
  namespace "expo.modules.ui"
19
19
  defaultConfig {
20
20
  versionCode 1
21
- versionName "58.0.6"
21
+ versionName "58.0.8"
22
22
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
23
23
  }
24
24
  buildFeatures {
@@ -203,6 +203,15 @@ class ExpoUIModule : Module() {
203
203
  }
204
204
  View(LazyColumnView::class)
205
205
  View(LazyRowView::class)
206
+ View(LazyItemsView::class) {
207
+ Events("onWindowChange")
208
+
209
+ OnViewDestroys { view: LazyItemsView ->
210
+ view.cancelPendingWindowChange()
211
+ }
212
+ }
213
+ View(LazyItemsPoolView::class)
214
+ View(LazyItemsSlotView::class)
206
215
 
207
216
  // Class-based views so TooltipBoxView can detect them by type via findChildOfType
208
217
  View(PlainTooltipView::class)
@@ -69,7 +69,8 @@ class LazyColumnView(context: Context, appContext: AppContext) :
69
69
  val padding = props.contentPadding.value
70
70
 
71
71
  LazyColumn(
72
- modifier = ModifierRegistry.applyModifiers(props.modifiers.value, appContext, this@Content, globalEventDispatcher),
72
+ modifier = ModifierRegistry.applyModifiers(props.modifiers.value, appContext, this@Content, globalEventDispatcher)
73
+ .lazyRecycledItemsCrossAxis(this@LazyColumnView, isVertical = true),
73
74
  verticalArrangement = verticalArrangement,
74
75
  horizontalAlignment = horizontalAlignment,
75
76
  contentPadding = PaddingValues(
@@ -82,6 +83,10 @@ class LazyColumnView(context: Context, appContext: AppContext) :
82
83
  val count = composableChildCount.intValue
83
84
  for (index in 0..<count) {
84
85
  val child = getChildAt(index) as? ExpoComposeView<*> ?: continue
86
+ if (child is LazyItemsView) {
87
+ lazyRecycledItems(child, this@Content, isVertical = true)
88
+ continue
89
+ }
85
90
  item {
86
91
  with(this@Content) {
87
92
  with(child) {
@@ -0,0 +1,313 @@
1
+ package expo.modules.ui
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import android.view.View
8
+ import android.view.ViewGroup
9
+ import androidx.compose.foundation.layout.Box
10
+ import androidx.compose.foundation.layout.Spacer
11
+ import androidx.compose.foundation.layout.fillMaxHeight
12
+ import androidx.compose.foundation.layout.fillMaxWidth
13
+ import androidx.compose.foundation.layout.height
14
+ import androidx.compose.foundation.layout.width
15
+ import androidx.compose.foundation.lazy.LazyListScope
16
+ import androidx.compose.runtime.Composable
17
+ import androidx.compose.runtime.DisposableEffect
18
+ import androidx.compose.runtime.MutableIntState
19
+ import androidx.compose.runtime.MutableState
20
+ import androidx.compose.runtime.mutableIntStateOf
21
+ import androidx.compose.runtime.mutableStateOf
22
+ import androidx.compose.runtime.remember
23
+ import androidx.compose.ui.Modifier
24
+ import androidx.compose.ui.layout.layout
25
+ import androidx.compose.ui.layout.onSizeChanged
26
+ import androidx.compose.ui.platform.LocalDensity
27
+ import androidx.compose.ui.unit.dp
28
+ import expo.modules.kotlin.AppContext
29
+ import expo.modules.kotlin.records.Field
30
+ import expo.modules.kotlin.records.Record
31
+ import expo.modules.kotlin.viewevent.EventDispatcher
32
+ import expo.modules.kotlin.views.ComposableScope
33
+ import expo.modules.kotlin.views.ComposeProps
34
+ import expo.modules.kotlin.views.ExpoComposeView
35
+ import expo.modules.kotlin.views.OptimizedComposeProps
36
+
37
+ @OptimizedComposeProps
38
+ data class LazyItemsProps(
39
+ val itemKeys: MutableState<List<String>> = mutableStateOf(emptyList()),
40
+ val revision: MutableState<Int> = mutableStateOf(0),
41
+ val estimatedItemSize: MutableState<Double> = mutableStateOf(64.0)
42
+ ) : ComposeProps
43
+
44
+ internal data class WindowChangeEvent(
45
+ @Field val first: Int,
46
+ @Field val last: Int,
47
+ @Field val revision: Int
48
+ ) : Record
49
+
50
+ /**
51
+ * A block of recycled items inside [LazyColumnView] or [LazyRowView]. It renders no content of its
52
+ * own: [lazyRecycledItems] expands it into one lazy item per key, each backed by a slot of its
53
+ * pool child.
54
+ */
55
+ @SuppressLint("ViewConstructor")
56
+ class LazyItemsView(context: Context, appContext: AppContext) :
57
+ ExpoComposeView<LazyItemsProps>(context, appContext) {
58
+ override val props = LazyItemsProps()
59
+ private val onWindowChange by EventDispatcher<WindowChangeEvent>()
60
+
61
+ internal val pool: MutableState<LazyItemsPoolView?> = mutableStateOf(null)
62
+ internal val window = LazyItemsWindow { first, last, revision ->
63
+ onWindowChange(WindowChangeEvent(first, last, revision))
64
+ }
65
+
66
+ override fun onViewAdded(child: View?) {
67
+ super.onViewAdded(child)
68
+ pool.value = findChildOfType<LazyItemsPoolView>(this)
69
+ }
70
+
71
+ override fun onViewRemoved(child: View?) {
72
+ super.onViewRemoved(child)
73
+ pool.value = findChildOfType<LazyItemsPoolView>(this)
74
+ }
75
+
76
+ internal fun cancelPendingWindowChange() {
77
+ window.cancel()
78
+ }
79
+
80
+ @Composable
81
+ override fun ComposableScope.Content() = Unit
82
+ }
83
+
84
+ @OptimizedComposeProps
85
+ class LazyItemsPoolProps : ComposeProps
86
+
87
+ /**
88
+ * Holds the recycled slots, so mounting a slot never republishes the props of [LazyItemsView].
89
+ */
90
+ @SuppressLint("ViewConstructor")
91
+ class LazyItemsPoolView(context: Context, appContext: AppContext) :
92
+ ExpoComposeView<LazyItemsPoolProps>(context, appContext) {
93
+ override val props = LazyItemsPoolProps()
94
+
95
+ internal val slotCount: MutableIntState = mutableIntStateOf(0)
96
+
97
+ override fun onViewAdded(child: View?) {
98
+ super.onViewAdded(child)
99
+ slotCount.intValue = childCount
100
+ }
101
+
102
+ override fun onViewRemoved(child: View?) {
103
+ super.onViewRemoved(child)
104
+ slotCount.intValue = childCount
105
+ }
106
+
107
+ @Composable
108
+ override fun ComposableScope.Content() = Unit
109
+ }
110
+
111
+ @OptimizedComposeProps
112
+ data class LazyItemsSlotProps(
113
+ val itemKey: MutableState<String> = mutableStateOf(""),
114
+ val index: MutableState<Int> = mutableStateOf(0),
115
+ val revision: MutableState<Int> = mutableStateOf(0)
116
+ ) : ComposeProps
117
+
118
+ /**
119
+ * One recycled slot. JS assigns item `index` to slot `index % slotCount` and republishes these
120
+ * props when the slot moves to another item.
121
+ */
122
+ @SuppressLint("ViewConstructor")
123
+ class LazyItemsSlotView(context: Context, appContext: AppContext) :
124
+ ExpoComposeView<LazyItemsSlotProps>(context, appContext) {
125
+ override val props = LazyItemsSlotProps()
126
+
127
+ @Composable
128
+ override fun ComposableScope.Content() {
129
+ Children(this)
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Expands a [LazyItemsView] child into one lazy item per key. An item composes its slot only while
135
+ * the slot still carries that item, and shows a placeholder of the measured or estimated size
136
+ * while JS moves the slot over.
137
+ */
138
+ internal fun LazyListScope.lazyRecycledItems(
139
+ view: LazyItemsView,
140
+ scope: ComposableScope,
141
+ isVertical: Boolean
142
+ ) {
143
+ val keys = view.props.itemKeys.value
144
+ val revision = view.props.revision.value
145
+ val estimatedItemSize = view.props.estimatedItemSize.value
146
+ if (keys.isEmpty()) {
147
+ return
148
+ }
149
+ val window = view.window
150
+ items(count = keys.size, key = { keys[it] }) { index ->
151
+ val itemKey = keys[index]
152
+ val token = remember(itemKey, index, revision) { Any() }
153
+ DisposableEffect(token) {
154
+ window.appear(itemKey, index, token, revision, keys)
155
+ onDispose { window.disappear(itemKey, token) }
156
+ }
157
+
158
+ val pool = view.pool.value
159
+ val slotCount = pool?.slotCount?.intValue ?: 0
160
+ val slot = if (pool != null && slotCount > 0) {
161
+ pool.getChildAt(index % slotCount) as? LazyItemsSlotView
162
+ } else {
163
+ null
164
+ }
165
+ // A slot carries this item only until JS moves it to another one.
166
+ val content = slot?.takeIf {
167
+ it.props.itemKey.value == itemKey &&
168
+ it.props.index.value == index &&
169
+ it.props.revision.value == revision
170
+ }
171
+
172
+ val density = LocalDensity.current
173
+ val placeholderSize = with(density) {
174
+ window.size(itemKey, estimatedItemSize.dp.roundToPx()).toDp()
175
+ }
176
+ Box(
177
+ modifier = Modifier.onSizeChanged { size ->
178
+ if (content != null) {
179
+ window.measure(itemKey, if (isVertical) size.height else size.width, revision)
180
+ }
181
+ }
182
+ ) {
183
+ if (content != null) {
184
+ with(scope) {
185
+ with(content) {
186
+ Content()
187
+ }
188
+ }
189
+ } else {
190
+ Spacer(
191
+ modifier = if (isVertical) {
192
+ Modifier.fillMaxWidth().height(placeholderSize)
193
+ } else {
194
+ Modifier.fillMaxHeight().width(placeholderSize)
195
+ }
196
+ )
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Clears the cached item sizes of every [LazyItemsView] child when the list's cross-axis
204
+ * constraint changes. It runs before the items measure, so it never discards fresh sizes.
205
+ */
206
+ internal fun Modifier.lazyRecycledItemsCrossAxis(list: ViewGroup, isVertical: Boolean): Modifier =
207
+ layout { measurable, constraints ->
208
+ val crossAxisSize = if (isVertical) constraints.maxWidth else constraints.maxHeight
209
+ for (index in 0..<list.childCount) {
210
+ (list.getChildAt(index) as? LazyItemsView)?.window?.updateCrossAxisSize(crossAxisSize)
211
+ }
212
+ val placeable = measurable.measure(constraints)
213
+ layout(placeable.width, placeable.height) {
214
+ placeable.place(0, 0)
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Tracks the live items without triggering recomposition, and batches window requests to JS.
220
+ */
221
+ internal class LazyItemsWindow(
222
+ private val send: (first: Int, last: Int, revision: Int) -> Unit
223
+ ) {
224
+ private val handler = Handler(Looper.getMainLooper())
225
+ private val appeared = mutableMapOf<String, Int>()
226
+ private val tokens = mutableMapOf<String, Any>()
227
+ private val sizes = mutableMapOf<String, Int>()
228
+ private var crossAxisSize = 0
229
+ private var revision = -1
230
+ private var pending = false
231
+ private var lastSent: Triple<Int, Int, Int>? = null
232
+
233
+ fun appear(key: String, index: Int, token: Any, revision: Int, keys: List<String>) {
234
+ reset(revision, keys)
235
+ if (index !in keys.indices || keys[index] != key) {
236
+ return
237
+ }
238
+ appeared[key] = index
239
+ tokens[key] = token
240
+ schedule()
241
+ }
242
+
243
+ fun disappear(key: String, token: Any) {
244
+ // Ignore callbacks from an item that has been replaced.
245
+ if (tokens[key] !== token) {
246
+ return
247
+ }
248
+ appeared.remove(key)
249
+ tokens.remove(key)
250
+ if (appeared.isEmpty()) {
251
+ lastSent = null
252
+ }
253
+ schedule()
254
+ }
255
+
256
+ fun size(key: String, fallback: Int): Int = sizes[key] ?: fallback
257
+
258
+ fun measure(key: String, size: Int, revision: Int) {
259
+ if (revision != this.revision || size <= 0) {
260
+ return
261
+ }
262
+ sizes[key] = size
263
+ }
264
+
265
+ fun updateCrossAxisSize(size: Int) {
266
+ if (size == crossAxisSize) {
267
+ return
268
+ }
269
+ if (crossAxisSize > 0) {
270
+ sizes.clear()
271
+ }
272
+ crossAxisSize = size
273
+ }
274
+
275
+ fun cancel() {
276
+ handler.removeCallbacksAndMessages(null)
277
+ pending = false
278
+ }
279
+
280
+ private fun reset(newRevision: Int, keys: List<String>) {
281
+ if (revision == newRevision) {
282
+ return
283
+ }
284
+ revision = newRevision
285
+ sizes.clear()
286
+ val positions = HashMap<String, Int>(keys.size)
287
+ keys.forEachIndexed { index, key -> positions[key] = index }
288
+ val kept = appeared.keys.mapNotNull { key -> positions[key]?.let { key to it } }
289
+ appeared.clear()
290
+ appeared.putAll(kept)
291
+ tokens.keys.retainAll { positions.containsKey(it) }
292
+ lastSent = null
293
+ schedule()
294
+ }
295
+
296
+ private fun schedule() {
297
+ if (pending) {
298
+ return
299
+ }
300
+ pending = true
301
+ handler.post {
302
+ pending = false
303
+ val first = appeared.values.minOrNull() ?: return@post
304
+ val last = appeared.values.maxOrNull() ?: return@post
305
+ val event = Triple(first, last, revision)
306
+ if (event == lastSent) {
307
+ return@post
308
+ }
309
+ lastSent = event
310
+ send(first, last, revision)
311
+ }
312
+ }
313
+ }
@@ -58,7 +58,8 @@ class LazyRowView(context: Context, appContext: AppContext) :
58
58
  val padding = props.contentPadding.value
59
59
 
60
60
  LazyRow(
61
- modifier = ModifierRegistry.applyModifiers(props.modifiers.value, appContext, this@Content, globalEventDispatcher),
61
+ modifier = ModifierRegistry.applyModifiers(props.modifiers.value, appContext, this@Content, globalEventDispatcher)
62
+ .lazyRecycledItemsCrossAxis(this@LazyRowView, isVertical = false),
62
63
  horizontalArrangement = horizontalArrangement,
63
64
  verticalAlignment = verticalAlignment,
64
65
  contentPadding = PaddingValues(
@@ -71,6 +72,10 @@ class LazyRowView(context: Context, appContext: AppContext) :
71
72
  val count = composableChildCount.intValue
72
73
  for (index in 0..<count) {
73
74
  val child = getChildAt(index) as? ExpoComposeView<*> ?: continue
75
+ if (child is LazyItemsView) {
76
+ lazyRecycledItems(child, this@Content, isVertical = false)
77
+ continue
78
+ }
74
79
  item {
75
80
  with(this@Content) {
76
81
  with(child) {
@@ -67,6 +67,7 @@ import androidx.compose.ui.layout.onVisibilityChanged
67
67
  import androidx.compose.ui.layout.positionInWindow
68
68
  import androidx.compose.ui.platform.LocalDensity
69
69
  import androidx.compose.ui.semantics.Role
70
+ import androidx.compose.ui.semantics.contentDescription
70
71
  import androidx.compose.ui.semantics.contentType
71
72
  import androidx.compose.ui.semantics.semantics
72
73
  import androidx.compose.ui.unit.DpOffset
@@ -681,9 +682,16 @@ object ModifierRegistry {
681
682
 
682
683
  register("semantics") { map, _, appContext, _ ->
683
684
  val params = recordFromMap<SemanticsParams>(map, appContext)
684
- params.contentType.toContentType()?.let { ct ->
685
- Modifier.semantics { contentType = ct }
686
- } ?: Modifier
685
+ val type = params.contentType.toContentType()
686
+ val description = params.contentDescription
687
+ if (type == null && description == null) {
688
+ Modifier
689
+ } else {
690
+ Modifier.semantics {
691
+ type?.let { contentType = it }
692
+ description?.let { contentDescription = it }
693
+ }
694
+ }
687
695
  }
688
696
 
689
697
  register("clip") { map, _, appContext, _ ->
@@ -10,9 +10,11 @@ import android.view.ViewParent
10
10
  import android.widget.FrameLayout
11
11
  import androidx.compose.foundation.layout.fillMaxSize
12
12
  import androidx.compose.foundation.layout.requiredSize
13
+ import androidx.compose.foundation.layout.wrapContentSize
13
14
  import androidx.compose.runtime.Composable
14
15
  import androidx.compose.runtime.MutableState
15
16
  import androidx.compose.runtime.mutableStateOf
17
+ import androidx.compose.ui.Alignment
16
18
  import androidx.compose.ui.Modifier
17
19
  import androidx.compose.ui.layout.onGloballyPositioned
18
20
  import androidx.compose.ui.layout.onSizeChanged
@@ -180,10 +182,22 @@ internal class RNHostView(context: Context, appContext: AppContext) :
180
182
 
181
183
  return with(density) {
182
184
  if (childSize.width > 0 && childSize.height > 0) {
183
- Modifier.requiredSize(
184
- childSize.width.toDp(),
185
- childSize.height.toDp()
186
- )
185
+ // When RNHostView's size is greater than its parent, then parent centers RNHostView and truncates its top and bottom.
186
+ // e.g. Parent size = 200px, RNHostView size = 300px, the parent will position the RNHostView at center and truncate 50px from top and bottom.
187
+ // This causes issues where the top of RNHostView can go out of screen.
188
+ // These scenario mostly happen when a Keyboard opens in a sheet.
189
+ // https://github.com/expo/expo/issues/49399
190
+ // Adding wrapContentSize with Alignment.TopCenter and unbounded = true
191
+ // makes sure that the RNHostView is always positioned at the top of its parent.
192
+ // Only the vertical axis changes. A matchContents child is measured unconstrained, so it is
193
+ // routinely wider than its Compose slot, and centering it horizontally is what keeps its
194
+ // contents lined up with the parent.
195
+ Modifier
196
+ .wrapContentSize(Alignment.TopCenter, unbounded = true)
197
+ .requiredSize(
198
+ childSize.width.toDp(),
199
+ childSize.height.toDp()
200
+ )
187
201
  } else {
188
202
  Modifier
189
203
  }
@@ -7,7 +7,8 @@ import expo.modules.kotlin.types.OptimizedRecord
7
7
 
8
8
  @OptimizedRecord
9
9
  internal data class SemanticsParams(
10
- @Field val contentType: String? = null
10
+ @Field val contentType: String? = null,
11
+ @Field val contentDescription: String? = null
11
12
  ) : Record
12
13
 
13
14
  internal fun String?.toContentType(): ContentType? = when (this) {
@@ -49,3 +49,9 @@ export interface LazyColumnProps {
49
49
  * A lazy column component that efficiently displays a vertically scrolling list.
50
50
  */
51
51
  export declare function LazyColumn(props: LazyColumnProps): import("react").JSX.Element;
52
+ export declare namespace LazyColumn {
53
+ var Items: {
54
+ <ItemT>({ recycling, ...props }: import("..").LazyItemsProps<ItemT>): import("react").JSX.Element;
55
+ displayName: string;
56
+ };
57
+ }
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { requireNativeView } from 'expo';
3
+ import { LazyItems } from '../LazyItems';
3
4
  import { createViewModifierEventListener } from '../modifiers/utils';
4
5
  const LazyColumnNativeView = requireNativeView('ExpoUI', 'LazyColumnView');
5
6
  function transformProps(props) {
@@ -17,5 +18,6 @@ function transformProps(props) {
17
18
  ...transformProps(props)
18
19
  });
19
20
  }
21
+ LazyColumn.Items = LazyItems;
20
22
 
21
23
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["jetpack-compose/LazyColumn/index.tsx"],"sourcesContent":[null],"names":["requireNativeView","createViewModifierEventListener","LazyColumnNativeView","transformProps","props","modifiers","restProps","undefined","LazyColumn"],"mappings":";AAAA,SAASA,iBAAiB,QAAQ,OAAO;AAGzC,SAASC,+BAA+B,QAAQ,qBAAqB;AAwDrE,MAAMC,uBAAmEF,kBACvE,UACA;AAGF,SAASG,eAAeC,KAAsB;IAC5C,MAAM,EAAEC,SAAS,EAAE,GAAGC,WAAW,GAAGF;IACpC,OAAO;QACLC;QACA,GAAIA,YAAYJ,gCAAgCI,aAAaE,SAAS;QACtE,GAAGD,SAAS;IACd;AACF;AAEA;;CAEC,GACD,OAAO,SAASE,WAAWJ,KAAsB;IAC/C,qBAAO,KAACF;QAAsB,GAAGC,eAAeC,MAAM;;AACxD"}
1
+ {"version":3,"sources":["jetpack-compose/LazyColumn/index.tsx"],"sourcesContent":[null],"names":["requireNativeView","LazyItems","createViewModifierEventListener","LazyColumnNativeView","transformProps","props","modifiers","restProps","undefined","LazyColumn","Items"],"mappings":";AAAA,SAASA,iBAAiB,QAAQ,OAAO;AAGzC,SAASC,SAAS,QAAQ,eAAe;AACzC,SAASC,+BAA+B,QAAQ,qBAAqB;AAwDrE,MAAMC,uBAAmEH,kBACvE,UACA;AAGF,SAASI,eAAeC,KAAsB;IAC5C,MAAM,EAAEC,SAAS,EAAE,GAAGC,WAAW,GAAGF;IACpC,OAAO;QACLC;QACA,GAAIA,YAAYJ,gCAAgCI,aAAaE,SAAS;QACtE,GAAGD,SAAS;IACd;AACF;AAEA;;CAEC,GACD,OAAO,SAASE,WAAWJ,KAAsB;IAC/C,qBAAO,KAACF;QAAsB,GAAGC,eAAeC,MAAM;;AACxD;AAEAI,WAAWC,KAAK,GAAGT"}
@@ -0,0 +1,54 @@
1
+ import { type RenderItem } from '../../recycling/useRecycledRows';
2
+ export interface LazyItemsProps<ItemT> {
3
+ /** Items to display. Replace the array when updating data. */
4
+ data: readonly ItemT[];
5
+ /** Returns a stable, unique string key, also used as the lazy list item key. */
6
+ keyExtractor: (item: ItemT, index: number) => string;
7
+ /**
8
+ * Renders a row. Wrap it in `useCallback`, or every row re-renders on each parent render.
9
+ * Recycled rows are reused for other items, so their local state (`useState`) carries over.
10
+ * Reset it when the item changes, or keep the state outside the row.
11
+ */
12
+ children: RenderItem<ItemT>;
13
+ /**
14
+ * Renders only the rows near the visible range and reuses them while scrolling. Set to `false` to
15
+ * render every row at once. Set it once; changing it remounts the rows.
16
+ * @default true
17
+ */
18
+ recycling?: boolean;
19
+ /**
20
+ * Extra rows to prepare on each side of the visible rows. Must be a non-negative integer.
21
+ * Ignored when `recycling` is `false`.
22
+ * @default 10
23
+ */
24
+ overscanCount?: number;
25
+ /**
26
+ * Placeholder size in dp along the scroll axis, until a row is measured. Must be positive.
27
+ * Measurements reset when the data changes. In `LazyColumn.Items` they also reset when the list
28
+ * width changes. Ignored when `recycling` is `false`.
29
+ * @default 64
30
+ */
31
+ estimatedItemSize?: number;
32
+ }
33
+ /**
34
+ * A block of recycled rows inside `LazyColumn` or `LazyRow`, mirroring the Compose
35
+ * `items(count, key)` builder. Only a small pool of rows around the visible range is mounted,
36
+ * so large data sets stay cheap. Rows that are not ready yet show a placeholder of
37
+ * `estimatedItemSize`, or of the size last measured for that item.
38
+ *
39
+ * Mount it as a direct child of `LazyColumn` or `LazyRow`. Plain children of the same list stay
40
+ * single items, so static and recycled content can be mixed in order.
41
+ * @platform android
42
+ */
43
+ export declare const LazyItems: {
44
+ <ItemT>({ recycling, ...props }: LazyItemsProps<ItemT>): import("react").JSX.Element;
45
+ displayName: string;
46
+ };
47
+ /**
48
+ * @hidden
49
+ * Creates the `Items` component of one lazy list, so its errors name that list.
50
+ */
51
+ export declare function createLazyItems(componentName: string): {
52
+ <ItemT>({ recycling, ...props }: LazyItemsProps<ItemT>): import("react").JSX.Element;
53
+ displayName: string;
54
+ };
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { requireNativeView } from 'expo';
3
+ import { Fragment } from 'react';
4
+ import { useItemKeys, useRecycledRows } from '../../recycling/useRecycledRows';
5
+ const LazyItemsNativeView = requireNativeView('ExpoUI', 'LazyItemsView');
6
+ const LazyItemsSlotNativeView = requireNativeView('ExpoUI', 'LazyItemsSlotView');
7
+ const LazyItemsPoolNativeView = requireNativeView('ExpoUI', 'LazyItemsPoolView');
8
+ /**
9
+ * A block of recycled rows inside `LazyColumn` or `LazyRow`, mirroring the Compose
10
+ * `items(count, key)` builder. Only a small pool of rows around the visible range is mounted,
11
+ * so large data sets stay cheap. Rows that are not ready yet show a placeholder of
12
+ * `estimatedItemSize`, or of the size last measured for that item.
13
+ *
14
+ * Mount it as a direct child of `LazyColumn` or `LazyRow`. Plain children of the same list stay
15
+ * single items, so static and recycled content can be mixed in order.
16
+ * @platform android
17
+ */ export const LazyItems = createLazyItems('LazyColumn.Items');
18
+ /**
19
+ * @hidden
20
+ * Creates the `Items` component of one lazy list, so its errors name that list.
21
+ */ export function createLazyItems(componentName) {
22
+ function Items({ recycling = true, ...props }) {
23
+ return recycling ? /*#__PURE__*/ _jsx(RecycledItems, {
24
+ ...props
25
+ }) : /*#__PURE__*/ _jsx(StaticItems, {
26
+ ...props
27
+ });
28
+ }
29
+ // Each row is a plain child of the lazy list, so it becomes one lazy item.
30
+ function StaticItems({ data, keyExtractor, children: renderItem }) {
31
+ const itemKeys = useItemKeys(componentName, data, keyExtractor);
32
+ return data.map((item, index)=>/*#__PURE__*/ _jsx(Fragment, {
33
+ children: renderItem({
34
+ item,
35
+ index
36
+ })
37
+ }, itemKeys[index]));
38
+ }
39
+ function RecycledItems({ data, keyExtractor, children: renderItem, overscanCount, estimatedItemSize = 64 }) {
40
+ const { itemKeys, revision, rows, onWindowChange } = useRecycledRows({
41
+ componentName,
42
+ Slot: LazyItemsSlotNativeView,
43
+ data,
44
+ keyExtractor,
45
+ renderItem,
46
+ overscanCount,
47
+ estimatedItemSize
48
+ });
49
+ return /*#__PURE__*/ _jsx(LazyItemsNativeView, {
50
+ itemKeys: itemKeys,
51
+ revision: revision,
52
+ estimatedItemSize: estimatedItemSize,
53
+ onWindowChange: ({ nativeEvent })=>onWindowChange(nativeEvent),
54
+ children: /*#__PURE__*/ _jsx(LazyItemsPoolNativeView, {
55
+ children: rows
56
+ })
57
+ });
58
+ }
59
+ Items.displayName = componentName;
60
+ return Items;
61
+ }
62
+
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["jetpack-compose/LazyItems/index.tsx"],"sourcesContent":[null],"names":["requireNativeView","Fragment","useItemKeys","useRecycledRows","LazyItemsNativeView","LazyItemsSlotNativeView","LazyItemsPoolNativeView","LazyItems","createLazyItems","componentName","Items","recycling","props","RecycledItems","StaticItems","data","keyExtractor","children","renderItem","itemKeys","map","item","index","overscanCount","estimatedItemSize","revision","rows","onWindowChange","Slot","nativeEvent","displayName"],"mappings":";AAAA,SAASA,iBAAiB,QAAQ,OAAO;AACzC,SAASC,QAAQ,QAA2B,QAAQ;AAEpD,SACEC,WAAW,EACXC,eAAe,QAIV,kCAAkC;AA0CzC,MAAMC,sBACJJ,kBAAwC,UAAU;AACpD,MAAMK,0BACJL,kBAAqC,UAAU;AACjD,MAAMM,0BACJN,kBAAkB,UAAU;AAE9B;;;;;;;;;CASC,GACD,OAAO,MAAMO,YAAYC,gBAAgB,oBAAoB;AAE7D;;;CAGC,GACD,OAAO,SAASA,gBAAgBC,aAAqB;IACnD,SAASC,MAAa,EAAEC,YAAY,IAAI,EAAE,GAAGC,OAA8B;QACzE,OAAOD,0BAAY,KAACE;YAAe,GAAGD,KAAK;2BAAO,KAACE;YAAa,GAAGF,KAAK;;IAC1E;IAEA,2EAA2E;IAC3E,SAASE,YAAmB,EAC1BC,IAAI,EACJC,YAAY,EACZC,UAAUC,UAAU,EACqB;QACzC,MAAMC,WAAWjB,YAAYO,eAAeM,MAAMC;QAClD,OAAOD,KAAKK,GAAG,CAAC,CAACC,MAAMC,sBACrB,KAACrB;0BAAgCiB,WAAW;oBAAEG;oBAAMC;gBAAM;eAA3CH,QAAQ,CAACG,MAAM;IAElC;IAEA,SAAST,cAAqB,EAC5BE,IAAI,EACJC,YAAY,EACZC,UAAUC,UAAU,EACpBK,aAAa,EACbC,oBAAoB,EAAE,EACmB;QACzC,MAAM,EAAEL,QAAQ,EAAEM,QAAQ,EAAEC,IAAI,EAAEC,cAAc,EAAE,GAAGxB,gBAAgB;YACnEM;YACAmB,MAAMvB;YACNU;YACAC;YACAE;YACAK;YACAC;QACF;QAEA,qBACE,KAACpB;YACCe,UAAUA;YACVM,UAAUA;YACVD,mBAAmBA;YACnBG,gBAAgB,CAAC,EAAEE,WAAW,EAAE,GAAKF,eAAeE;sBACpD,cAAA,KAACvB;0BAAyBoB;;;IAGhC;IACAhB,MAAMoB,WAAW,GAAGrB;IACpB,OAAOC;AACT"}