@expo/ui 56.0.16 → 56.0.17

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 (99) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +40 -6
  4. package/android/src/main/java/expo/modules/ui/HostView.kt +0 -2
  5. package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +20 -0
  6. package/android/src/main/java/expo/modules/ui/RNHostView.kt +182 -6
  7. package/android/src/main/java/expo/modules/ui/textfield/BasicTextField.kt +203 -0
  8. package/android/src/main/java/expo/modules/ui/{TextFieldView.kt → textfield/TextField.kt} +34 -248
  9. package/android/src/main/java/expo/modules/ui/textfield/TextFieldShared.kt +299 -0
  10. package/build/State/useNativeState.d.ts +8 -3
  11. package/build/State/useNativeState.d.ts.map +1 -1
  12. package/build/community/pager-view/PagerView.android.d.ts.map +1 -1
  13. package/build/jetpack-compose/TextField/BasicTextField.d.ts +36 -0
  14. package/build/jetpack-compose/TextField/BasicTextField.d.ts.map +1 -0
  15. package/build/jetpack-compose/TextField/TextField.d.ts +131 -0
  16. package/build/jetpack-compose/TextField/TextField.d.ts.map +1 -0
  17. package/build/jetpack-compose/TextField/index.d.ts +3 -244
  18. package/build/jetpack-compose/TextField/index.d.ts.map +1 -1
  19. package/build/jetpack-compose/TextField/shared.d.ts +171 -0
  20. package/build/jetpack-compose/TextField/shared.d.ts.map +1 -0
  21. package/build/jetpack-compose/index.d.ts +1 -1
  22. package/build/jetpack-compose/index.d.ts.map +1 -1
  23. package/build/jetpack-compose/modifiers/index.d.ts +11 -0
  24. package/build/jetpack-compose/modifiers/index.d.ts.map +1 -1
  25. package/build/swift-ui/Image/index.d.ts +3 -1
  26. package/build/swift-ui/Image/index.d.ts.map +1 -1
  27. package/build/swift-ui/modifiers/index.d.ts +33 -4
  28. package/build/swift-ui/modifiers/index.d.ts.map +1 -1
  29. package/build/universal/TextInput/index.android.d.ts.map +1 -1
  30. package/build/universal/TextInput/types.d.ts +5 -1
  31. package/build/universal/TextInput/types.d.ts.map +1 -1
  32. package/expo-module.config.json +1 -1
  33. package/ios/ImageView.swift +1 -5
  34. package/ios/Modifiers/ImageScaleModifier.swift +29 -0
  35. package/ios/Modifiers/OnGeometryChangeModifier.swift +8 -16
  36. package/ios/Modifiers/ViewModifierRegistry.swift +32 -0
  37. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.16/expo.modules.ui-56.0.16-sources.jar → 56.0.17/expo.modules.ui-56.0.17-sources.jar} +0 -0
  38. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17-sources.jar.md5 +1 -0
  39. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17-sources.jar.sha1 +1 -0
  40. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17-sources.jar.sha256 +1 -0
  41. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17-sources.jar.sha512 +1 -0
  42. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.aar +0 -0
  43. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.aar.md5 +1 -0
  44. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.aar.sha1 +1 -0
  45. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.aar.sha256 +1 -0
  46. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.aar.sha512 +1 -0
  47. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.16/expo.modules.ui-56.0.16.module → 56.0.17/expo.modules.ui-56.0.17.module} +22 -22
  48. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.module.md5 +1 -0
  49. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.module.sha1 +1 -0
  50. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.module.sha256 +1 -0
  51. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.module.sha512 +1 -0
  52. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.16/expo.modules.ui-56.0.16.pom → 56.0.17/expo.modules.ui-56.0.17.pom} +1 -1
  53. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.pom.md5 +1 -0
  54. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.pom.sha1 +1 -0
  55. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.pom.sha256 +1 -0
  56. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.17/expo.modules.ui-56.0.17.pom.sha512 +1 -0
  57. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  58. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  59. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  60. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  61. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  62. package/package.json +3 -3
  63. package/src/State/index.fx.ts +4 -1
  64. package/src/State/useNativeState.ts +24 -5
  65. package/src/community/datetime-picker/DateTimePicker.tsx +1 -1
  66. package/src/community/menu/MenuView.ios.tsx +1 -1
  67. package/src/community/pager-view/PagerView.android.tsx +16 -2
  68. package/src/community/pager-view/PagerView.ios.tsx +1 -1
  69. package/src/community/picker/Picker.ios.tsx +1 -1
  70. package/src/community/slider/Slider.ios.tsx +1 -1
  71. package/src/jetpack-compose/TextField/BasicTextField.tsx +118 -0
  72. package/src/jetpack-compose/TextField/TextField.tsx +198 -0
  73. package/src/jetpack-compose/TextField/index.ts +19 -0
  74. package/src/jetpack-compose/TextField/{index.tsx → shared.ts} +71 -203
  75. package/src/jetpack-compose/index.ts +6 -0
  76. package/src/jetpack-compose/modifiers/index.ts +13 -0
  77. package/src/swift-ui/BottomSheet/index.tsx +1 -1
  78. package/src/swift-ui/Image/index.tsx +12 -3
  79. package/src/swift-ui/modifiers/index.ts +42 -5
  80. package/src/universal/TextInput/index.android.tsx +26 -60
  81. package/src/universal/TextInput/types.ts +5 -1
  82. package/android/src/main/java/expo/modules/ui/ShadowNodeSyncFlush.kt +0 -28
  83. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16-sources.jar.md5 +0 -1
  84. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16-sources.jar.sha1 +0 -1
  85. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16-sources.jar.sha256 +0 -1
  86. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16-sources.jar.sha512 +0 -1
  87. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar +0 -0
  88. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar.md5 +0 -1
  89. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar.sha1 +0 -1
  90. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar.sha256 +0 -1
  91. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar.sha512 +0 -1
  92. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.module.md5 +0 -1
  93. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.module.sha1 +0 -1
  94. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.module.sha256 +0 -1
  95. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.module.sha512 +0 -1
  96. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.pom.md5 +0 -1
  97. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.pom.sha1 +0 -1
  98. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.pom.sha256 +0 -1
  99. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,22 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.17 — 2026-06-10
14
+
15
+ ### 🛠 Breaking changes
16
+
17
+ - [universal][android] Use `BasicTextField` component instead of Filled Material TextField. ([#46442](https://github.com/expo/expo/pull/46442) by [@nishan](https://github.com/intergalacticspacehighway))
18
+
19
+ ### 🎉 New features
20
+
21
+ - [iOS] Added the SwiftUI `imageScale` modifier to scale SF Symbols within a view relative to the surrounding text (`small`, `medium`, `large`). ([#46774](https://github.com/expo/expo/pull/46774) by [@ramonclaudio](https://github.com/ramonclaudio))
22
+ - [jetpack-compose] Added `onGloballyPositioned` modifier, which reports a composable's window position and size. ([#46744](https://github.com/expo/expo/pull/46744) by [@nishan](https://github.com/intergalacticspacehighway))
23
+ - [iOS] Extended the SwiftUI `onGeometryChange` modifier to also report the view's global position (`x`/`y`) alongside its size. ([#46744](https://github.com/expo/expo/pull/46744) by [@nishan](https://github.com/intergalacticspacehighway))
24
+ - [iOS] Added the SwiftUI `minimumScaleFactor` modifier to let text shrink down to a given fraction of its size before truncating. ([#46740](https://github.com/expo/expo/pull/46740) by [@nishan](https://github.com/intergalacticspacehighway))
25
+ - [iOS][android] Added React Compiler-friendly `get()` / `set()` accessors to `useNativeState`, as an alternative to reading and writing `.value`. ([#46690](https://github.com/expo/expo/pull/46692) by [@nishan](https://github.com/intergalacticspacehighway))
26
+ - [jetpack-compose] Added `BasicTextField` component. ([#46442](https://github.com/expo/expo/pull/46442) by [@nishan](https://github.com/intergalacticspacehighway))
27
+ - [iOS] Added the SwiftUI `accessibilityInputLabels` modifier to set alternative spoken phrases Voice Control uses to refer to a view (for example "Hang up" for an "End" button). ([#46661](https://github.com/expo/expo/pull/46661) by [@ramonclaudio](https://github.com/ramonclaudio))
28
+
13
29
  ## 56.0.16 — 2026-06-05
14
30
 
15
31
  ### 🎉 New features
@@ -28,7 +44,13 @@
28
44
 
29
45
  ### 🐛 Bug fixes
30
46
 
47
+ - [android] Fix React Native touchables (e.g. `Pressable`) on `community/pager-view` pages not responding, or triggering the wrong page's handler, after navigating between pages. ([#46778](https://github.com/expo/expo/pull/46778) by [@nishan](https://github.com/intergalacticspacehighway))
48
+ - [iOS] Fix `font`, `dynamicTypeSize`, and `resizable` modifiers not applying to the SwiftUI `Image`. SF Symbols scale with Dynamic Type when a `font` modifier sets a `textStyle`. ([#46714](https://github.com/expo/expo/pull/46714) by [@ramonclaudio](https://github.com/ramonclaudio))
49
+ - [android] Fix React Native `ScrollView` nested scrolling inside `BottomSheet`. ([#46544](https://github.com/expo/expo/pull/46544) by [@nishan](https://github.com/intergalacticspacehighway))
50
+ - [jetpack-compose] Fix layout shift when `Host` with `matchContents` is used inside React Native Screens. ([#46604](https://github.com/expo/expo/pull/46604) by [@nishan](https://github.com/intergalacticspacehighway))
51
+ - [iOS] Fix `PagerView` offsetting `ScrollView` by safe area insets. ([#46637](https://github.com/expo/expo/pull/46637) by [@nishan](https://github.com/intergalacticspacehighway))
31
52
  - [iOS] Fix `SegmentedControl` being overlapped by sibling components inside a `ScrollView`, by disabling the `Host` safe area insets. ([#46575](https://github.com/expo/expo/pull/46575) by [@nishan](https://github.com/intergalacticspacehighway))
53
+ - [iOS] Fix the community `DateTimePicker`, `MenuView`, `Picker`, and `Slider` applying safe area insets (including keyboard avoidance) twice, by disabling the `Host` safe area insets (`ignoreSafeArea="all"`). ([#46721](https://github.com/expo/expo/pull/46721) by [@nishan](https://github.com/intergalacticspacehighway))
32
54
  - [jetpack-compose] Fix `TextField` jiggling the surrounding content while its label animates on focus (a Material 3 expressive motion spring overshoot). ([#46568](https://github.com/expo/expo/pull/46568) by [@nishan](https://github.com/intergalacticspacehighway))
33
55
  - [iOS] Fix `BottomSheet` animating open from the bottom-left corner in `fitToContents` mode. ([#46546](https://github.com/expo/expo/pull/46546) by [@nishan](https://github.com/intergalacticspacehighway))
34
56
  - [iOS] Fix `TextField` and `SecureField` worklet `onTextChange` firing more than once per keystroke (when a change triggers reformatting) and on programmatic text updates. ([#46483](https://github.com/expo/expo/pull/46483) by [@nishan](https://github.com/intergalacticspacehighway))
@@ -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 = '56.0.16'
15
+ version = '56.0.17'
16
16
 
17
17
  android {
18
18
  namespace "expo.modules.ui"
19
19
  defaultConfig {
20
20
  versionCode 1
21
- versionName "56.0.16"
21
+ versionName "56.0.17"
22
22
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
23
23
  }
24
24
  buildFeatures {
@@ -36,6 +36,15 @@ import expo.modules.ui.menu.DropdownMenuItemProps
36
36
  import expo.modules.kotlin.jni.worklets.Worklet
37
37
  import expo.modules.ui.state.ObservableState
38
38
  import expo.modules.ui.state.WorkletCallback
39
+ import expo.modules.ui.textfield.BasicTextFieldContent
40
+ import expo.modules.ui.textfield.BasicTextFieldProps
41
+ import expo.modules.ui.textfield.InnerTextFieldView
42
+ import expo.modules.ui.textfield.KeyboardActionEvent
43
+ import expo.modules.ui.textfield.PlaceholderView
44
+ import expo.modules.ui.textfield.TextFieldContent
45
+ import expo.modules.ui.textfield.TextFieldProps
46
+ import expo.modules.ui.textfield.TextFieldSelectionPayload
47
+ import expo.modules.ui.textfield.TextFieldValuePayload
39
48
  import expo.modules.ui.menu.ExposedDropdownMenuBoxContent
40
49
  import expo.modules.ui.menu.ExposedDropdownMenuBoxProps
41
50
  import expo.modules.ui.menu.ExposedDropdownMenuContent
@@ -109,8 +118,7 @@ class ExpoUIModule : Module() {
109
118
  //region Views use expo-modules-core DSL for uncommon features
110
119
 
111
120
  View(HostView::class) {
112
- // See ShadowNodeSyncFlush.kt for why onExpoUISyncFlush is needed.
113
- Events("onLayoutContent", "onExpoUISyncFlush")
121
+ Events("onLayoutContent")
114
122
 
115
123
  OnViewDidUpdateProps { view ->
116
124
  view.onViewDidUpdateProps()
@@ -147,14 +155,13 @@ class ExpoUIModule : Module() {
147
155
  colorScheme.toTokenMap()
148
156
  }
149
157
 
150
- View(RNHostView::class) {
151
- // See ShadowNodeSyncFlush.kt for why this internal phantom event is needed.
152
- Events("onExpoUISyncFlush")
153
- }
158
+ View(RNHostView::class)
154
159
 
155
160
  View(SlotView::class) {
156
161
  Events("onSlotEvent")
157
162
  }
163
+ View(InnerTextFieldView::class)
164
+ View(PlaceholderView::class)
158
165
  View(IconView::class)
159
166
  View(LazyColumnView::class)
160
167
  View(LazyRowView::class)
@@ -697,6 +704,33 @@ class ExpoUIModule : Module() {
697
704
  }
698
705
  }
699
706
 
707
+ ExpoUIView<BasicTextFieldProps>("BasicTextFieldView") {
708
+ val setText by AsyncFunction<String>()
709
+ val setSelection by AsyncFunction<Int, Int>()
710
+ val clear by AsyncFunction()
711
+ val focus by AsyncFunction()
712
+ val blur by AsyncFunction()
713
+ val onValueChange by Event<TextFieldValuePayload>()
714
+ val onFocusChanged by Event<GenericEventPayload1<Boolean>>()
715
+ val onKeyboardAction by Event<KeyboardActionEvent>()
716
+ val onSelectionChange by Event<TextFieldSelectionPayload>()
717
+
718
+ Content { props ->
719
+ BasicTextFieldContent(
720
+ props,
721
+ setText,
722
+ setSelection,
723
+ clear,
724
+ focus,
725
+ blur,
726
+ onValueChanged = { onValueChange(it) },
727
+ onFocusChange = { onFocusChanged(it) },
728
+ onKeyboardActionTriggered = { onKeyboardAction(it) },
729
+ onSelectionChanged = { onSelectionChange(it) }
730
+ )
731
+ }
732
+ }
733
+
700
734
  ExpoUIView<RadioButtonProps>("RadioButtonView") {
701
735
  val onButtonPressed by Event<Unit>()
702
736
 
@@ -193,7 +193,6 @@ internal class HostView(context: Context, appContext: AppContext) :
193
193
  if (constraints.maxWidth == 0) widthDp else Double.NaN,
194
194
  if (constraints.maxHeight == 0) heightDp else Double.NaN
195
195
  )
196
- flushPendingStateUpdates()
197
196
  }
198
197
  }
199
198
 
@@ -222,7 +221,6 @@ internal class HostView(context: Context, appContext: AppContext) :
222
221
  val styleWidth = if (matchContentsHorizontal == true && width > 0) width else null
223
222
  val styleHeight = if (matchContentsVertical == true && height > 0) height else null
224
223
  shadowNodeProxy.setStyleSize(styleWidth?.toDouble(), styleHeight?.toDouble())
225
- flushPendingStateUpdates()
226
224
  }
227
225
 
228
226
  onLayoutContent(LayoutContentEvent(width.toDouble(), height.toDouble()))
@@ -57,8 +57,10 @@ import androidx.compose.ui.graphics.Shape
57
57
  import androidx.compose.ui.graphics.TransformOrigin
58
58
  import androidx.compose.ui.graphics.graphicsLayer
59
59
  import androidx.compose.ui.graphics.shadow.Shadow
60
+ import androidx.compose.ui.layout.onGloballyPositioned
60
61
  import androidx.compose.ui.layout.onSizeChanged
61
62
  import androidx.compose.ui.layout.onVisibilityChanged
63
+ import androidx.compose.ui.layout.positionInWindow
62
64
  import androidx.compose.ui.platform.LocalDensity
63
65
  import androidx.compose.ui.semantics.Role
64
66
  import androidx.compose.ui.semantics.contentType
@@ -692,6 +694,24 @@ object ModifierRegistry {
692
694
  }
693
695
  }
694
696
 
697
+ register("onGloballyPositioned") { _, _, _, eventDispatcher ->
698
+ val density = LocalDensity.current
699
+ Modifier.onGloballyPositioned { coordinates ->
700
+ val position = coordinates.positionInWindow()
701
+ with(density) {
702
+ eventDispatcher(
703
+ "onGloballyPositioned",
704
+ mapOf(
705
+ "x" to position.x.toDp().value,
706
+ "y" to position.y.toDp().value,
707
+ "width" to coordinates.size.width.toDp().value,
708
+ "height" to coordinates.size.height.toDp().value
709
+ )
710
+ )
711
+ }
712
+ }
713
+ }
714
+
695
715
  register("clickable") { map, _, _, eventDispatcher ->
696
716
  val params = recordFromMap<ClickableParams>(map)
697
717
  if (params.indication) {
@@ -19,6 +19,9 @@ import androidx.compose.ui.layout.onSizeChanged
19
19
  import androidx.compose.ui.platform.LocalDensity
20
20
  import androidx.compose.ui.unit.IntSize
21
21
  import androidx.compose.ui.viewinterop.AndroidView
22
+ import androidx.core.view.NestedScrollingChild3
23
+ import androidx.core.view.NestedScrollingChildHelper
24
+ import androidx.core.view.ViewCompat
22
25
  import com.facebook.react.bridge.ReactContext
23
26
  import com.facebook.react.common.annotations.UnstableReactNativeAPI
24
27
  import com.facebook.react.config.ReactFeatureFlags
@@ -158,7 +161,6 @@ internal class RNHostView(context: Context, appContext: AppContext) :
158
161
  size.width.toDp().value.toDouble(),
159
162
  size.height.toDp().value.toDouble()
160
163
  )
161
- flushPendingStateUpdates()
162
164
  }
163
165
  }
164
166
  }
@@ -168,13 +170,30 @@ internal class RNHostView(context: Context, appContext: AppContext) :
168
170
  * A thin FrameLayout that intercepts touch events and dispatches them to JS via
169
171
  * JSTouchDispatcher/JSPointerDispatcher, replicating the pattern from React Native's
170
172
  * DialogRootViewGroup in ReactModalHostView.
173
+ * Implements NestedScrollingChild3 to forward scroll events up to the parent Compose view, because Compose only listens for NestedScrollingChild3 nested-scroll events.
171
174
  */
172
175
  private class TouchDispatchingRootViewGroup(
173
176
  context: Context
174
- ) : FrameLayout(context), RootView {
177
+ ) : FrameLayout(context), RootView, NestedScrollingChild3 {
175
178
  private val jsTouchDispatcher = JSTouchDispatcher(this)
176
179
  private var jsPointerDispatcher: JSPointerDispatcher? = null
177
180
 
181
+ // The "child face": this helper does the real work of finding the nearest scrolling-aware
182
+ // ancestor (Compose, here) and forwarding our scroll offers to it.
183
+ private val childHelper = NestedScrollingChildHelper(this)
184
+
185
+ // How far the sheet has slid this view on-screen since the gesture began. Used to keep the
186
+ // FlatList's touch coordinates coherent while the view moves under the finger.
187
+ private val gestureStartLocation = IntArray(2)
188
+ private val currentLocation = IntArray(2)
189
+ private var trackingGestureOffset = false
190
+
191
+ // True if the sheet consumed scroll on the most recent drag frame; drives the settle decision.
192
+ private var sheetMovingOnLastDragFrame = false
193
+
194
+ // True once a fling was dispatched this gesture, so the gentle-release settle doesn't double-fire.
195
+ private var flingHandledThisGesture = false
196
+
178
197
  var eventDispatcher: EventDispatcher? = null
179
198
 
180
199
  private val reactContext: ThemedReactContext
@@ -184,6 +203,7 @@ private class TouchDispatchingRootViewGroup(
184
203
  if (ReactFeatureFlags.dispatchPointerEvents) {
185
204
  jsPointerDispatcher = JSPointerDispatcher(this)
186
205
  }
206
+ childHelper.isNestedScrollingEnabled = true
187
207
  }
188
208
 
189
209
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -200,6 +220,38 @@ private class TouchDispatchingRootViewGroup(
200
220
  // and we must not override those values.
201
221
  }
202
222
 
223
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
224
+ if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
225
+ // dispatchTouchEvent is the true start of every gesture, so reset all per-gesture state here.
226
+ getLocationInWindow(gestureStartLocation)
227
+ trackingGestureOffset = true
228
+ sheetMovingOnLastDragFrame = false
229
+ flingHandledThisGesture = false
230
+ }
231
+
232
+ // While a nested scroll is in flight the sheet may be sliding this whole view up/down. Re-express
233
+ // every event as if the view hadn't moved, so the FlatList's own scroll tracking stays coherent.
234
+ val handled = if (trackingGestureOffset && hasNestedScrollingParent()) {
235
+ getLocationInWindow(currentLocation)
236
+ val dy = currentLocation[1] - gestureStartLocation[1]
237
+ if (dy != 0) {
238
+ ev.offsetLocation(0f, dy.toFloat())
239
+ val result = super.dispatchTouchEvent(ev)
240
+ ev.offsetLocation(0f, -dy.toFloat())
241
+ result
242
+ } else {
243
+ super.dispatchTouchEvent(ev)
244
+ }
245
+ } else {
246
+ super.dispatchTouchEvent(ev)
247
+ }
248
+
249
+ if (ev.actionMasked == MotionEvent.ACTION_UP || ev.actionMasked == MotionEvent.ACTION_CANCEL) {
250
+ trackingGestureOffset = false
251
+ }
252
+ return handled
253
+ }
254
+
203
255
  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
204
256
  eventDispatcher?.let { dispatcher ->
205
257
  jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
@@ -241,12 +293,136 @@ private class TouchDispatchingRootViewGroup(
241
293
  jsPointerDispatcher?.onChildEndedNativeGesture()
242
294
  }
243
295
 
296
+ override fun handleException(t: Throwable) {
297
+ reactContext.reactApplicationContext.handleException(RuntimeException(t))
298
+ }
299
+
244
300
  override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
245
- // No-op - override to still receive events in onInterceptTouchEvent
246
- // even when a child view disallows interception
301
+ // Forward the request up so Compose learns the list claimed the gesture and its own sheet-drag
302
+ // yields. But don't call super: setting our own FLAG_DISALLOW_INTERCEPT would skip
303
+ // onInterceptTouchEvent, which must keep firing to dispatch touches to JS (the reason #43716
304
+ // added this override).
305
+ parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
247
306
  }
248
307
 
249
- override fun handleException(t: Throwable) {
250
- reactContext.reactApplicationContext.handleException(RuntimeException(t))
308
+ // --- Parent face: catch the FlatList's scroll offers and relay them up via the child face. ---
309
+ override fun onStartNestedScroll(child: View, target: View, axes: Int): Boolean =
310
+ axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
311
+
312
+ override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
313
+ super.onNestedScrollAccepted(child, target, axes)
314
+ startNestedScroll(axes)
251
315
  }
316
+
317
+ override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
318
+ // Expand path: offer the delta to the sheet (above us) before the list scrolls with the rest.
319
+ dispatchNestedPreScroll(dx, dy, consumed, null)
320
+ // Did the sheet move this frame? consumed[1] is what it consumed in y axis.
321
+ sheetMovingOnLastDragFrame = consumed[1] != 0
322
+ }
323
+
324
+ override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {
325
+ // Collapse path: after the list scrolled what it could, hand the leftover up to the sheet.
326
+ // Use the (…, type, consumed) variant so we can read how much the sheet ate (consumed[1]).
327
+ val consumed = IntArray(2)
328
+ dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, ViewCompat.TYPE_TOUCH, consumed)
329
+ if (consumed[1] != 0) sheetMovingOnLastDragFrame = true
330
+ }
331
+
332
+ override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
333
+ // A real fling is being dispatched, so its settle is already in motion; note that so the
334
+ // gentle-release fallback in onStopNestedScroll doesn't add a second, redundant settle.
335
+ flingHandledThisGesture = true
336
+ val composeConsumed = dispatchNestedPreFling(velocityX, velocityY)
337
+ // RN's ScrollView wants a synchronous yes/no, but Compose's pre-fling is async (it returns false
338
+ // now and settles later). So decide here: if the sheet was mid-move and this is an up-flick
339
+ // (velocityY > 0), swallow the list's fling so the sheet finishes expanding. A down-flick passes
340
+ // through so it can still settle/collapse.
341
+ return composeConsumed || (sheetMovingOnLastDragFrame && velocityY > 0)
342
+ }
343
+
344
+ override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean =
345
+ // Post-fling: hand the leftover flick up so the sheet can fling to its next anchor.
346
+ dispatchNestedFling(velocityX, velocityY, consumed)
347
+
348
+ override fun onStopNestedScroll(target: View) {
349
+ // Must run BEFORE stopNestedScroll() so Compose is still our parent and can receive the fling.
350
+ settleSheetIfNoFling()
351
+ super.onStopNestedScroll(target)
352
+ stopNestedScroll()
353
+ }
354
+
355
+ // A slow (sub-threshold) release never flings, so the sheet would hang where it was dragged. If the
356
+ // sheet moved this gesture and no real fling settled it, dispatch a zero-velocity fling so the
357
+ // holder snaps it to the nearest anchor.
358
+ private fun settleSheetIfNoFling() {
359
+ if (sheetMovingOnLastDragFrame && !flingHandledThisGesture) {
360
+ flingHandledThisGesture = true
361
+ dispatchNestedFling(0f, 0f, false)
362
+ }
363
+ }
364
+
365
+ // --- Child face: the wrapper's upstream voice. The parent hooks above relay through these. ---
366
+ // So we listen to ViewGroup nested scroll methods and call below methods from them,
367
+ // which essentially converts regular Nested scrolling methods to NestedScrollingChild3.
368
+ override fun setNestedScrollingEnabled(enabled: Boolean) {
369
+ childHelper.isNestedScrollingEnabled = enabled
370
+ }
371
+ override fun isNestedScrollingEnabled(): Boolean = childHelper.isNestedScrollingEnabled
372
+ override fun startNestedScroll(axes: Int): Boolean = childHelper.startNestedScroll(axes)
373
+ override fun startNestedScroll(axes: Int, type: Int): Boolean = childHelper.startNestedScroll(axes, type)
374
+ override fun stopNestedScroll() = childHelper.stopNestedScroll()
375
+ override fun stopNestedScroll(type: Int) = childHelper.stopNestedScroll(type)
376
+ override fun hasNestedScrollingParent(): Boolean = childHelper.hasNestedScrollingParent()
377
+ override fun hasNestedScrollingParent(type: Int): Boolean = childHelper.hasNestedScrollingParent(type)
378
+
379
+ override fun dispatchNestedScroll(
380
+ dxConsumed: Int,
381
+ dyConsumed: Int,
382
+ dxUnconsumed: Int,
383
+ dyUnconsumed: Int,
384
+ offsetInWindow: IntArray?
385
+ ): Boolean = childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
386
+
387
+ override fun dispatchNestedScroll(
388
+ dxConsumed: Int,
389
+ dyConsumed: Int,
390
+ dxUnconsumed: Int,
391
+ dyUnconsumed: Int,
392
+ offsetInWindow: IntArray?,
393
+ type: Int
394
+ ): Boolean = childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type)
395
+
396
+ override fun dispatchNestedScroll(
397
+ dxConsumed: Int,
398
+ dyConsumed: Int,
399
+ dxUnconsumed: Int,
400
+ dyUnconsumed: Int,
401
+ offsetInWindow: IntArray?,
402
+ type: Int,
403
+ consumed: IntArray
404
+ ) {
405
+ childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed)
406
+ }
407
+
408
+ override fun dispatchNestedPreScroll(
409
+ dx: Int,
410
+ dy: Int,
411
+ consumed: IntArray?,
412
+ offsetInWindow: IntArray?
413
+ ): Boolean = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
414
+
415
+ override fun dispatchNestedPreScroll(
416
+ dx: Int,
417
+ dy: Int,
418
+ consumed: IntArray?,
419
+ offsetInWindow: IntArray?,
420
+ type: Int
421
+ ): Boolean = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
422
+
423
+ override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean =
424
+ childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
425
+
426
+ override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean =
427
+ childHelper.dispatchNestedPreFling(velocityX, velocityY)
252
428
  }
@@ -0,0 +1,203 @@
1
+ package expo.modules.ui.textfield
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.graphics.Color
6
+ import androidx.compose.foundation.text.BasicTextField
7
+ import androidx.compose.foundation.text.selection.LocalTextSelectionColors
8
+ import androidx.compose.foundation.text.selection.TextSelectionColors
9
+ import androidx.compose.material3.MaterialTheme
10
+ import androidx.compose.runtime.Composable
11
+ import androidx.compose.runtime.CompositionLocalProvider
12
+ import androidx.compose.runtime.compositionLocalOf
13
+ import androidx.compose.ui.graphics.SolidColor
14
+ import androidx.compose.ui.graphics.isUnspecified
15
+ import expo.modules.kotlin.AppContext
16
+ import expo.modules.kotlin.views.AsyncFunctionHandle
17
+ import expo.modules.kotlin.views.AsyncFunctionHandle2
18
+ import expo.modules.kotlin.views.ComposableScope
19
+ import expo.modules.kotlin.views.ComposeProps
20
+ import expo.modules.kotlin.views.ExpoComposeView
21
+ import expo.modules.kotlin.views.FunctionalComposableScope
22
+ import expo.modules.kotlin.views.OptimizedComposeProps
23
+ import expo.modules.ui.GenericEventPayload1
24
+ import expo.modules.ui.ModifierList
25
+ import expo.modules.ui.composeOrNull
26
+ import expo.modules.ui.findChildSlotView
27
+ import expo.modules.ui.renderSlot
28
+ import expo.modules.ui.state.ObservableState
29
+ import expo.modules.ui.state.WorkletCallback
30
+
31
+ // region Inner text field plumbing
32
+
33
+ /**
34
+ * Carries the framework-provided `innerTextField` lambda from `BasicTextField`'s
35
+ * `decorationBox` down to the [InnerTextFieldView] marker, wherever the JS
36
+ * decoration places it. `null` when read outside a decoration (the marker then
37
+ * renders nothing rather than crashing).
38
+ */
39
+ val LocalInnerTextField = compositionLocalOf<(@Composable () -> Unit)?> { null }
40
+
41
+ class InnerTextFieldProps : ComposeProps
42
+
43
+ /**
44
+ * Slot view for `BasicTextField.InnerTextField`.
45
+ */
46
+ @SuppressLint("ViewConstructor")
47
+ class InnerTextFieldView(context: Context, appContext: AppContext) :
48
+ ExpoComposeView<InnerTextFieldProps>(context, appContext) {
49
+ override val props = InnerTextFieldProps()
50
+
51
+ @Composable
52
+ override fun ComposableScope.Content() {
53
+ LocalInnerTextField.current?.invoke()
54
+ }
55
+ }
56
+
57
+ // endregion Inner text field plumbing
58
+
59
+ // region Placeholder plumbing
60
+
61
+ /**
62
+ * Tracks whether textfield is empty, in order to render placeholder slot
63
+ */
64
+ val LocalTextFieldIsEmpty = compositionLocalOf { true }
65
+
66
+ class PlaceholderProps : ComposeProps
67
+
68
+ /**
69
+ * Slot view for `BasicTextField.Placeholder`. Renders its children only while
70
+ * the field is empty.
71
+ */
72
+ @SuppressLint("ViewConstructor")
73
+ class PlaceholderView(context: Context, appContext: AppContext) :
74
+ ExpoComposeView<PlaceholderProps>(context, appContext) {
75
+ override val props = PlaceholderProps()
76
+
77
+ @Composable
78
+ override fun ComposableScope.Content() {
79
+ if (LocalTextFieldIsEmpty.current) {
80
+ Children(this)
81
+ }
82
+ }
83
+ }
84
+
85
+ // endregion Placeholder plumbing
86
+
87
+ // region Props
88
+
89
+ @OptimizedComposeProps
90
+ data class BasicTextFieldProps(
91
+ val value: ObservableState = ObservableState(""),
92
+ val selection: ObservableState = ObservableState(mapOf("start" to 0, "end" to 0)),
93
+ val maxLength: Int? = null,
94
+ val autoFocus: Boolean = false,
95
+ val enabled: Boolean = true,
96
+ val readOnly: Boolean = false,
97
+ val singleLine: Boolean = false,
98
+ val maxLines: Int? = null,
99
+ val minLines: Int? = null,
100
+ val textStyle: TextFieldTextStyleRecord? = null,
101
+ val visualTransformation: String? = null,
102
+ val keyboardOptions: TextFieldKeyboardOptionsRecord? = null,
103
+ val cursorColor: Color? = null,
104
+ val textSelectionColors: TextFieldSelectionColorsRecord? = null,
105
+ val onValueChangeSync: WorkletCallback? = null,
106
+ val modifiers: ModifierList = emptyList()
107
+ ) : ComposeProps
108
+
109
+ // endregion Props
110
+
111
+ // region View
112
+
113
+ @Composable
114
+ fun FunctionalComposableScope.BasicTextFieldContent(
115
+ props: BasicTextFieldProps,
116
+ setText: AsyncFunctionHandle<String>,
117
+ setSelection: AsyncFunctionHandle2<Int, Int>,
118
+ clear: AsyncFunctionHandle<Unit>,
119
+ focus: AsyncFunctionHandle<Unit>,
120
+ blur: AsyncFunctionHandle<Unit>,
121
+ onValueChanged: (TextFieldValuePayload) -> Unit,
122
+ onFocusChange: (GenericEventPayload1<Boolean>) -> Unit,
123
+ onKeyboardActionTriggered: (KeyboardActionEvent) -> Unit,
124
+ onSelectionChanged: (TextFieldSelectionPayload) -> Unit
125
+ ) {
126
+ val core = rememberTextFieldCore(
127
+ value = props.value,
128
+ selection = props.selection,
129
+ maxLength = props.maxLength,
130
+ autoFocus = props.autoFocus,
131
+ keyboardOptionsRecord = props.keyboardOptions,
132
+ modifiers = props.modifiers,
133
+ onValueChangeSync = props.onValueChangeSync,
134
+ setText = setText,
135
+ setSelection = setSelection,
136
+ clear = clear,
137
+ focus = focus,
138
+ blur = blur,
139
+ onValueChanged = onValueChanged,
140
+ onFocusChange = onFocusChange,
141
+ onKeyboardActionTriggered = onKeyboardActionTriggered,
142
+ onSelectionChanged = onSelectionChanged
143
+ )
144
+
145
+ val singleLine = props.singleLine
146
+ val maxLines = props.maxLines ?: if (singleLine) 1 else Int.MAX_VALUE
147
+ val minLines = props.minLines ?: 1
148
+
149
+ val textStyle = props.textStyle.toTextStyle(appContext.reactContext).let {
150
+ if (it.color.isUnspecified) it.copy(color = MaterialTheme.colorScheme.onSurface) else it
151
+ }
152
+ val visualTransformation = props.visualTransformation.toVisualTransformation()
153
+ val cursorBrush = SolidColor(props.cursorColor.composeOrNull ?: MaterialTheme.colorScheme.primary)
154
+
155
+ val current = LocalTextSelectionColors.current
156
+ val selectionColors = props.textSelectionColors?.let { record ->
157
+ val handle = record.handleColor.composeOrNull
158
+ val background = record.backgroundColor.composeOrNull
159
+ if (handle == null && background == null) {
160
+ current
161
+ } else {
162
+ TextSelectionColors(
163
+ handleColor = handle ?: current.handleColor,
164
+ backgroundColor = background ?: handle?.copy(alpha = 0.4f) ?: current.backgroundColor
165
+ )
166
+ }
167
+ } ?: current
168
+
169
+ val decoration: (@Composable () -> Unit)? =
170
+ findChildSlotView(view, "decorationBox")?.let { slot -> { slot.renderSlot() } }
171
+
172
+ CompositionLocalProvider(LocalTextSelectionColors provides selectionColors) {
173
+ BasicTextField(
174
+ value = core.value,
175
+ onValueChange = core.onValueChange,
176
+ modifier = core.modifier,
177
+ enabled = props.enabled,
178
+ readOnly = props.readOnly,
179
+ textStyle = textStyle,
180
+ keyboardOptions = core.keyboardOptions,
181
+ keyboardActions = core.keyboardActions,
182
+ singleLine = singleLine,
183
+ maxLines = maxLines,
184
+ minLines = minLines,
185
+ visualTransformation = visualTransformation,
186
+ cursorBrush = cursorBrush,
187
+ decorationBox = { innerTextField ->
188
+ if (decoration != null) {
189
+ CompositionLocalProvider(
190
+ LocalInnerTextField provides innerTextField,
191
+ LocalTextFieldIsEmpty provides core.value.text.isEmpty()
192
+ ) {
193
+ decoration()
194
+ }
195
+ } else {
196
+ innerTextField()
197
+ }
198
+ }
199
+ )
200
+ }
201
+ }
202
+
203
+ // endregion View